mirror of
https://github.com/mpv-player/mpv
synced 2024-12-24 15:52:25 +00:00
7c07da57e3
The CoreAudio API is built around device IDs so we store the integer as string and read it back.
440 lines
15 KiB
C
440 lines
15 KiB
C
/*
|
|
* This file is part of mpv.
|
|
*
|
|
* mpv is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* mpv 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with mpv. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <CoreAudio/HostTime.h>
|
|
|
|
#include "config.h"
|
|
#include "ao.h"
|
|
#include "internal.h"
|
|
#include "audio/format.h"
|
|
#include "osdep/timer.h"
|
|
#include "options/m_option.h"
|
|
#include "misc/ring.h"
|
|
#include "common/msg.h"
|
|
#include "audio/out/ao_coreaudio_properties.h"
|
|
#include "audio/out/ao_coreaudio_utils.h"
|
|
|
|
struct priv {
|
|
AudioDeviceID device;
|
|
AudioUnit audio_unit;
|
|
|
|
uint64_t hw_latency_us;
|
|
};
|
|
|
|
bool ca_layout_to_mp_chmap(struct ao *ao, AudioChannelLayout *layout,
|
|
struct mp_chmap *chmap);
|
|
|
|
static int64_t ca_frames_to_us(struct ao *ao, uint32_t frames)
|
|
{
|
|
return frames / (float) ao->samplerate * 1e6;
|
|
}
|
|
|
|
static int64_t ca_get_hardware_latency(struct ao *ao) {
|
|
struct priv *p = ao->priv;
|
|
|
|
double audiounit_latency_sec = 0.0;
|
|
uint32_t size = sizeof(audiounit_latency_sec);
|
|
OSStatus err = AudioUnitGetProperty(
|
|
p->audio_unit,
|
|
kAudioUnitProperty_Latency,
|
|
kAudioUnitScope_Global,
|
|
0,
|
|
&audiounit_latency_sec,
|
|
&size);
|
|
CHECK_CA_ERROR("cannot get audio unit latency");
|
|
|
|
uint32_t frames = 0;
|
|
err = CA_GET_O(p->device, kAudioDevicePropertyLatency, &frames);
|
|
CHECK_CA_ERROR("cannot get device latency");
|
|
|
|
uint64_t audiounit_latency_us = audiounit_latency_sec * 1e6;
|
|
uint64_t device_latency_us = ca_frames_to_us(ao, frames);
|
|
|
|
MP_VERBOSE(ao, "audiounit latency [us]: %lld\n", audiounit_latency_us);
|
|
MP_VERBOSE(ao, "device latency [us]: %lld\n", device_latency_us);
|
|
|
|
return audiounit_latency_us + device_latency_us;
|
|
|
|
coreaudio_error:
|
|
return 0;
|
|
}
|
|
|
|
static int64_t ca_get_latency(const AudioTimeStamp *ts)
|
|
{
|
|
uint64_t out = AudioConvertHostTimeToNanos(ts->mHostTime);
|
|
uint64_t now = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime());
|
|
|
|
if (now > out)
|
|
return 0;
|
|
|
|
return (out - now) * 1e-3;
|
|
}
|
|
|
|
static OSStatus render_cb_lpcm(void *ctx, AudioUnitRenderActionFlags *aflags,
|
|
const AudioTimeStamp *ts, UInt32 bus,
|
|
UInt32 frames, AudioBufferList *buffer_list)
|
|
{
|
|
struct ao *ao = ctx;
|
|
struct priv *p = ao->priv;
|
|
AudioBuffer buf = buffer_list->mBuffers[0];
|
|
|
|
int64_t end = mp_time_us();
|
|
end += p->hw_latency_us + ca_get_latency(ts) + ca_frames_to_us(ao, frames);
|
|
|
|
ao_read_data(ao, &buf.mData, frames, end);
|
|
return noErr;
|
|
}
|
|
|
|
static int get_volume(struct ao *ao, struct ao_control_vol *vol) {
|
|
struct priv *p = ao->priv;
|
|
float auvol;
|
|
OSStatus err =
|
|
AudioUnitGetParameter(p->audio_unit, kHALOutputParam_Volume,
|
|
kAudioUnitScope_Global, 0, &auvol);
|
|
|
|
CHECK_CA_ERROR("could not get HAL output volume");
|
|
vol->left = vol->right = auvol * 100.0;
|
|
return CONTROL_TRUE;
|
|
coreaudio_error:
|
|
return CONTROL_ERROR;
|
|
}
|
|
|
|
static int set_volume(struct ao *ao, struct ao_control_vol *vol) {
|
|
struct priv *p = ao->priv;
|
|
float auvol = (vol->left + vol->right) / 200.0;
|
|
OSStatus err =
|
|
AudioUnitSetParameter(p->audio_unit, kHALOutputParam_Volume,
|
|
kAudioUnitScope_Global, 0, auvol, 0);
|
|
CHECK_CA_ERROR("could not set HAL output volume");
|
|
return CONTROL_TRUE;
|
|
coreaudio_error:
|
|
return CONTROL_ERROR;
|
|
}
|
|
|
|
static int control(struct ao *ao, enum aocontrol cmd, void *arg)
|
|
{
|
|
switch (cmd) {
|
|
case AOCONTROL_GET_VOLUME:
|
|
return get_volume(ao, arg);
|
|
case AOCONTROL_SET_VOLUME:
|
|
return set_volume(ao, arg);
|
|
case AOCONTROL_HAS_SOFT_VOLUME:
|
|
return CONTROL_TRUE;
|
|
case AOCONTROL_HAS_PER_APP_VOLUME:
|
|
return CONTROL_TRUE;
|
|
}
|
|
return CONTROL_UNKNOWN;
|
|
}
|
|
|
|
static bool init_chmap(struct ao *ao);
|
|
static bool init_audiounit(struct ao *ao, AudioStreamBasicDescription asbd);
|
|
|
|
static int init(struct ao *ao)
|
|
{
|
|
struct priv *p = ao->priv;
|
|
|
|
OSStatus err = ca_select_device(ao, ao->device, &p->device);
|
|
CHECK_CA_ERROR("failed to select device");
|
|
|
|
if (!init_chmap(ao))
|
|
goto coreaudio_error;
|
|
|
|
ao->format = af_fmt_from_planar(ao->format);
|
|
|
|
AudioStreamBasicDescription asbd;
|
|
ca_fill_asbd(ao, &asbd);
|
|
|
|
if (!init_audiounit(ao, asbd))
|
|
goto coreaudio_error;
|
|
|
|
return CONTROL_OK;
|
|
|
|
coreaudio_error:
|
|
return CONTROL_ERROR;
|
|
}
|
|
|
|
static bool init_chmap(struct ao *ao)
|
|
{
|
|
struct priv *p = ao->priv;
|
|
OSStatus err;
|
|
AudioChannelLayout *layouts;
|
|
size_t n_layouts;
|
|
|
|
err = CA_GET_ARY_O(p->device,
|
|
kAudioDevicePropertyPreferredChannelLayout,
|
|
&layouts, &n_layouts);
|
|
CHECK_CA_ERROR("could not get audio device prefered layouts");
|
|
|
|
struct mp_chmap_sel chmap_sel = {0};
|
|
for (int i = 0; i < n_layouts; i++) {
|
|
struct mp_chmap chmap = {0};
|
|
if (ca_layout_to_mp_chmap(ao, &layouts[i], &chmap))
|
|
mp_chmap_sel_add_map(&chmap_sel, &chmap);
|
|
}
|
|
|
|
talloc_free(layouts);
|
|
|
|
if (ao->channels.num < 3) {
|
|
struct mp_chmap chmap;
|
|
mp_chmap_from_channels(&chmap, ao->channels.num);
|
|
mp_chmap_sel_add_map(&chmap_sel, &chmap);
|
|
}
|
|
|
|
if (!ao_chmap_sel_adjust(ao, &chmap_sel, &ao->channels)) {
|
|
MP_ERR(ao, "could not select a suitable channel map among the "
|
|
"hardware supported ones. Make sure to configure your "
|
|
"output device correctly in 'Audio MIDI Setup.app'\n");
|
|
goto coreaudio_error;
|
|
}
|
|
|
|
return true;
|
|
|
|
coreaudio_error:
|
|
return false;
|
|
}
|
|
|
|
static bool init_audiounit(struct ao *ao, AudioStreamBasicDescription asbd)
|
|
{
|
|
OSStatus err;
|
|
uint32_t size;
|
|
struct priv *p = ao->priv;
|
|
|
|
AudioComponentDescription desc = (AudioComponentDescription) {
|
|
.componentType = kAudioUnitType_Output,
|
|
.componentSubType = (ao->device) ?
|
|
kAudioUnitSubType_HALOutput :
|
|
kAudioUnitSubType_DefaultOutput,
|
|
.componentManufacturer = kAudioUnitManufacturer_Apple,
|
|
.componentFlags = 0,
|
|
.componentFlagsMask = 0,
|
|
};
|
|
|
|
AudioComponent comp = AudioComponentFindNext(NULL, &desc);
|
|
if (comp == NULL) {
|
|
MP_ERR(ao, "unable to find audio component\n");
|
|
goto coreaudio_error;
|
|
}
|
|
|
|
err = AudioComponentInstanceNew(comp, &(p->audio_unit));
|
|
CHECK_CA_ERROR("unable to open audio component");
|
|
|
|
err = AudioUnitInitialize(p->audio_unit);
|
|
CHECK_CA_ERROR_L(coreaudio_error_component,
|
|
"unable to initialize audio unit");
|
|
|
|
size = sizeof(AudioStreamBasicDescription);
|
|
err = AudioUnitSetProperty(p->audio_unit,
|
|
kAudioUnitProperty_StreamFormat,
|
|
kAudioUnitScope_Input, 0, &asbd, size);
|
|
|
|
CHECK_CA_ERROR_L(coreaudio_error_audiounit,
|
|
"unable to set the input format on the audio unit");
|
|
|
|
err = AudioUnitSetProperty(p->audio_unit,
|
|
kAudioOutputUnitProperty_CurrentDevice,
|
|
kAudioUnitScope_Global, 0, &p->device,
|
|
sizeof(p->device));
|
|
CHECK_CA_ERROR_L(coreaudio_error_audiounit,
|
|
"can't link audio unit to selected device");
|
|
|
|
p->hw_latency_us = ca_get_hardware_latency(ao);
|
|
|
|
AURenderCallbackStruct render_cb = (AURenderCallbackStruct) {
|
|
.inputProc = render_cb_lpcm,
|
|
.inputProcRefCon = ao,
|
|
};
|
|
|
|
err = AudioUnitSetProperty(p->audio_unit,
|
|
kAudioUnitProperty_SetRenderCallback,
|
|
kAudioUnitScope_Input, 0, &render_cb,
|
|
sizeof(AURenderCallbackStruct));
|
|
|
|
CHECK_CA_ERROR_L(coreaudio_error_audiounit,
|
|
"unable to set render callback on audio unit");
|
|
|
|
return true;
|
|
|
|
coreaudio_error_audiounit:
|
|
AudioUnitUninitialize(p->audio_unit);
|
|
coreaudio_error_component:
|
|
AudioComponentInstanceDispose(p->audio_unit);
|
|
coreaudio_error:
|
|
return false;
|
|
}
|
|
|
|
static void stop(struct ao *ao)
|
|
{
|
|
struct priv *p = ao->priv;
|
|
OSStatus err = AudioOutputUnitStop(p->audio_unit);
|
|
CHECK_CA_WARN("can't stop audio unit");
|
|
}
|
|
|
|
static void start(struct ao *ao)
|
|
{
|
|
struct priv *p = ao->priv;
|
|
OSStatus err = AudioOutputUnitStart(p->audio_unit);
|
|
CHECK_CA_WARN("can't start audio unit");
|
|
}
|
|
|
|
|
|
static void uninit(struct ao *ao)
|
|
{
|
|
struct priv *p = ao->priv;
|
|
AudioOutputUnitStop(p->audio_unit);
|
|
AudioUnitUninitialize(p->audio_unit);
|
|
AudioComponentInstanceDispose(p->audio_unit);
|
|
}
|
|
|
|
// Channel Mapping functions
|
|
static const int speaker_map[][2] = {
|
|
{ kAudioChannelLabel_Left, MP_SPEAKER_ID_FL },
|
|
{ kAudioChannelLabel_Right, MP_SPEAKER_ID_FR },
|
|
{ kAudioChannelLabel_Center, MP_SPEAKER_ID_FC },
|
|
{ kAudioChannelLabel_LFEScreen, MP_SPEAKER_ID_LFE },
|
|
{ kAudioChannelLabel_LeftSurround, MP_SPEAKER_ID_BL },
|
|
{ kAudioChannelLabel_RightSurround, MP_SPEAKER_ID_BR },
|
|
{ kAudioChannelLabel_LeftCenter, MP_SPEAKER_ID_FLC },
|
|
{ kAudioChannelLabel_RightCenter, MP_SPEAKER_ID_FRC },
|
|
{ kAudioChannelLabel_CenterSurround, MP_SPEAKER_ID_BC },
|
|
{ kAudioChannelLabel_LeftSurroundDirect, MP_SPEAKER_ID_SL },
|
|
{ kAudioChannelLabel_RightSurroundDirect, MP_SPEAKER_ID_SR },
|
|
{ kAudioChannelLabel_TopCenterSurround, MP_SPEAKER_ID_TC },
|
|
{ kAudioChannelLabel_VerticalHeightLeft, MP_SPEAKER_ID_TFL },
|
|
{ kAudioChannelLabel_VerticalHeightCenter, MP_SPEAKER_ID_TFC },
|
|
{ kAudioChannelLabel_VerticalHeightRight, MP_SPEAKER_ID_TFR },
|
|
{ kAudioChannelLabel_TopBackLeft, MP_SPEAKER_ID_TBL },
|
|
{ kAudioChannelLabel_TopBackCenter, MP_SPEAKER_ID_TBC },
|
|
{ kAudioChannelLabel_TopBackRight, MP_SPEAKER_ID_TBR },
|
|
|
|
// unofficial extensions
|
|
{ kAudioChannelLabel_RearSurroundLeft, MP_SPEAKER_ID_SDL },
|
|
{ kAudioChannelLabel_RearSurroundRight, MP_SPEAKER_ID_SDR },
|
|
{ kAudioChannelLabel_LeftWide, MP_SPEAKER_ID_WL },
|
|
{ kAudioChannelLabel_RightWide, MP_SPEAKER_ID_WR },
|
|
{ kAudioChannelLabel_LFE2, MP_SPEAKER_ID_LFE2 },
|
|
|
|
{ kAudioChannelLabel_HeadphonesLeft, MP_SPEAKER_ID_DL },
|
|
{ kAudioChannelLabel_HeadphonesRight, MP_SPEAKER_ID_DR },
|
|
|
|
{ kAudioChannelLabel_Unknown, -1 },
|
|
};
|
|
|
|
static int ca_label_to_mp_speaker_id(AudioChannelLabel label)
|
|
{
|
|
for (int i = 0; speaker_map[i][1] >= 0; i++)
|
|
if (speaker_map[i][0] == label)
|
|
return speaker_map[i][1];
|
|
return -1;
|
|
}
|
|
|
|
static void ca_log_layout(struct ao *ao, AudioChannelLayout *layout)
|
|
{
|
|
if (!mp_msg_test(ao->log, MSGL_V))
|
|
return;
|
|
|
|
AudioChannelDescription *descs = layout->mChannelDescriptions;
|
|
|
|
MP_VERBOSE(ao, "layout: tag: <%d>, bitmap: <%d>, "
|
|
"descriptions <%d>\n",
|
|
layout->mChannelLayoutTag,
|
|
layout->mChannelBitmap,
|
|
layout->mNumberChannelDescriptions);
|
|
|
|
for (int i = 0; i < layout->mNumberChannelDescriptions; i++) {
|
|
AudioChannelDescription d = descs[i];
|
|
MP_VERBOSE(ao, " - description %d: label <%d, %d>, flags: <%u>, "
|
|
"coords: <%f, %f, %f>\n", i,
|
|
d.mChannelLabel,
|
|
ca_label_to_mp_speaker_id(d.mChannelLabel),
|
|
d.mChannelFlags,
|
|
d.mCoordinates[0],
|
|
d.mCoordinates[1],
|
|
d.mCoordinates[2]);
|
|
}
|
|
}
|
|
|
|
bool ca_layout_to_mp_chmap(struct ao *ao, AudioChannelLayout *layout,
|
|
struct mp_chmap *chmap)
|
|
{
|
|
AudioChannelLayoutTag tag = layout->mChannelLayoutTag;
|
|
uint32_t layout_size = sizeof(layout);
|
|
OSStatus err;
|
|
|
|
if (tag == kAudioChannelLayoutTag_UseChannelBitmap) {
|
|
err = AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForBitmap,
|
|
sizeof(uint32_t),
|
|
&layout->mChannelBitmap,
|
|
&layout_size,
|
|
layout);
|
|
CHECK_CA_ERROR("failed to convert channel bitmap to descriptions");
|
|
} else if (tag != kAudioChannelLayoutTag_UseChannelDescriptions) {
|
|
err = AudioFormatGetProperty(kAudioFormatProperty_ChannelLayoutForTag,
|
|
sizeof(AudioChannelLayoutTag),
|
|
&layout->mChannelLayoutTag,
|
|
&layout_size,
|
|
layout);
|
|
CHECK_CA_ERROR("failed to convert channel tag to descriptions");
|
|
}
|
|
|
|
ca_log_layout(ao, layout);
|
|
|
|
// If the channel layout uses channel descriptions, from my
|
|
// experiments there are there three possibile cases:
|
|
// * The description has a label kAudioChannelLabel_Unknown:
|
|
// Can't do anything about this (looks like non surround
|
|
// layouts are like this).
|
|
// * The description uses positional information: this in
|
|
// theory could be used but one would have to map spatial
|
|
// positions to labels which is not really feasible.
|
|
// * The description has a well known label which can be mapped
|
|
// to the waveextensible definition: this is the kind of
|
|
// descriptions we process here.
|
|
|
|
for (int n = 0; n < layout->mNumberChannelDescriptions; n++) {
|
|
AudioChannelLabel label = layout->mChannelDescriptions[n].mChannelLabel;
|
|
uint8_t speaker = ca_label_to_mp_speaker_id(label);
|
|
if (label == kAudioChannelLabel_Unknown)
|
|
continue;
|
|
if (speaker < 0) {
|
|
MP_VERBOSE(ao, "channel label=%d unusable to build channel "
|
|
"bitmap, skipping layout\n", label);
|
|
} else {
|
|
chmap->speaker[n] = speaker;
|
|
chmap->num = n + 1;
|
|
}
|
|
}
|
|
|
|
return chmap->num > 0;
|
|
coreaudio_error:
|
|
ca_log_layout(ao, layout);
|
|
return false;
|
|
}
|
|
|
|
#define OPT_BASE_STRUCT struct priv
|
|
|
|
const struct ao_driver audio_out_coreaudio = {
|
|
.description = "CoreAudio AudioUnit",
|
|
.name = "coreaudio",
|
|
.uninit = uninit,
|
|
.init = init,
|
|
.control = control,
|
|
.pause = stop,
|
|
.resume = start,
|
|
.list_devs = ca_get_device_list,
|
|
.priv_size = sizeof(struct priv),
|
|
};
|