Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544/* * This file is part of mpv. * * mpv 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. * * 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mpv. If not, see <http://www.gnu.org/licenses/>. */
/* * This file contains functions interacting with the CoreAudio framework * that are not specific to the AUHAL. These are split in a separate file for * the sake of readability. In the future the could be used by other AOs based * on CoreAudio but not the AUHAL (such as using AudioQueue services). */
#include "audio/out/ao_coreaudio_utils.h"#include "osdep/timer.h"#include "osdep/endian.h"#include "audio/format.h"#include "osdep/mac/compat.h"
#if HAVE_COREAUDIO || HAVE_AVFOUNDATION#include "audio/out/ao_coreaudio_properties.h"#include <CoreAudio/HostTime.h>#else#include <mach/mach_time.h>#endif
#if HAVE_COREAUDIO || HAVE_AVFOUNDATIONstatic bool ca_is_output_device(struct ao *ao, AudioDeviceID dev){ size_t n_buffers; AudioBufferList *buffers; const ca_scope scope = kAudioDevicePropertyStreamConfiguration; OSStatus err = CA_GET_ARY_O(dev, scope, &buffers, &n_buffers); if (err != noErr) return false; talloc_free(buffers); return n_buffers > 0;}
void ca_get_device_list(struct ao *ao, struct ao_device_list *list){ AudioDeviceID *devs; size_t n_devs; OSStatus err = CA_GET_ARY(kAudioObjectSystemObject, kAudioHardwarePropertyDevices, &devs, &n_devs); CHECK_CA_ERROR("Failed to get list of output devices."); for (int i = 0; i < n_devs; i++) { if (!ca_is_output_device(ao, devs[i])) continue; void *ta_ctx = talloc_new(NULL); char *name; char *desc; err = CA_GET_STR(devs[i], kAudioDevicePropertyDeviceUID, &name); if (err != noErr) { MP_VERBOSE(ao, "skipping device %d, which has no UID\n", i); talloc_free(ta_ctx); continue; } talloc_steal(ta_ctx, name); err = CA_GET_STR(devs[i], kAudioObjectPropertyName, &desc); if (err != noErr) desc = talloc_strdup(NULL, "Unknown"); talloc_steal(ta_ctx, desc); ao_device_list_add(list, ao, &(struct ao_device_desc){name, desc}); talloc_free(ta_ctx); } talloc_free(devs);coreaudio_error: return;}
OSStatus ca_select_device(struct ao *ao, char* name, AudioDeviceID *device){ OSStatus err = noErr; *device = kAudioObjectUnknown;
if (name && name[0]) { CFStringRef uid = cfstr_from_cstr(name); AudioValueTranslation v = (AudioValueTranslation) { .mInputData = &uid, .mInputDataSize = sizeof(CFStringRef), .mOutputData = device, .mOutputDataSize = sizeof(*device), }; uint32_t size = sizeof(AudioValueTranslation); AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { .mSelector = kAudioHardwarePropertyDeviceForUID, .mScope = kAudioObjectPropertyScopeGlobal, .mElement = kAudioObjectPropertyElementMain, }; err = AudioObjectGetPropertyData( kAudioObjectSystemObject, &p_addr, 0, 0, &size, &v); CFRelease(uid); CHECK_CA_ERROR("unable to query for device UID");
uint32_t is_alive = 1; err = CA_GET(*device, kAudioDevicePropertyDeviceIsAlive, &is_alive); CHECK_CA_ERROR("could not check whether device is alive (invalid device?)");
if (!is_alive) MP_WARN(ao, "device is not alive!\n"); } else { // device not set by user, get the default one err = CA_GET(kAudioObjectSystemObject, kAudioHardwarePropertyDefaultOutputDevice, device); CHECK_CA_ERROR("could not get default audio device"); }
if (mp_msg_test(ao->log, MSGL_V)) { char *desc; OSStatus err2 = CA_GET_STR(*device, kAudioObjectPropertyName, &desc); if (err2 == noErr) { MP_VERBOSE(ao, "selected audio output device: %s (%" PRIu32 ")\n", desc, *device); talloc_free(desc); } }
coreaudio_error: return err;}#endif
bool check_ca_st(struct ao *ao, int level, OSStatus code, const char *message){ if (code == noErr) return true;
if (ao) mp_msg(ao->log, level, "%s (%s/%d)\n", message, mp_tag_str(code), (int)code);
return false;}
static void ca_fill_asbd_raw(AudioStreamBasicDescription *asbd, int mp_format, int samplerate, int num_channels){ asbd->mSampleRate = samplerate; // Set "AC3" for other spdif formats too - unknown if that works. asbd->mFormatID = af_fmt_is_spdif(mp_format) ? kAudioFormat60958AC3 : kAudioFormatLinearPCM; asbd->mChannelsPerFrame = num_channels; asbd->mBitsPerChannel = af_fmt_to_bytes(mp_format) * 8; asbd->mFormatFlags = kAudioFormatFlagIsPacked;
int channels_per_buffer = num_channels; if (af_fmt_is_planar(mp_format)) { asbd->mFormatFlags |= kAudioFormatFlagIsNonInterleaved; channels_per_buffer = 1; }
if (af_fmt_is_float(mp_format)) { asbd->mFormatFlags |= kAudioFormatFlagIsFloat; } else if (!af_fmt_is_unsigned(mp_format)) { asbd->mFormatFlags |= kAudioFormatFlagIsSignedInteger; }
if (BYTE_ORDER == BIG_ENDIAN) asbd->mFormatFlags |= kAudioFormatFlagIsBigEndian;
asbd->mFramesPerPacket = 1; asbd->mBytesPerPacket = asbd->mBytesPerFrame = asbd->mFramesPerPacket * channels_per_buffer * (asbd->mBitsPerChannel / 8);}
void ca_fill_asbd(struct ao *ao, AudioStreamBasicDescription *asbd){ ca_fill_asbd_raw(asbd, ao->format, ao->samplerate, ao->channels.num);}
bool ca_formatid_is_compressed(uint32_t formatid){ switch (formatid) case 'IAC3': case 'iac3': case kAudioFormat60958AC3: case kAudioFormatAC3: return true; return false;}
// This might be wrong, but for now it's sufficient for us.static uint32_t ca_normalize_formatid(uint32_t formatID){ return ca_formatid_is_compressed(formatID) ? kAudioFormat60958AC3 : formatID;}
bool ca_asbd_equals(const AudioStreamBasicDescription *a, const AudioStreamBasicDescription *b){ int flags = kAudioFormatFlagIsPacked | kAudioFormatFlagIsFloat | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsBigEndian; bool spdif = ca_formatid_is_compressed(a->mFormatID) && ca_formatid_is_compressed(b->mFormatID);
return (a->mFormatFlags & flags) == (b->mFormatFlags & flags) && a->mBitsPerChannel == b->mBitsPerChannel && ca_normalize_formatid(a->mFormatID) == ca_normalize_formatid(b->mFormatID) && (spdif || a->mBytesPerPacket == b->mBytesPerPacket) && (spdif || a->mChannelsPerFrame == b->mChannelsPerFrame) && a->mSampleRate == b->mSampleRate;}
// Return the AF_FORMAT_* (AF_FORMAT_S16 etc.) corresponding to the asbd.int ca_asbd_to_mp_format(const AudioStreamBasicDescription *asbd){ for (int fmt = 1; fmt < AF_FORMAT_COUNT; fmt++) { AudioStreamBasicDescription mp_asbd = {0}; ca_fill_asbd_raw(&mp_asbd, fmt, asbd->mSampleRate, asbd->mChannelsPerFrame); if (ca_asbd_equals(&mp_asbd, asbd)) return af_fmt_is_spdif(fmt) ? AF_FORMAT_S_AC3 : fmt; } return 0;}
void ca_print_asbd(struct ao *ao, const char *description, const AudioStreamBasicDescription *asbd){ uint32_t flags = asbd->mFormatFlags; char *format = mp_tag_str(asbd->mFormatID); int mpfmt = ca_asbd_to_mp_format(asbd);
MP_VERBOSE(ao, "%s %7.1fHz %" PRIu32 "bit %s " "[%" PRIu32 "][%" PRIu32 "bpp][%" PRIu32 "fbp]" "[%" PRIu32 "bpf][%" PRIu32 "ch] " "%s %s %s%s%s%s (%s)\n", description, asbd->mSampleRate, asbd->mBitsPerChannel, format, asbd->mFormatFlags, asbd->mBytesPerPacket, asbd->mFramesPerPacket, asbd->mBytesPerFrame, asbd->mChannelsPerFrame, (flags & kAudioFormatFlagIsFloat) ? "float" : "int", (flags & kAudioFormatFlagIsBigEndian) ? "BE" : "LE", (flags & kAudioFormatFlagIsSignedInteger) ? "S" : "U", (flags & kAudioFormatFlagIsPacked) ? " packed" : "", (flags & kAudioFormatFlagIsAlignedHigh) ? " aligned" : "", (flags & kAudioFormatFlagIsNonInterleaved) ? " P" : "", mpfmt ? af_fmt_to_str(mpfmt) : "-");}
// Return whether new is an improvement over old. Assume a higher value means// better quality, and we always prefer the value closest to the requested one,// which is still larger than the requested one.// Equal values prefer the new one (so ca_asbd_is_better() checks other params).static bool value_is_better(double req, double old, double new){ if (new >= req) { return old < req || new <= old; } else { return old < req && new >= old; }}
// Return whether new is an improvement over old (req is the requested format).bool ca_asbd_is_better(AudioStreamBasicDescription *req, AudioStreamBasicDescription *old, AudioStreamBasicDescription *new){ if (new->mChannelsPerFrame > MP_NUM_CHANNELS) return false; if (old->mChannelsPerFrame > MP_NUM_CHANNELS) return true; if (req->mFormatID != new->mFormatID) return false; if (req->mFormatID != old->mFormatID) return true;
if (!value_is_better(req->mBitsPerChannel, old->mBitsPerChannel, new->mBitsPerChannel)) return false;
if (!value_is_better(req->mSampleRate, old->mSampleRate, new->mSampleRate)) return false;
if (!value_is_better(req->mChannelsPerFrame, old->mChannelsPerFrame, new->mChannelsPerFrame)) return false;
return true;}
int64_t ca_frames_to_ns(struct ao *ao, uint32_t frames){ return MP_TIME_S_TO_NS(frames / (double)ao->samplerate);}
int64_t ca_get_latency(const AudioTimeStamp *ts){#if HAVE_COREAUDIO || HAVE_AVFOUNDATION uint64_t out = AudioConvertHostTimeToNanos(ts->mHostTime); uint64_t now = AudioConvertHostTimeToNanos(AudioGetCurrentHostTime());
if (now > out) return 0;
return out - now;#else static mach_timebase_info_data_t timebase; if (timebase.denom == 0) mach_timebase_info(&timebase);
uint64_t out = ts->mHostTime; uint64_t now = mach_absolute_time();
if (now > out) return 0;
return (out - now) * timebase.numer / timebase.denom;#endif}
#if HAVE_COREAUDIO || HAVE_AVFOUNDATIONbool ca_stream_supports_compressed(struct ao *ao, AudioStreamID stream){ AudioStreamRangedDescription *formats = NULL; size_t n_formats;
OSStatus err = CA_GET_ARY(stream, kAudioStreamPropertyAvailablePhysicalFormats, &formats, &n_formats);
CHECK_CA_ERROR("Could not get number of stream formats.");
for (int i = 0; i < n_formats; i++) { AudioStreamBasicDescription asbd = formats[i].mFormat;
ca_print_asbd(ao, "- ", &asbd);
if (ca_formatid_is_compressed(asbd.mFormatID)) { talloc_free(formats); return true; } }
talloc_free(formats);coreaudio_error: return false;}
OSStatus ca_lock_device(AudioDeviceID device, pid_t *pid){ *pid = getpid(); OSStatus err = CA_SET(device, kAudioDevicePropertyHogMode, pid); if (err != noErr) *pid = -1;
return err;}
OSStatus ca_unlock_device(AudioDeviceID device, pid_t *pid){ if (*pid == getpid()) { *pid = -1; return CA_SET(device, kAudioDevicePropertyHogMode, &pid); } return noErr;}
static OSStatus ca_change_mixing(struct ao *ao, AudioDeviceID device, uint32_t val, bool *changed){ *changed = false;
AudioObjectPropertyAddress p_addr = (AudioObjectPropertyAddress) { .mSelector = kAudioDevicePropertySupportsMixing, .mScope = kAudioObjectPropertyScopeGlobal, .mElement = kAudioObjectPropertyElementMain, };
if (AudioObjectHasProperty(device, &p_addr)) { OSStatus err; Boolean writeable = 0; err = CA_SETTABLE(device, kAudioDevicePropertySupportsMixing, &writeable);
if (!CHECK_CA_WARN("can't tell if mixing property is settable")) { return err; }
if (!writeable) return noErr;
err = CA_SET(device, kAudioDevicePropertySupportsMixing, &val); if (err != noErr) return err;
if (!CHECK_CA_WARN("can't set mix mode")) { return err; }
*changed = true; }
return noErr;}
OSStatus ca_disable_mixing(struct ao *ao, AudioDeviceID device, bool *changed){ return ca_change_mixing(ao, device, 0, changed);}
OSStatus ca_enable_mixing(struct ao *ao, AudioDeviceID device, bool changed){ if (changed) { bool dont_care = false; return ca_change_mixing(ao, device, 1, &dont_care); }
return noErr;}
int64_t ca_get_device_latency_ns(struct ao *ao, AudioDeviceID device){ uint32_t latency_frames = 0; uint32_t latency_properties[] = { kAudioDevicePropertyLatency, kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertySafetyOffset, }; for (int n = 0; n < MP_ARRAY_SIZE(latency_properties); n++) { uint32_t temp; OSStatus err = CA_GET_O(device, latency_properties[n], &temp); CHECK_CA_WARN("cannot get device latency"); if (err == noErr) { latency_frames += temp; MP_VERBOSE(ao, "Latency property %s: %d frames\n", mp_tag_str(latency_properties[n]), (int)temp); } }
double sample_rate; OSStatus err = CA_GET_O(device, kAudioDevicePropertyNominalSampleRate, &sample_rate); CHECK_CA_WARN("cannot get device sample rate, falling back to AO sample rate!"); if (err == noErr) { MP_VERBOSE(ao, "Device sample rate: %f\n", sample_rate); } else { sample_rate = ao->samplerate; }
return MP_TIME_S_TO_NS(latency_frames / sample_rate);}
static OSStatus ca_change_format_listener( AudioObjectID object, uint32_t n_addresses, const AudioObjectPropertyAddress addresses[], void *data){ struct coreaudio_cb_sem *sem = data; mp_mutex_lock(&sem->mutex); mp_cond_broadcast(&sem->cond); mp_mutex_unlock(&sem->mutex); return noErr;}
bool ca_change_physical_format_sync(struct ao *ao, AudioStreamID stream, AudioStreamBasicDescription change_format){ struct coreaudio_cb_sem *sem = ao->priv;
OSStatus err = noErr; bool format_set = false;
ca_print_asbd(ao, "setting stream physical format:", &change_format);
AudioStreamBasicDescription prev_format; err = CA_GET(stream, kAudioStreamPropertyPhysicalFormat, &prev_format); CHECK_CA_ERROR("can't get current physical format");
ca_print_asbd(ao, "format in use before switching:", &prev_format);
/* Install the callback. */ AudioObjectPropertyAddress p_addr = { .mSelector = kAudioStreamPropertyPhysicalFormat, .mScope = kAudioObjectPropertyScopeGlobal, .mElement = kAudioObjectPropertyElementMain, };
err = AudioObjectAddPropertyListener(stream, &p_addr, ca_change_format_listener, sem); CHECK_CA_ERROR("can't add property listener during format change");
mp_mutex_lock(&sem->mutex);
/* Change the format. */ err = CA_SET(stream, kAudioStreamPropertyPhysicalFormat, &change_format); CHECK_CA_WARN("error changing physical format");
/* The AudioStreamSetProperty is not only asynchronous, * it is also not Atomic, in its behaviour. */ int64_t wait_until = mp_time_ns() + MP_TIME_S_TO_NS(2); AudioStreamBasicDescription actual_format = {0}; while (1) { err = CA_GET(stream, kAudioStreamPropertyPhysicalFormat, &actual_format); if (!CHECK_CA_WARN("could not retrieve physical format")) break;
format_set = ca_asbd_equals(&change_format, &actual_format); if (format_set) break;
if (mp_cond_timedwait_until(&sem->cond, &sem->mutex, wait_until)) { MP_VERBOSE(ao, "reached timeout\n"); break; } }
mp_mutex_unlock(&sem->mutex);
ca_print_asbd(ao, "actual format in use:", &actual_format);
if (!format_set) { MP_WARN(ao, "changing physical format failed\n"); // Some drivers just fuck up and get into a broken state. Restore the // old format in this case. err = CA_SET(stream, kAudioStreamPropertyPhysicalFormat, &prev_format); CHECK_CA_WARN("error restoring physical format"); }
err = AudioObjectRemovePropertyListener(stream, &p_addr, ca_change_format_listener, sem); CHECK_CA_ERROR("can't remove property listener");
coreaudio_error: return format_set;}#endif