mirror of
https://git.ffmpeg.org/ffmpeg.git
synced 2024-12-21 23:10:13 +00:00
790f793844
There are lots of files that don't need it: The number of object files that actually need it went down from 2011 to 884 here. Keep it for external users in order to not cause breakages. Also improve the other headers a bit while just at it. Signed-off-by: Andreas Rheinhardt <andreas.rheinhardt@outlook.com>
315 lines
14 KiB
Objective-C
315 lines
14 KiB
Objective-C
/*
|
|
* 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/mem.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,
|
|
};
|