| /* | |
| * 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; | |
| #if !TARGET_OS_IPHONE && __MAC_OS_X_VERSION_MIN_REQUIRED >= 120000 | |
| prop.mElement = kAudioObjectPropertyElementMain; | |
| #else | |
| prop.mElement = kAudioObjectPropertyElementMaster; | |
| #endif | |
| 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, | |
| }; |