mirror of https://github.com/FFmpeg/FFmpeg.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
309 lines
14 KiB
309 lines
14 KiB
/* |
|
* AudioToolbox output device |
|
* Copyright (c) 2020 Thilo Borgmann <thilo.borgmann@mail.de> |
|
* |
|
* This file is part of FFmpeg. |
|
* |
|
* FFmpeg is free software; you can redistribute it and/or |
|
* modify it under the terms of the GNU Lesser General Public |
|
* License as published by the Free Software Foundation; either |
|
* version 2.1 of the License, or (at your option) any later version. |
|
* |
|
* FFmpeg is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
* Lesser General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU Lesser General Public |
|
* License along with FFmpeg; if not, write to the Free Software |
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
|
*/ |
|
|
|
/** |
|
* @file |
|
* AudioToolbox output device |
|
* @author Thilo Borgmann <thilo.borgmann@mail.de> |
|
*/ |
|
|
|
#import <AudioToolbox/AudioToolbox.h> |
|
#include <pthread.h> |
|
|
|
#include "libavutil/opt.h" |
|
#include "libavformat/internal.h" |
|
#include "libavformat/mux.h" |
|
#include "libavutil/internal.h" |
|
#include "avdevice.h" |
|
|
|
typedef struct |
|
{ |
|
AVClass *class; |
|
|
|
AudioQueueBufferRef buffer[2]; |
|
pthread_mutex_t buffer_lock[2]; |
|
int cur_buf; |
|
AudioQueueRef queue; |
|
|
|
int list_devices; |
|
int audio_device_index; |
|
|
|
} ATContext; |
|
|
|
static int check_status(AVFormatContext *avctx, OSStatus *status, const char *msg) |
|
{ |
|
if (*status != noErr) { |
|
av_log(avctx, AV_LOG_ERROR, "Error: %s (%i)\n", msg, *status); |
|
return 1; |
|
} else { |
|
av_log(avctx, AV_LOG_DEBUG, " OK : %s\n", msg); |
|
return 0; |
|
} |
|
} |
|
|
|
static void queue_callback(void* atctx, AudioQueueRef inAQ, |
|
AudioQueueBufferRef inBuffer) |
|
{ |
|
// unlock the buffer that has just been consumed |
|
ATContext *ctx = (ATContext*)atctx; |
|
for (int i = 0; i < 2; i++) { |
|
if (inBuffer == ctx->buffer[i]) { |
|
pthread_mutex_unlock(&ctx->buffer_lock[i]); |
|
} |
|
} |
|
} |
|
|
|
static av_cold int at_write_header(AVFormatContext *avctx) |
|
{ |
|
ATContext *ctx = (ATContext*)avctx->priv_data; |
|
OSStatus err = noErr; |
|
CFStringRef device_UID = NULL; |
|
AudioDeviceID *devices; |
|
int num_devices; |
|
|
|
|
|
// get devices |
|
UInt32 data_size = 0; |
|
AudioObjectPropertyAddress prop; |
|
prop.mSelector = kAudioHardwarePropertyDevices; |
|
prop.mScope = kAudioObjectPropertyScopeGlobal; |
|
prop.mElement = kAudioObjectPropertyElementMaster; |
|
err = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &prop, 0, NULL, &data_size); |
|
if (check_status(avctx, &err, "AudioObjectGetPropertyDataSize devices")) |
|
return AVERROR(EINVAL); |
|
|
|
num_devices = data_size / sizeof(AudioDeviceID); |
|
|
|
devices = (AudioDeviceID*)(av_malloc(data_size)); |
|
err = AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop, 0, NULL, &data_size, devices); |
|
if (check_status(avctx, &err, "AudioObjectGetPropertyData devices")) { |
|
av_freep(&devices); |
|
return AVERROR(EINVAL); |
|
} |
|
|
|
// list devices |
|
if (ctx->list_devices) { |
|
CFStringRef device_name = NULL; |
|
prop.mScope = kAudioDevicePropertyScopeInput; |
|
|
|
av_log(ctx, AV_LOG_INFO, "CoreAudio devices:\n"); |
|
for(UInt32 i = 0; i < num_devices; ++i) { |
|
// UID |
|
data_size = sizeof(device_UID); |
|
prop.mSelector = kAudioDevicePropertyDeviceUID; |
|
err = AudioObjectGetPropertyData(devices[i], &prop, 0, NULL, &data_size, &device_UID); |
|
if (check_status(avctx, &err, "AudioObjectGetPropertyData UID")) |
|
continue; |
|
|
|
// name |
|
data_size = sizeof(device_name); |
|
prop.mSelector = kAudioDevicePropertyDeviceNameCFString; |
|
err = AudioObjectGetPropertyData(devices[i], &prop, 0, NULL, &data_size, &device_name); |
|
if (check_status(avctx, &err, "AudioObjecTGetPropertyData name")) |
|
continue; |
|
|
|
av_log(ctx, AV_LOG_INFO, "[%d] %30s, %s\n", i, |
|
CFStringGetCStringPtr(device_name, kCFStringEncodingMacRoman), |
|
CFStringGetCStringPtr(device_UID, kCFStringEncodingMacRoman)); |
|
} |
|
} |
|
|
|
// get user-defined device UID or use default device |
|
// -audio_device_index overrides any URL given |
|
const char *stream_name = avctx->url; |
|
if (stream_name && ctx->audio_device_index == -1) { |
|
sscanf(stream_name, "%d", &ctx->audio_device_index); |
|
} |
|
|
|
if (ctx->audio_device_index >= 0) { |
|
// get UID of selected device |
|
data_size = sizeof(device_UID); |
|
prop.mSelector = kAudioDevicePropertyDeviceUID; |
|
err = AudioObjectGetPropertyData(devices[ctx->audio_device_index], &prop, 0, NULL, &data_size, &device_UID); |
|
if (check_status(avctx, &err, "AudioObjecTGetPropertyData UID")) { |
|
av_freep(&devices); |
|
return AVERROR(EINVAL); |
|
} |
|
} else { |
|
// use default device |
|
device_UID = NULL; |
|
} |
|
|
|
av_log(ctx, AV_LOG_DEBUG, "stream_name: %s\n", stream_name); |
|
av_log(ctx, AV_LOG_DEBUG, "audio_device_idnex: %i\n", ctx->audio_device_index); |
|
av_log(ctx, AV_LOG_DEBUG, "UID: %s\n", CFStringGetCStringPtr(device_UID, kCFStringEncodingMacRoman)); |
|
|
|
// check input stream |
|
if (avctx->nb_streams != 1 || avctx->streams[0]->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) { |
|
av_log(ctx, AV_LOG_ERROR, "Only a single audio stream is supported.\n"); |
|
return AVERROR(EINVAL); |
|
} |
|
|
|
av_freep(&devices); |
|
AVCodecParameters *codecpar = avctx->streams[0]->codecpar; |
|
|
|
// audio format |
|
AudioStreamBasicDescription device_format = {0}; |
|
device_format.mSampleRate = codecpar->sample_rate; |
|
device_format.mFormatID = kAudioFormatLinearPCM; |
|
device_format.mFormatFlags |= (codecpar->format == AV_SAMPLE_FMT_FLT) ? kLinearPCMFormatFlagIsFloat : 0; |
|
device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_S8) ? kLinearPCMFormatFlagIsSignedInteger : 0; |
|
device_format.mFormatFlags |= (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S16BE, AV_CODEC_ID_PCM_S16LE)) ? kLinearPCMFormatFlagIsSignedInteger : 0; |
|
device_format.mFormatFlags |= (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S24BE, AV_CODEC_ID_PCM_S24LE)) ? kLinearPCMFormatFlagIsSignedInteger : 0; |
|
device_format.mFormatFlags |= (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S32BE, AV_CODEC_ID_PCM_S32LE)) ? kLinearPCMFormatFlagIsSignedInteger : 0; |
|
device_format.mFormatFlags |= (av_sample_fmt_is_planar(codecpar->format)) ? kAudioFormatFlagIsNonInterleaved : 0; |
|
device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_F32BE) ? kAudioFormatFlagIsBigEndian : 0; |
|
device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_S16BE) ? kAudioFormatFlagIsBigEndian : 0; |
|
device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_S24BE) ? kAudioFormatFlagIsBigEndian : 0; |
|
device_format.mFormatFlags |= (codecpar->codec_id == AV_CODEC_ID_PCM_S32BE) ? kAudioFormatFlagIsBigEndian : 0; |
|
device_format.mChannelsPerFrame = codecpar->ch_layout.nb_channels; |
|
device_format.mBitsPerChannel = (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S24BE, AV_CODEC_ID_PCM_S24LE)) ? 24 : (av_get_bytes_per_sample(codecpar->format) << 3); |
|
device_format.mBytesPerFrame = (device_format.mBitsPerChannel >> 3) * device_format.mChannelsPerFrame; |
|
device_format.mFramesPerPacket = 1; |
|
device_format.mBytesPerPacket = device_format.mBytesPerFrame * device_format.mFramesPerPacket; |
|
device_format.mReserved = 0; |
|
|
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mSampleRate = %i\n", codecpar->sample_rate); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatID = %s\n", "kAudioFormatLinearPCM"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->format == AV_SAMPLE_FMT_FLT) ? "kLinearPCMFormatFlagIsFloat" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_S8) ? "kLinearPCMFormatFlagIsSignedInteger" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S32BE, AV_CODEC_ID_PCM_S32LE)) ? "kLinearPCMFormatFlagIsSignedInteger" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S16BE, AV_CODEC_ID_PCM_S16LE)) ? "kLinearPCMFormatFlagIsSignedInteger" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_NE(AV_CODEC_ID_PCM_S24BE, AV_CODEC_ID_PCM_S24LE)) ? "kLinearPCMFormatFlagIsSignedInteger" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (av_sample_fmt_is_planar(codecpar->format)) ? "kAudioFormatFlagIsNonInterleaved" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_F32BE) ? "kAudioFormatFlagIsBigEndian" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_S16BE) ? "kAudioFormatFlagIsBigEndian" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_S24BE) ? "kAudioFormatFlagIsBigEndian" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags |= %s\n", (codecpar->codec_id == AV_CODEC_ID_PCM_S32BE) ? "kAudioFormatFlagIsBigEndian" : "0"); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFormatFlags == %i\n", device_format.mFormatFlags); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mChannelsPerFrame = %i\n", codecpar->ch_layout.nb_channels); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mBitsPerChannel = %i\n", av_get_bytes_per_sample(codecpar->format) << 3); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mBytesPerFrame = %i\n", (device_format.mBitsPerChannel >> 3) * codecpar->ch_layout.nb_channels); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mBytesPerPacket = %i\n", device_format.mBytesPerFrame); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mFramesPerPacket = %i\n", 1); |
|
av_log(ctx, AV_LOG_DEBUG, "device_format.mReserved = %i\n", 0); |
|
|
|
// create new output queue for the device |
|
err = AudioQueueNewOutput(&device_format, queue_callback, ctx, |
|
NULL, kCFRunLoopCommonModes, |
|
0, &ctx->queue); |
|
if (check_status(avctx, &err, "AudioQueueNewOutput")) { |
|
if (err == kAudioFormatUnsupportedDataFormatError) |
|
av_log(ctx, AV_LOG_ERROR, "Unsupported output format.\n"); |
|
return AVERROR(EINVAL); |
|
} |
|
|
|
// set user-defined device or leave untouched for default |
|
if (device_UID != NULL) { |
|
err = AudioQueueSetProperty(ctx->queue, kAudioQueueProperty_CurrentDevice, &device_UID, sizeof(device_UID)); |
|
if (check_status(avctx, &err, "AudioQueueSetProperty output UID")) |
|
return AVERROR(EINVAL); |
|
} |
|
|
|
// start the queue |
|
err = AudioQueueStart(ctx->queue, NULL); |
|
if (check_status(avctx, &err, "AudioQueueStart")) |
|
return AVERROR(EINVAL); |
|
|
|
// init the mutexes for double-buffering |
|
pthread_mutex_init(&ctx->buffer_lock[0], NULL); |
|
pthread_mutex_init(&ctx->buffer_lock[1], NULL); |
|
|
|
return 0; |
|
} |
|
|
|
static int at_write_packet(AVFormatContext *avctx, AVPacket *pkt) |
|
{ |
|
ATContext *ctx = (ATContext*)avctx->priv_data; |
|
OSStatus err = noErr; |
|
|
|
// use the other buffer |
|
ctx->cur_buf = !ctx->cur_buf; |
|
|
|
// lock for writing or wait for the buffer to be available |
|
// will be unlocked by queue callback |
|
pthread_mutex_lock(&ctx->buffer_lock[ctx->cur_buf]); |
|
|
|
// (re-)allocate the buffer if not existant or of different size |
|
if (!ctx->buffer[ctx->cur_buf] || ctx->buffer[ctx->cur_buf]->mAudioDataBytesCapacity != pkt->size) { |
|
err = AudioQueueAllocateBuffer(ctx->queue, pkt->size, &ctx->buffer[ctx->cur_buf]); |
|
if (check_status(avctx, &err, "AudioQueueAllocateBuffer")) { |
|
pthread_mutex_unlock(&ctx->buffer_lock[ctx->cur_buf]); |
|
return AVERROR(ENOMEM); |
|
} |
|
} |
|
|
|
AudioQueueBufferRef buf = ctx->buffer[ctx->cur_buf]; |
|
|
|
// copy audio data into buffer and enqueue the buffer |
|
memcpy(buf->mAudioData, pkt->data, buf->mAudioDataBytesCapacity); |
|
buf->mAudioDataByteSize = buf->mAudioDataBytesCapacity; |
|
err = AudioQueueEnqueueBuffer(ctx->queue, buf, 0, NULL); |
|
if (check_status(avctx, &err, "AudioQueueEnqueueBuffer")) { |
|
pthread_mutex_unlock(&ctx->buffer_lock[ctx->cur_buf]); |
|
return AVERROR(EINVAL); |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static av_cold int at_write_trailer(AVFormatContext *avctx) |
|
{ |
|
ATContext *ctx = (ATContext*)avctx->priv_data; |
|
OSStatus err = noErr; |
|
|
|
pthread_mutex_destroy(&ctx->buffer_lock[0]); |
|
pthread_mutex_destroy(&ctx->buffer_lock[1]); |
|
|
|
err = AudioQueueFlush(ctx->queue); |
|
check_status(avctx, &err, "AudioQueueFlush"); |
|
err = AudioQueueDispose(ctx->queue, true); |
|
check_status(avctx, &err, "AudioQueueDispose"); |
|
|
|
return 0; |
|
} |
|
|
|
static const AVOption options[] = { |
|
{ "list_devices", "list available audio devices", offsetof(ATContext, list_devices), AV_OPT_TYPE_BOOL, {.i64=0}, 0, 1, AV_OPT_FLAG_ENCODING_PARAM }, |
|
{ "audio_device_index", "select audio device by index (starts at 0)", offsetof(ATContext, audio_device_index), AV_OPT_TYPE_INT, {.i64 = -1}, -1, INT_MAX, AV_OPT_FLAG_ENCODING_PARAM }, |
|
{ NULL }, |
|
}; |
|
|
|
static const AVClass at_class = { |
|
.class_name = "AudioToolbox", |
|
.item_name = av_default_item_name, |
|
.option = options, |
|
.version = LIBAVUTIL_VERSION_INT, |
|
.category = AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT, |
|
}; |
|
|
|
const FFOutputFormat ff_audiotoolbox_muxer = { |
|
.p.name = "audiotoolbox", |
|
.p.long_name = NULL_IF_CONFIG_SMALL("AudioToolbox output device"), |
|
.priv_data_size = sizeof(ATContext), |
|
.p.audio_codec = AV_NE(AV_CODEC_ID_PCM_S16BE, AV_CODEC_ID_PCM_S16LE), |
|
.p.video_codec = AV_CODEC_ID_NONE, |
|
.write_header = at_write_header, |
|
.write_packet = at_write_packet, |
|
.write_trailer = at_write_trailer, |
|
.p.flags = AVFMT_NOFILE, |
|
.p.priv_class = &at_class, |
|
};
|
|
|