2020-06-15 15:09:33 +02:00
/ *
* 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"
2023-01-27 16:06:00 +02:00
# include "libavformat/mux.h"
2020-06-15 15:09:33 +02:00
# 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 ;
2022-11-06 23:29:48 +02:00
device_format . mChannelsPerFrame = codecpar -> ch_layout . nb_channels ;
2020-06-15 15:09:33 +02:00
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 ) ;
2022-11-06 23:29:48 +02:00
av_log ( ctx , AV_LOG _DEBUG , "device_format.mChannelsPerFrame = %i\n" , codecpar -> ch_layout . nb_channels ) ;
2020-06-15 15:09:33 +02:00
av_log ( ctx , AV_LOG _DEBUG , "device_format.mBitsPerChannel = %i\n" , av_get _bytes _per _sample ( codecpar -> format ) < < 3 ) ;
2022-11-06 23:29:48 +02:00
av_log ( ctx , AV_LOG _DEBUG , "device_format.mBytesPerFrame = %i\n" , ( device_format . mBitsPerChannel > > 3 ) * codecpar -> ch_layout . nb_channels ) ;
2020-06-15 15:09:33 +02:00
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 ,
} ;
2023-01-27 16:06:00 +02:00
const FFOutputFormat ff_audiotoolbox _muxer = {
. p . name = "audiotoolbox" ,
. p . long_name = NULL_IF _CONFIG _SMALL ( "AudioToolbox output device" ) ,
2020-06-15 15:09:33 +02:00
. priv_data _size = sizeof ( ATContext ) ,
2023-01-27 16:06:00 +02:00
. p . audio_codec = AV_NE ( AV_CODEC _ID _PCM _S16BE , AV_CODEC _ID _PCM _S16LE ) ,
. p . video_codec = AV_CODEC _ID _NONE ,
2020-06-15 15:09:33 +02:00
. write_header = at_write _header ,
. write_packet = at_write _packet ,
. write_trailer = at_write _trailer ,
2023-01-27 16:06:00 +02:00
. p . flags = AVFMT_NOFILE ,
. p . priv_class = & at_class ,
2020-06-15 15:09:33 +02:00
} ;