From patchwork Mon Jun 8 19:45:05 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Thilo Borgmann X-Patchwork-Id: 20221 Return-Path: X-Original-To: patchwork@ffaux-bg.ffmpeg.org Delivered-To: patchwork@ffaux-bg.ffmpeg.org Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org [79.124.17.100]) by ffaux.localdomain (Postfix) with ESMTP id A3CFA44AF8B for ; Mon, 8 Jun 2020 22:45:13 +0300 (EEST) Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id 7006968B19B; Mon, 8 Jun 2020 22:45:13 +0300 (EEST) X-Original-To: ffmpeg-devel@ffmpeg.org Delivered-To: ffmpeg-devel@ffmpeg.org Received: from shout01.mail.de (shout01.mail.de [62.201.172.24]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id E375C68A41E for ; Mon, 8 Jun 2020 22:45:06 +0300 (EEST) Received: from postfix01.mail.de (postfix03.bt.mail.de [10.0.121.127]) by shout01.mail.de (Postfix) with ESMTP id 8950B100616 for ; Mon, 8 Jun 2020 21:45:06 +0200 (CEST) Received: from smtp01.mail.de (smtp01.bt.mail.de [10.0.121.211]) by postfix01.mail.de (Postfix) with ESMTP id 7244A801D1 for ; Mon, 8 Jun 2020 21:45:06 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=mail.de; s=mailde201610; t=1591645506; bh=QA0B9LUvduJ9JSL3YA0FNqG+j1eDrIFafgzrG7Gc5ZY=; h=To:From:Subject:Date:From; b=lTJYp17BX8WAcQtoluttyfKwhUPnuDn3sQosBlJWIbKTooq590K2j2kf9ujEIEalp Xr0VetH8bz7AkYC3Nt6bBtbkEMlFiMCSJLLTAh/G6GVOU84UAaX1tBm7oKcElUpvaX ae6CoiAgqZU231ZwpOyAuoS7+vtP3MuAAf38LucA= Received: from [127.0.0.1] (localhost [127.0.0.1]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)) (No client certificate requested) by smtp01.mail.de (Postfix) with ESMTPSA id 12CA210035D for ; Mon, 8 Jun 2020 21:45:06 +0200 (CEST) To: FFmpeg development discussions and patches From: Thilo Borgmann Autocrypt: addr=thilo.borgmann@mail.de; prefer-encrypt=mutual; keydata= xsBNBF1NyqgBCAC/eJ7l/Ic+5bhj3N4eTyLRMCQVyxPF6sIDu0RqZCsLYx6GjH6BBrPcewbG wLfWNqWPWpcHLbOwDE2FWSBgDbc7Zwrmtkxt4+RNj2/JYRyCiV1uIRm1rlcf80pqDEYM+1O8 23b/JrONn7pQpX8alrYfhP3tLR2PKO/wHmG4AlSEG5T8angmGMvfKVSzxq7E07MIdUqiHHj/ t/F+6WFkpaec++CWcU+9s9IWRV+HskgihqZBqvzRYptvFi81con+VqpYjtpqoYAj4XwqM6j4 /8kTobgPo0n/QRCFcpZYRA7SCT4LbwOvy+qZ1e5372rzOcIF6PGdVzH4CCRT1Ypg1D6TABEB AAHNJ1RoaWxvIEJvcmdtYW5uIDx0aGlsby5ib3JnbWFubkBtYWlsLmRlPsLAqwQTAQoAPhYh BM4dt/RNIPw63Z/+WiV8W48dILkvBQJdTcqoAhsDBQkDwmcABQsJCAcDBRUKCQgLBRYCAwEA Ah4BAheAACEJECV8W48dILkvFiEEzh239E0g/Drdn/5aJXxbjx0guS/UtAf+JkEY+fQ29Gcv 509tfZW+uvTEmXzvIWmMP7TGYHGnHc/swhB8IY25J7/Z88qUxD0tTjnTHlNEEk4ZYAwHrgfv G/epK/AccKmPTj/FsmoUp4cRLwq17rQv2nc8L7i/Yg6syYTHaRF0SCQ8TPvcSoC4hP3IBWZj t23R0TyDeSnvK/+IeIsNejgz37Q+bWBtWsvUl6EBB0y51T6dp1RjsBS3Ot4FkFOqzMqCc3y1 808khhLgBnU24LnA62RMcUfvgMIRx0w8ioj1xJyp3U6c/JFhHxlLW5uVnOZ/odhuf+Hnel2u wyRCQxI2dLhBDZ9LVbospxZQok2GgRJIK6cnX23afc7ATQRdTcqoAQgAxQIqcXiQTaIFAWXc 9YEGuhrQF+TmKF7Jt1IGFmvfdmofnoFZ/8teZB03XP1VSVI/IXM7CTsEfQlBMgGcc9Tj3c9Z v+De/FfjBLJYOYKJIleIfhIZBLr6Hxru2qGma+piKXjbJCBJabSIu4YDM6iH3D2GJwanSspc ee3yvTSR7we1hcoavemw3pseurg6Y4dWsKEJLNP2xZ0C7fyidQsIh7RtNuzKsSVqqx1YbBwO 4cek4gQfJ9UC0nvt9X/pX1COhDeNAsTI8h2iC13Gi14a+EWF8ppvK0V3i5VDaxWrreUDY1Nt L6qGiQte/X4tvzRLEIe1gpT5wRK/QVXARlF8lQARAQABwsCTBBgBCgAmFiEEzh239E0g/Drd n/5aJXxbjx0guS8FAl1NyqgCGwwFCQPCZwAAIQkQJXxbjx0guS8WIQTOHbf0TSD8Ot2f/lol fFuPHSC5L+GLCACkU8DKD+HOEPvxTftn3pQ6YE+yfwSeS1uBjH6D2SsXdTrAZEE9KNjqYExr PuMy1GxAH3CeD+Kqc0sra5I9s3AdNAM1YeHQbY4zWaHfcaeU/cF/c2MxyhnRbr/UF382PUY5 yoWAmIBomSZdaifXXuU/CosZkpk01LDfl5fx5SfF13TrZ1FtPbXmP5S0n2HzxLM64hF0RzVz zR8xEofnP7jWJGcO7KcUCqSy64KGgNlYzyCWso+Tf0AJOtWYkfRvI/MyruItQ31jRs7VRh67 /lX38m5iqqjjwQ2GNZq29qGCNmjmsBjgzMmoy+5jUuoy233waGIGRFt1g9pLiMmchdWR Message-ID: Date: Mon, 8 Jun 2020 21:45:05 +0200 MIME-Version: 1.0 Content-Language: en-US X-purgate: clean X-purgate: This mail is considered clean (visit http://www.eleven.de for further information) X-purgate-type: clean X-purgate-Ad: Categorized by eleven eXpurgate (R) http://www.eleven.de X-purgate: This mail is considered clean (visit http://www.eleven.de for further information) X-purgate: clean X-purgate-size: 26935 X-purgate-ID: 154282::1591645506-000036FF-CC7A2FE1/0/0 Subject: [FFmpeg-devel] [PATCH v3] lavdevice: Add AudioToolbox output device. X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.20 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: FFmpeg development discussions and patches Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" $subject, v3. -Thilo From c9002ece648aa59b4334efe9fea83d265d64e258 Mon Sep 17 00:00:00 2001 From: Thilo Borgmann Date: Mon, 8 Jun 2020 21:42:27 +0200 Subject: [PATCH] lavdevice: Add AudioToolbox output device. --- Changelog | 1 + configure | 3 + doc/outdevs.texi | 46 ++++++ libavdevice/Makefile | 1 + libavdevice/alldevices.c | 1 + libavdevice/audiotoolbox.m | 309 +++++++++++++++++++++++++++++++++++++ 6 files changed, 361 insertions(+) create mode 100644 libavdevice/audiotoolbox.m diff --git a/Changelog b/Changelog index e9781131a3..061be6f882 100644 --- a/Changelog +++ b/Changelog @@ -70,6 +70,7 @@ version : - NotchLC decoder - gradients source video filter - MediaFoundation encoder wrapper +- AudioToolbox output device version 4.2: diff --git a/configure b/configure index f97cad0298..1aab998d8e 100755 --- a/configure +++ b/configure @@ -3366,6 +3366,8 @@ alsa_outdev_deps="alsa" avfoundation_indev_deps="avfoundation corevideo coremedia pthreads" avfoundation_indev_suggest="coregraphics applicationservices" avfoundation_indev_extralibs="-framework Foundation" +audiotoolbox_outdev_deps="audiotoolbox pthreads" +audiotoolbox_outdev_extralibs="-framework AudioToolbox -framework CoreAudio" bktr_indev_deps_any="dev_bktr_ioctl_bt848_h machine_ioctl_bt848_h dev_video_bktr_ioctl_bt848_h dev_ic_bt8xx_h" caca_outdev_deps="libcaca" decklink_deps_any="libdl LoadLibrary" @@ -6151,6 +6153,7 @@ enabled videotoolbox && check_apple_framework VideoToolbox check_apple_framework CoreFoundation check_apple_framework CoreMedia check_apple_framework CoreVideo +check_apple_framework CoreAudio enabled avfoundation && { disable coregraphics applicationservices diff --git a/doc/outdevs.texi b/doc/outdevs.texi index 60606eb6e7..aaf247995c 100644 --- a/doc/outdevs.texi +++ b/doc/outdevs.texi @@ -38,6 +38,52 @@ ffmpeg -i INPUT -f alsa hw:1,7 @end example @end itemize +@section AudioToolbox + +AudioToolbox output device. + +Allows native output to CoreAudio devices on OSX. + +The output filename can be empty (or @code{-}) to refer to the default system output device or a number that refers to the device index as shown using: @code{-list_devices true}. + +Alternatively, the audio input device can be chosen by index using the +@option{ + -audio_device_index +} +, overriding any device name or index given in the input filename. + +All available devices can be enumerated by using @option{-list_devices true}, listing +all device names, UIDs and corresponding indices. + +@subsection Options + +AudioToolbox supports the following options: + +@table @option + +@item -audio_device_index +Specify the audio device by its index. Overrides anything given in the output filename. + +@end table + +@subsection Examples + +@itemize + +@item +Print the list of supported devices and output a sine wave to the default device: +@example +$ ffmpeg -f lavfi -i sine=r=44100 -f audiotoolbox -list_devices true - +@end example + +@item +Output a sine wave to the device with the index 2, overriding any output filename: +@example +$ ffmpeg -f lavfi -i sine=r=44100 -f audiotoolbox -audio_device_index 2 - +@end example + +@end itemize + @section caca CACA output device. diff --git a/libavdevice/Makefile b/libavdevice/Makefile index 6ea62b914e..0dfe47a1f4 100644 --- a/libavdevice/Makefile +++ b/libavdevice/Makefile @@ -15,6 +15,7 @@ OBJS-$(CONFIG_SHARED) += reverse.o OBJS-$(CONFIG_ALSA_INDEV) += alsa_dec.o alsa.o timefilter.o OBJS-$(CONFIG_ALSA_OUTDEV) += alsa_enc.o alsa.o OBJS-$(CONFIG_ANDROID_CAMERA_INDEV) += android_camera.o +OBJS-$(CONFIG_AUDIOTOOLBOX_OUTDEV) += audiotoolbox.o OBJS-$(CONFIG_AVFOUNDATION_INDEV) += avfoundation.o OBJS-$(CONFIG_BKTR_INDEV) += bktr.o OBJS-$(CONFIG_CACA_OUTDEV) += caca.o diff --git a/libavdevice/alldevices.c b/libavdevice/alldevices.c index 8633433254..a6f68dd3bb 100644 --- a/libavdevice/alldevices.c +++ b/libavdevice/alldevices.c @@ -27,6 +27,7 @@ extern AVInputFormat ff_alsa_demuxer; extern AVOutputFormat ff_alsa_muxer; extern AVInputFormat ff_android_camera_demuxer; +extern AVOutputFormat ff_audiotoolbox_muxer; extern AVInputFormat ff_avfoundation_demuxer; extern AVInputFormat ff_bktr_demuxer; extern AVOutputFormat ff_caca_muxer; diff --git a/libavdevice/audiotoolbox.m b/libavdevice/audiotoolbox.m new file mode 100644 index 0000000000..c8ac729c2b --- /dev/null +++ b/libavdevice/audiotoolbox.m @@ -0,0 +1,309 @@ +/* + * AudioToolbox output device + * Copyright (c) 2020 Thilo Borgmann + * + * 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 + */ + +#import +#include + +#include "libavutil/opt.h" +#include "libavformat/internal.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->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->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->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, +}; + +AVOutputFormat ff_audiotoolbox_muxer = { + .name = "audiotoolbox", + .long_name = NULL_IF_CONFIG_SMALL("AudioToolbox output device"), + .priv_data_size = sizeof(ATContext), + .audio_codec = AV_NE(AV_CODEC_ID_PCM_S16BE, AV_CODEC_ID_PCM_S16LE), + .video_codec = AV_CODEC_ID_NONE, + .write_header = at_write_header, + .write_packet = at_write_packet, + .write_trailer = at_write_trailer, + .flags = AVFMT_NOFILE, + .priv_class = &at_class, +}; +