mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-08 02:08:22 -10:00
coreaudio: the microphone joins coreaudio.c - one C driver for macOS, iOS and tvOS
Two microphone drivers as Objective-C files, 1600 lines between them, and neither needed to be Objective-C. coreaudio_mic_macos.m had no Objective-C in it at all - CoreFoundation strings are C. The iOS one had a single block: the AVAudioSession put into a record category with the session's rate read back. That is the Cocoa layer's object, not the driver's, and now lives there as cocoa_audio_session_begin_record() in ui_cocoatouch.m, declared in a plain C header; the driver calls it under TARGET_OS_IPHONE and stays C. The two drivers were the same driver twice: an input-enabled output unit, HALOutput or RemoteIO, whose input callback pulls each slice with AudioUnitRender into a fifo the frontend reads, with a lock the callback only try-locks and a timed condition wait for the reader. The merged one is that once, ~650 lines at the end of coreaudio.c, opening its unit through the same runtime-resolved component API as playback. macOS keeps everything it had: input devices listed with their UIDs riding in attr.p, a saved setting matched by UID first and name second, the default input device followed through a HAL property listener with the reconnect done on the reader's thread, a 256-frame device period, the client format at the device's own rate. iOS and tvOS keep the session category with the options and reasons they had. Both gain the other's fixes: the driver context is its own allocation, latching the non-blocking state so a mic opened later inherits it, and open() no longer starts the unit - start() does, as the frontend expects and as macOS already did; the iOS driver had started at open and again at start. Griffin loses the two includes, Makefile.common the object and the AVFoundation link the macOS microphone never needed. C90: every declaration at the top of its block, checked as gnu89 with mixed declarations an error, for macOS and for iOS. Verified on Linux with the real file linked against stubs of the Apple calls and a capture thread handing over a sample ramp: the device list with UIDs; open by UID, by name and by default; 8 KB of blocking reads with the ramp intact; the default device changing under a running mic and the reconnect landing on the new one with the mic still alive; non-blocking reads; stop and close, under ASan and UBSan. The earlier playback harnesses run unchanged. Wants a Mac and an iOS build; the Cocoa function is not compiled here.
This commit is contained in:
1 parent
f207426e73
commit
9fed0dabe1
7 files changed
+753
-1618
No files matched your search
+1
-11
@@ -806,14 +806,6 @@ endif
|
||||
ifeq ($(HAVE_COREAUDIO), 1)
|
||||
DEFINES += -DHAVE_COREAUDIO
|
||||
OBJ += audio/drivers/coreaudio.o
|
||||
ifeq ($(HAVE_MICROPHONE), 1)
|
||||
OBJ += audio/drivers/coreaudio_mic_macos.o
|
||||
# AVFoundation is 10.7+ and only needed by the microphone driver.
|
||||
# The base coreaudio.c path doesn't use it, so keep it out of the
|
||||
# link line on targets without microphone support (e.g. pre-10.7
|
||||
# / Tiger / Leopard / PowerPC).
|
||||
LIBS += -framework AVFoundation
|
||||
endif
|
||||
HAVE_COREAUDIO_LIBS = 1
|
||||
LIBS += -framework AudioToolbox
|
||||
endif
|
||||
@@ -2386,9 +2378,7 @@ endif
|
||||
# AVFoundation camera + recording (Apple, macOS 10.7+ / iOS). The
|
||||
# 10.7 gate is enforced in qb/config.libs.sh (see the HAVE_AVF block
|
||||
# there); this section just wires the object files and frameworks in
|
||||
# when HAVE_AVF is on. Note that HAVE_MICROPHONE above also adds
|
||||
# -framework AVFoundation for its own reasons; make tolerates the
|
||||
# repeat and the linker de-duplicates.
|
||||
# when HAVE_AVF is on.
|
||||
ifeq ($(HAVE_AVF), 1)
|
||||
DEFINES += -DHAVE_AVF
|
||||
OBJ += camera/drivers/avfoundation.o \
|
||||
|
||||
@@ -1045,3 +1045,672 @@ audio_driver_t audio_coreaudio = {
|
||||
coreaudio_wait_writable
|
||||
};
|
||||
|
||||
|
||||
#ifdef HAVE_MICROPHONE
|
||||
/* =====================================================================
|
||||
* Microphone. One driver for macOS, iOS and tvOS, C like the rest of
|
||||
* the file: an input-enabled output unit - HALOutput or RemoteIO - whose
|
||||
* input callback pulls each slice with AudioUnitRender into a fifo the
|
||||
* frontend reads. macOS adds device selection by UID or name, an input
|
||||
* device list, and following the default input device when none was
|
||||
* named; iOS and tvOS add nothing but the session category, which is
|
||||
* Objective-C and lives in the Cocoa layer behind
|
||||
* cocoa_audio_session_begin_record().
|
||||
*
|
||||
* The fifo is guarded by a lock the callback only try-locks: a slice
|
||||
* that finds the lock held is dropped rather than making the real-time
|
||||
* thread wait. The reader blocks on the condition the callback signals,
|
||||
* timed, in blocking mode.
|
||||
* ===================================================================== */
|
||||
#include <queues/fifo_queue.h>
|
||||
#include <rthreads/rthreads.h>
|
||||
#include <string.h>
|
||||
#include "../microphone_driver.h"
|
||||
#if TARGET_OS_IPHONE
|
||||
#include "../../ui/drivers/cocoa/cocoa_audio_session.h"
|
||||
#endif
|
||||
|
||||
typedef struct coreaudio_mic
|
||||
{
|
||||
AudioUnit unit;
|
||||
fifo_buffer_t *fifo;
|
||||
slock_t *lock;
|
||||
scond_t *cond;
|
||||
AudioStreamBasicDescription format;
|
||||
retro_atomic_int_t running;
|
||||
retro_atomic_int_t initialized;
|
||||
void *cb_buf;
|
||||
size_t cb_buf_size;
|
||||
unsigned sample_rate;
|
||||
bool nonblock;
|
||||
bool use_float;
|
||||
#if !TARGET_OS_IPHONE
|
||||
AudioDeviceID device;
|
||||
bool follow_default;
|
||||
retro_atomic_int_t device_changed;
|
||||
#endif
|
||||
} coreaudio_mic_t;
|
||||
|
||||
/* Driver-wide context: created by init(), destroyed by free(). The
|
||||
* frontend holds it for as long as the driver is selected, longer than
|
||||
* any microphone opened through it, so it is never the same allocation
|
||||
* as one. It latches the non-blocking state so a mic opened later
|
||||
* inherits it. */
|
||||
typedef struct coreaudio_mic_driver
|
||||
{
|
||||
coreaudio_mic_t *mic;
|
||||
bool nonblock;
|
||||
} coreaudio_mic_driver_t;
|
||||
|
||||
static void coreaudio_mic_close(void *driver_context, void *mic_context);
|
||||
static bool coreaudio_mic_stop(void *driver_context, void *mic_context);
|
||||
|
||||
static void coreaudio_mic_set_format(coreaudio_mic_t *mic, bool use_float)
|
||||
{
|
||||
AudioStreamBasicDescription *f = &mic->format;
|
||||
mic->use_float = use_float;
|
||||
memset(f, 0, sizeof(*f));
|
||||
f->mSampleRate = mic->sample_rate;
|
||||
f->mFormatID = kAudioFormatLinearPCM;
|
||||
f->mFormatFlags = use_float
|
||||
? (kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked)
|
||||
: (kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked);
|
||||
f->mFramesPerPacket = 1;
|
||||
f->mChannelsPerFrame = 1; /* mono */
|
||||
f->mBitsPerChannel = use_float ? 32 : 16;
|
||||
f->mBytesPerFrame = f->mChannelsPerFrame * f->mBitsPerChannel / 8;
|
||||
f->mBytesPerPacket = f->mBytesPerFrame * f->mFramesPerPacket;
|
||||
}
|
||||
|
||||
/* The input callback, on the real-time thread. */
|
||||
static OSStatus coreaudio_mic_input_cb(void *ref,
|
||||
AudioUnitRenderActionFlags *flags, const AudioTimeStamp *ts,
|
||||
UInt32 bus, UInt32 frames, AudioBufferList *io_data)
|
||||
{
|
||||
coreaudio_mic_t *mic = (coreaudio_mic_t*)ref;
|
||||
AudioBufferList list;
|
||||
size_t bytes;
|
||||
|
||||
(void)bus;
|
||||
(void)io_data;
|
||||
|
||||
if (!mic || !retro_atomic_load_acquire_int(&mic->running))
|
||||
return noErr;
|
||||
|
||||
bytes = (size_t)frames * mic->format.mBytesPerFrame;
|
||||
/* A slice larger than the buffer sized from MaximumFramesPerSlice
|
||||
* is dropped rather than allocated for on this thread. */
|
||||
if (!bytes || bytes > mic->cb_buf_size)
|
||||
return noErr;
|
||||
|
||||
list.mNumberBuffers = 1;
|
||||
list.mBuffers[0].mNumberChannels = mic->format.mChannelsPerFrame;
|
||||
list.mBuffers[0].mDataByteSize = (UInt32)bytes;
|
||||
list.mBuffers[0].mData = mic->cb_buf;
|
||||
|
||||
if (AudioUnitRender(mic->unit, flags, ts, 1, frames, &list) == noErr
|
||||
&& list.mBuffers[0].mData && list.mBuffers[0].mDataByteSize)
|
||||
{
|
||||
size_t got = list.mBuffers[0].mDataByteSize;
|
||||
if (got > bytes)
|
||||
got = bytes;
|
||||
/* Never wait here: a reader holding the lock costs one slice. */
|
||||
if (slock_try_lock(mic->lock))
|
||||
{
|
||||
if (FIFO_WRITE_AVAIL(mic->fifo) >= got)
|
||||
fifo_write(mic->fifo, list.mBuffers[0].mData, got);
|
||||
scond_signal(mic->cond);
|
||||
slock_unlock(mic->lock);
|
||||
}
|
||||
}
|
||||
/* Always noErr: an error return can stop the callbacks for good. */
|
||||
return noErr;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
/* --- macOS: HAL input devices ------------------------------------- */
|
||||
|
||||
static bool coreaudio_mic_device_has_input(AudioDeviceID id)
|
||||
{
|
||||
AudioObjectPropertyAddress prop;
|
||||
UInt32 size = 0;
|
||||
prop.mSelector = kAudioDevicePropertyStreams;
|
||||
prop.mScope = kAudioDevicePropertyScopeInput;
|
||||
prop.mElement = CA_ELEMENT_MAIN;
|
||||
return AudioObjectGetPropertyDataSize(id, &prop, 0, NULL, &size) == noErr
|
||||
&& size > 0;
|
||||
}
|
||||
|
||||
/* A CFString property of a device as UTF-8; false if absent. */
|
||||
static bool coreaudio_mic_device_string(AudioDeviceID id,
|
||||
UInt32 selector, char *s, size_t len)
|
||||
{
|
||||
AudioObjectPropertyAddress prop;
|
||||
CFStringRef cf = NULL;
|
||||
UInt32 size = sizeof(cf);
|
||||
bool ok;
|
||||
prop.mSelector = selector;
|
||||
prop.mScope = kAudioObjectPropertyScopeGlobal;
|
||||
prop.mElement = CA_ELEMENT_MAIN;
|
||||
s[0] = 0;
|
||||
if (AudioObjectGetPropertyData(id, &prop, 0, NULL, &size, &cf) != noErr || !cf)
|
||||
return false;
|
||||
ok = CFStringGetCString(cf, s, (CFIndex)len, kCFStringEncodingUTF8) && s[0];
|
||||
CFRelease(cf);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static AudioDeviceID coreaudio_mic_default_device(void)
|
||||
{
|
||||
AudioObjectPropertyAddress prop;
|
||||
AudioDeviceID id = kAudioObjectUnknown;
|
||||
UInt32 size = sizeof(id);
|
||||
prop.mSelector = kAudioHardwarePropertyDefaultInputDevice;
|
||||
prop.mScope = kAudioObjectPropertyScopeGlobal;
|
||||
prop.mElement = CA_ELEMENT_MAIN;
|
||||
if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop,
|
||||
0, NULL, &size, &id) != noErr)
|
||||
return kAudioObjectUnknown;
|
||||
return id;
|
||||
}
|
||||
|
||||
/* The device list stores the UID beside each name so a chosen entry
|
||||
* survives renaming; a saved setting may carry either, so the lookup
|
||||
* tries the UID first and the name second. */
|
||||
static AudioDeviceID coreaudio_mic_find_device(const char *uid_or_name)
|
||||
{
|
||||
UInt32 i, count = 0, pass;
|
||||
AudioDeviceID found = kAudioObjectUnknown;
|
||||
AudioDeviceID *devices;
|
||||
|
||||
if (string_is_empty(uid_or_name) || string_is_equal(uid_or_name, "default"))
|
||||
return kAudioObjectUnknown;
|
||||
if (!(devices = coreaudio_hal_devices(&count)))
|
||||
return kAudioObjectUnknown;
|
||||
|
||||
for (pass = 0; pass < 2 && found == kAudioObjectUnknown; pass++)
|
||||
{
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
char s[256];
|
||||
if (!coreaudio_mic_device_has_input(devices[i]))
|
||||
continue;
|
||||
if ( coreaudio_mic_device_string(devices[i],
|
||||
pass == 0 ? kAudioDevicePropertyDeviceUID
|
||||
: kAudioDevicePropertyDeviceNameCFString,
|
||||
s, sizeof(s))
|
||||
&& string_is_equal(s, uid_or_name))
|
||||
{
|
||||
found = devices[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
free(devices);
|
||||
if (found == kAudioObjectUnknown)
|
||||
RARCH_WARN("[CoreAudio] Input device \"%s\" not found; using the default.\n",
|
||||
uid_or_name);
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Called by the HAL on a thread of its own when the default input
|
||||
* device changes; the reconnect happens on the reader's thread. */
|
||||
static OSStatus coreaudio_mic_default_listener(AudioObjectID obj,
|
||||
UInt32 n, const AudioObjectPropertyAddress addrs[], void *data)
|
||||
{
|
||||
coreaudio_mic_t *mic = (coreaudio_mic_t*)data;
|
||||
(void)obj;
|
||||
(void)n;
|
||||
(void)addrs;
|
||||
if (mic)
|
||||
retro_atomic_store_release_int(&mic->device_changed, 1);
|
||||
return noErr;
|
||||
}
|
||||
|
||||
static void coreaudio_mic_listen_default(coreaudio_mic_t *mic, bool on)
|
||||
{
|
||||
AudioObjectPropertyAddress prop;
|
||||
prop.mSelector = kAudioHardwarePropertyDefaultInputDevice;
|
||||
prop.mScope = kAudioObjectPropertyScopeGlobal;
|
||||
prop.mElement = CA_ELEMENT_MAIN;
|
||||
if (on)
|
||||
AudioObjectAddPropertyListener(kAudioObjectSystemObject, &prop,
|
||||
coreaudio_mic_default_listener, mic);
|
||||
else
|
||||
AudioObjectRemovePropertyListener(kAudioObjectSystemObject, &prop,
|
||||
coreaudio_mic_default_listener, mic);
|
||||
}
|
||||
|
||||
/* Move a mic that follows the default onto the new default, keeping
|
||||
* the format - and so the rate reported to the core - it opened with;
|
||||
* the unit's own converter covers any difference to the new hardware. */
|
||||
static void coreaudio_mic_reconnect(coreaudio_mic_t *mic)
|
||||
{
|
||||
AudioDeviceID dev = coreaudio_mic_default_device();
|
||||
bool was_running;
|
||||
|
||||
if (dev == kAudioObjectUnknown || dev == mic->device)
|
||||
return;
|
||||
RARCH_LOG("[CoreAudio] Default input device changed; reconnecting.\n");
|
||||
|
||||
was_running = retro_atomic_load_acquire_int(&mic->running) != 0;
|
||||
if (was_running)
|
||||
AudioOutputUnitStop(mic->unit);
|
||||
retro_atomic_store_release_int(&mic->running, 0);
|
||||
if (retro_atomic_load_acquire_int(&mic->initialized))
|
||||
{
|
||||
AudioUnitUninitialize(mic->unit);
|
||||
retro_atomic_store_release_int(&mic->initialized, 0);
|
||||
}
|
||||
|
||||
mic->device = dev;
|
||||
if (AudioUnitSetProperty(mic->unit, kAudioOutputUnitProperty_CurrentDevice,
|
||||
kAudioUnitScope_Global, 0, &dev, sizeof(dev)) != noErr)
|
||||
return;
|
||||
AudioUnitSetProperty(mic->unit, kAudioUnitProperty_StreamFormat,
|
||||
kAudioUnitScope_Output, 1, &mic->format, sizeof(mic->format));
|
||||
if (AudioUnitInitialize(mic->unit) != noErr)
|
||||
return;
|
||||
retro_atomic_store_release_int(&mic->initialized, 1);
|
||||
|
||||
slock_lock(mic->lock);
|
||||
fifo_clear(mic->fifo);
|
||||
slock_unlock(mic->lock);
|
||||
|
||||
if (was_running && AudioOutputUnitStart(mic->unit) == noErr)
|
||||
retro_atomic_store_release_int(&mic->running, 1);
|
||||
}
|
||||
#endif /* !TARGET_OS_IPHONE */
|
||||
|
||||
/* --- The driver ------------------------------------------------------ */
|
||||
|
||||
static void *coreaudio_mic_init(void)
|
||||
{
|
||||
return calloc(1, sizeof(coreaudio_mic_driver_t));
|
||||
}
|
||||
|
||||
static void coreaudio_mic_free(void *driver_context)
|
||||
{
|
||||
coreaudio_mic_driver_t *drv = (coreaudio_mic_driver_t*)driver_context;
|
||||
if (!drv)
|
||||
return;
|
||||
/* The frontend closes every microphone first; do not leak one
|
||||
* should that ever stop being true. */
|
||||
if (drv->mic)
|
||||
coreaudio_mic_close(drv, drv->mic);
|
||||
free(drv);
|
||||
}
|
||||
|
||||
static int coreaudio_mic_read(void *driver_context, void *mic_context,
|
||||
void *buf, size_t len)
|
||||
{
|
||||
coreaudio_mic_t *mic = (coreaudio_mic_t*)mic_context;
|
||||
size_t avail, n;
|
||||
|
||||
(void)driver_context;
|
||||
if (!mic || !mic->fifo || !buf)
|
||||
return -1;
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
/* A pending default-device change, before the lock is taken. */
|
||||
if (retro_atomic_load_acquire_int(&mic->device_changed))
|
||||
{
|
||||
retro_atomic_store_release_int(&mic->device_changed, 0);
|
||||
coreaudio_mic_reconnect(mic);
|
||||
}
|
||||
#endif
|
||||
|
||||
slock_lock(mic->lock);
|
||||
avail = FIFO_READ_AVAIL(mic->fifo);
|
||||
n = avail < len ? avail : len;
|
||||
if (!n && !mic->nonblock)
|
||||
{
|
||||
/* Blocking: one slice's worth of wait for the callback, timed. */
|
||||
scond_wait_timeout(mic->cond, mic->lock, 10000);
|
||||
avail = FIFO_READ_AVAIL(mic->fifo);
|
||||
n = avail < len ? avail : len;
|
||||
}
|
||||
if (n)
|
||||
fifo_read(mic->fifo, buf, n);
|
||||
slock_unlock(mic->lock);
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
static void coreaudio_mic_set_nonblock_state(void *driver_context, bool state)
|
||||
{
|
||||
coreaudio_mic_driver_t *drv = (coreaudio_mic_driver_t*)driver_context;
|
||||
if (!drv)
|
||||
return;
|
||||
drv->nonblock = state;
|
||||
if (drv->mic)
|
||||
drv->mic->nonblock = state;
|
||||
}
|
||||
|
||||
static struct string_list *coreaudio_mic_device_list_new(const void *driver_context)
|
||||
{
|
||||
#if TARGET_OS_IPHONE
|
||||
(void)driver_context;
|
||||
return NULL;
|
||||
#else
|
||||
UInt32 i, count = 0;
|
||||
AudioDeviceID *devices;
|
||||
struct string_list *sl;
|
||||
union string_list_elem_attr attr;
|
||||
|
||||
(void)driver_context;
|
||||
if (!(sl = string_list_new()))
|
||||
return NULL;
|
||||
if (!(devices = coreaudio_hal_devices(&count)))
|
||||
{
|
||||
string_list_free(sl);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
char name[256], uid[256];
|
||||
if (!coreaudio_mic_device_has_input(devices[i]))
|
||||
continue;
|
||||
if ( !coreaudio_mic_device_string(devices[i],
|
||||
kAudioDevicePropertyDeviceNameCFString, name, sizeof(name))
|
||||
|| !coreaudio_mic_device_string(devices[i],
|
||||
kAudioDevicePropertyDeviceUID, uid, sizeof(uid)))
|
||||
continue;
|
||||
/* The UID rides along in attr.p, owned by the list; see the
|
||||
* free below. */
|
||||
if (!(attr.p = strdup(uid)))
|
||||
continue;
|
||||
if (!string_list_append(sl, name, attr))
|
||||
free(attr.p);
|
||||
}
|
||||
|
||||
free(devices);
|
||||
if (!sl->size)
|
||||
{
|
||||
string_list_free(sl);
|
||||
return NULL;
|
||||
}
|
||||
return sl;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void coreaudio_mic_device_list_free(const void *driver_context,
|
||||
struct string_list *sl)
|
||||
{
|
||||
size_t i;
|
||||
(void)driver_context;
|
||||
if (!sl)
|
||||
return;
|
||||
for (i = 0; i < sl->size; i++)
|
||||
{
|
||||
if (sl->elems[i].attr.p)
|
||||
free(sl->elems[i].attr.p);
|
||||
sl->elems[i].attr.p = NULL;
|
||||
}
|
||||
string_list_free(sl);
|
||||
}
|
||||
|
||||
static void *coreaudio_mic_open(void *driver_context, const char *device,
|
||||
unsigned rate, unsigned latency, unsigned *new_rate)
|
||||
{
|
||||
coreaudio_mic_driver_t *drv = (coreaudio_mic_driver_t*)driver_context;
|
||||
coreaudio_mic_t *mic;
|
||||
ca_component_desc_t desc = {0};
|
||||
AURenderCallbackStruct cb;
|
||||
void *comp;
|
||||
UInt32 one = 1, zero = 0, max_frames = 4096, max_frames_size = sizeof(max_frames);
|
||||
size_t fifo_size;
|
||||
|
||||
if (!drv)
|
||||
return NULL;
|
||||
if (drv->mic)
|
||||
{
|
||||
RARCH_WARN("[CoreAudio] A microphone is already open; closing it first.\n");
|
||||
coreaudio_mic_close(drv, drv->mic);
|
||||
}
|
||||
if (!ca_cm_resolve())
|
||||
return NULL;
|
||||
if (!(mic = (coreaudio_mic_t*)calloc(1, sizeof(*mic))))
|
||||
return NULL;
|
||||
|
||||
retro_atomic_int_init(&mic->running, 0);
|
||||
retro_atomic_int_init(&mic->initialized, 0);
|
||||
mic->lock = slock_new();
|
||||
mic->cond = scond_new();
|
||||
mic->sample_rate = rate;
|
||||
mic->nonblock = drv->nonblock;
|
||||
if (!mic->lock || !mic->cond)
|
||||
goto error;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
/* The session must be in a record category before the unit will
|
||||
* capture, and its rate is the unit's rate. Objective-C, in Cocoa. */
|
||||
{
|
||||
unsigned actual = 0;
|
||||
if (!cocoa_audio_session_begin_record(rate, &actual))
|
||||
goto error;
|
||||
if (actual)
|
||||
mic->sample_rate = actual;
|
||||
}
|
||||
#else
|
||||
retro_atomic_int_init(&mic->device_changed, 0);
|
||||
mic->device = coreaudio_mic_find_device(device);
|
||||
if (mic->device == kAudioObjectUnknown)
|
||||
{
|
||||
mic->device = coreaudio_mic_default_device();
|
||||
mic->follow_default = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* The unit: input enabled on bus 1, output disabled on bus 0. */
|
||||
desc.type = kAudioUnitType_Output;
|
||||
#if TARGET_OS_IPHONE
|
||||
desc.subtype = kAudioUnitSubType_RemoteIO;
|
||||
#else
|
||||
desc.subtype = kAudioUnitSubType_HALOutput;
|
||||
#endif
|
||||
desc.manufacturer = kAudioUnitManufacturer_Apple;
|
||||
if (!(comp = ca_cm.find_next(NULL, &desc)))
|
||||
goto error;
|
||||
if (ca_cm.open(comp, &mic->unit) != noErr || !mic->unit)
|
||||
goto error;
|
||||
if (AudioUnitSetProperty(mic->unit, kAudioOutputUnitProperty_EnableIO,
|
||||
kAudioUnitScope_Input, 1, &one, sizeof(one)) != noErr)
|
||||
goto error;
|
||||
AudioUnitSetProperty(mic->unit, kAudioOutputUnitProperty_EnableIO,
|
||||
kAudioUnitScope_Output, 0, &zero, sizeof(zero));
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
if (mic->device != kAudioObjectUnknown
|
||||
&& AudioUnitSetProperty(mic->unit, kAudioOutputUnitProperty_CurrentDevice,
|
||||
kAudioUnitScope_Global, 0, &mic->device, sizeof(mic->device)) != noErr)
|
||||
{
|
||||
RARCH_WARN("[CoreAudio] Input device %u refused; using the default.\n",
|
||||
(unsigned)mic->device);
|
||||
mic->device = kAudioObjectUnknown;
|
||||
}
|
||||
{
|
||||
AudioDeviceID actual = kAudioObjectUnknown;
|
||||
UInt32 size = sizeof(actual);
|
||||
if (AudioUnitGetProperty(mic->unit, kAudioOutputUnitProperty_CurrentDevice,
|
||||
kAudioUnitScope_Global, 0, &actual, &size) == noErr)
|
||||
mic->device = actual;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* The client format on the output side of the input bus: mono int16
|
||||
* at the device's own rate, so the unit converts nothing it need not. */
|
||||
coreaudio_mic_set_format(mic, false);
|
||||
#if !TARGET_OS_IPHONE
|
||||
{
|
||||
AudioStreamBasicDescription hw = {0};
|
||||
UInt32 size = sizeof(hw);
|
||||
if (AudioUnitGetProperty(mic->unit, kAudioUnitProperty_StreamFormat,
|
||||
kAudioUnitScope_Input, 1, &hw, &size) == noErr && hw.mSampleRate > 0)
|
||||
{
|
||||
mic->sample_rate = (unsigned)hw.mSampleRate;
|
||||
mic->format.mSampleRate = hw.mSampleRate;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (AudioUnitSetProperty(mic->unit, kAudioUnitProperty_StreamFormat,
|
||||
kAudioUnitScope_Output, 1, &mic->format, sizeof(mic->format)) != noErr)
|
||||
goto error;
|
||||
|
||||
cb.inputProc = coreaudio_mic_input_cb;
|
||||
cb.inputProcRefCon = mic;
|
||||
if (AudioUnitSetProperty(mic->unit, kAudioOutputUnitProperty_SetInputCallback,
|
||||
kAudioUnitScope_Global, 0, &cb, sizeof(cb)) != noErr)
|
||||
goto error;
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
/* A short device period for the capture side. Advisory. */
|
||||
{
|
||||
UInt32 period = 256;
|
||||
AudioUnitSetProperty(mic->unit, kAudioDevicePropertyBufferFrameSize,
|
||||
kAudioUnitScope_Global, 0, &period, sizeof(period));
|
||||
}
|
||||
#endif
|
||||
|
||||
if (AudioUnitInitialize(mic->unit) != noErr)
|
||||
goto error;
|
||||
retro_atomic_store_release_int(&mic->initialized, 1);
|
||||
|
||||
/* The render buffer, sized from what the unit may hand over per
|
||||
* slice, so the callback allocates nothing. */
|
||||
AudioUnitGetProperty(mic->unit, kAudioUnitProperty_MaximumFramesPerSlice,
|
||||
kAudioUnitScope_Global, 0, &max_frames, &max_frames_size);
|
||||
mic->cb_buf_size = (size_t)max_frames * mic->format.mBytesPerFrame;
|
||||
if (!(mic->cb_buf = calloc(1, mic->cb_buf_size)))
|
||||
goto error;
|
||||
|
||||
fifo_size = (size_t)latency * mic->sample_rate * mic->format.mBytesPerFrame / 1000;
|
||||
if (!fifo_size)
|
||||
fifo_size = (size_t)mic->sample_rate * mic->format.mBytesPerFrame / 10;
|
||||
if (!(mic->fifo = fifo_new(fifo_size)))
|
||||
goto error;
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
if (mic->follow_default)
|
||||
coreaudio_mic_listen_default(mic, true);
|
||||
#endif
|
||||
|
||||
RARCH_LOG("[CoreAudio] Microphone open: %u Hz mono %s, %u ms fifo.\n",
|
||||
mic->sample_rate, mic->use_float ? "float" : "int16", latency);
|
||||
if (new_rate)
|
||||
*new_rate = mic->sample_rate;
|
||||
drv->mic = mic;
|
||||
return mic;
|
||||
|
||||
error:
|
||||
RARCH_ERR("[CoreAudio] Failed to open the microphone.\n");
|
||||
/* close() takes a handle that was only partly built. */
|
||||
coreaudio_mic_close(drv, mic);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void coreaudio_mic_close(void *driver_context, void *mic_context)
|
||||
{
|
||||
coreaudio_mic_driver_t *drv = (coreaudio_mic_driver_t*)driver_context;
|
||||
coreaudio_mic_t *mic = (coreaudio_mic_t*)mic_context;
|
||||
if (!mic)
|
||||
return;
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
if (mic->follow_default)
|
||||
coreaudio_mic_listen_default(mic, false);
|
||||
#endif
|
||||
coreaudio_mic_stop(drv, mic);
|
||||
|
||||
/* Stopping does not wait for a callback in flight; disposing does,
|
||||
* so everything the callback touches outlives the dispose. */
|
||||
if (mic->unit)
|
||||
{
|
||||
if (retro_atomic_load_acquire_int(&mic->initialized))
|
||||
AudioUnitUninitialize(mic->unit);
|
||||
ca_cm.close(mic->unit);
|
||||
mic->unit = NULL;
|
||||
}
|
||||
if (mic->cb_buf)
|
||||
free(mic->cb_buf);
|
||||
if (mic->fifo)
|
||||
fifo_free(mic->fifo);
|
||||
if (mic->lock)
|
||||
slock_free(mic->lock);
|
||||
if (mic->cond)
|
||||
scond_free(mic->cond);
|
||||
if (drv && drv->mic == mic)
|
||||
drv->mic = NULL;
|
||||
free(mic);
|
||||
}
|
||||
|
||||
static bool coreaudio_mic_alive(const void *driver_context, const void *mic_context)
|
||||
{
|
||||
const coreaudio_mic_t *mic = (const coreaudio_mic_t*)mic_context;
|
||||
(void)driver_context;
|
||||
return mic && retro_atomic_load_acquire_int(&mic->running);
|
||||
}
|
||||
|
||||
static bool coreaudio_mic_start(void *driver_context, void *mic_context)
|
||||
{
|
||||
coreaudio_mic_t *mic = (coreaudio_mic_t*)mic_context;
|
||||
(void)driver_context;
|
||||
if (!mic || !mic->unit || !retro_atomic_load_acquire_int(&mic->initialized))
|
||||
return false;
|
||||
if (retro_atomic_load_acquire_int(&mic->running))
|
||||
return true;
|
||||
slock_lock(mic->lock);
|
||||
fifo_clear(mic->fifo);
|
||||
slock_unlock(mic->lock);
|
||||
if (AudioOutputUnitStart(mic->unit) != noErr)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to start the microphone.\n");
|
||||
return false;
|
||||
}
|
||||
retro_atomic_store_release_int(&mic->running, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool coreaudio_mic_stop(void *driver_context, void *mic_context)
|
||||
{
|
||||
coreaudio_mic_t *mic = (coreaudio_mic_t*)mic_context;
|
||||
OSStatus status;
|
||||
(void)driver_context;
|
||||
if (!mic || !mic->unit || !retro_atomic_load_acquire_int(&mic->running))
|
||||
return true;
|
||||
status = AudioOutputUnitStop(mic->unit);
|
||||
/* Not running from here on either way. */
|
||||
retro_atomic_store_release_int(&mic->running, 0);
|
||||
if (mic->fifo)
|
||||
{
|
||||
slock_lock(mic->lock);
|
||||
fifo_clear(mic->fifo);
|
||||
slock_unlock(mic->lock);
|
||||
}
|
||||
return status == noErr;
|
||||
}
|
||||
|
||||
static bool coreaudio_mic_use_float(const void *driver_context, const void *mic_context)
|
||||
{
|
||||
const coreaudio_mic_t *mic = (const coreaudio_mic_t*)mic_context;
|
||||
(void)driver_context;
|
||||
return mic && mic->use_float;
|
||||
}
|
||||
|
||||
microphone_driver_t microphone_coreaudio = {
|
||||
coreaudio_mic_init,
|
||||
coreaudio_mic_free,
|
||||
coreaudio_mic_read,
|
||||
coreaudio_mic_set_nonblock_state,
|
||||
"coreaudio",
|
||||
coreaudio_mic_device_list_new,
|
||||
coreaudio_mic_device_list_free,
|
||||
coreaudio_mic_open,
|
||||
coreaudio_mic_close,
|
||||
coreaudio_mic_alive,
|
||||
coreaudio_mic_start,
|
||||
coreaudio_mic_stop,
|
||||
coreaudio_mic_use_float
|
||||
};
|
||||
#endif /* HAVE_MICROPHONE */
|
||||
@@ -1,567 +0,0 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2025 - Joseph Mattiello
|
||||
*
|
||||
* RetroArch 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 Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*/
|
||||
|
||||
#import <AudioToolbox/AudioToolbox.h>
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <memory.h>
|
||||
|
||||
#include <boolean.h>
|
||||
#include <rthreads/rthreads.h>
|
||||
#include <queues/fifo_queue.h>
|
||||
|
||||
#include "../audio_driver.h"
|
||||
#include "../microphone_driver.h"
|
||||
#include "../../verbosity.h"
|
||||
|
||||
/* Per-microphone context: created by open_mic(), destroyed by close_mic().
|
||||
* Owns the audio unit and every buffer the render callback touches. */
|
||||
typedef struct coreaudio_microphone
|
||||
{
|
||||
AudioUnit audio_unit;
|
||||
AudioStreamBasicDescription format;
|
||||
fifo_buffer_t *sample_buffer;
|
||||
bool is_running;
|
||||
bool nonblock;
|
||||
int sample_rate;
|
||||
bool use_float;
|
||||
slock_t *fifo_lock;
|
||||
scond_t *fifo_cond;
|
||||
void *callback_buffer;
|
||||
size_t callback_buffer_size;
|
||||
unsigned drop_count;
|
||||
} coreaudio_microphone_t;
|
||||
|
||||
/* Driver-wide context: created by init(), destroyed by free().
|
||||
* The frontend holds this for as long as the driver is selected, which is a
|
||||
* strictly longer lifetime than any microphone opened through it, so the two
|
||||
* must never be the same allocation. */
|
||||
typedef struct coreaudio_microphone_driver
|
||||
{
|
||||
coreaudio_microphone_t *mic; /* The frontend opens at most one at a time */
|
||||
bool nonblock; /* Latched so it survives open/close cycles */
|
||||
} coreaudio_microphone_driver_t;
|
||||
|
||||
static void coreaudio_microphone_close_mic(void *driver_context,
|
||||
void *microphone_context);
|
||||
|
||||
/* Callback for receiving audio samples — runs on the real-time audio thread. */
|
||||
static OSStatus coreaudio_input_callback(
|
||||
void *inRefCon,
|
||||
AudioUnitRenderActionFlags *ioActionFlags,
|
||||
const AudioTimeStamp *inTimeStamp,
|
||||
UInt32 inBusNumber,
|
||||
UInt32 inNumberFrames,
|
||||
AudioBufferList *ioData)
|
||||
{
|
||||
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)inRefCon;
|
||||
AudioBufferList bufferList;
|
||||
OSStatus status;
|
||||
size_t bufferSize;
|
||||
size_t actual_bytes;
|
||||
|
||||
if (!microphone || !microphone->is_running)
|
||||
return noErr;
|
||||
|
||||
bufferSize = inNumberFrames * microphone->format.mBytesPerFrame;
|
||||
if (bufferSize == 0)
|
||||
return noErr;
|
||||
|
||||
/* Use pre-allocated buffer; drop this chunk if it doesn't fit rather
|
||||
* than allocating on the real-time thread. */
|
||||
if (bufferSize > microphone->callback_buffer_size)
|
||||
return noErr;
|
||||
|
||||
memset(microphone->callback_buffer, 0, bufferSize);
|
||||
|
||||
bufferList.mNumberBuffers = 1;
|
||||
bufferList.mBuffers[0].mDataByteSize = (UInt32)bufferSize;
|
||||
bufferList.mBuffers[0].mData = microphone->callback_buffer;
|
||||
bufferList.mBuffers[0].mNumberChannels = microphone->format.mChannelsPerFrame;
|
||||
|
||||
status = AudioUnitRender(microphone->audio_unit, ioActionFlags,
|
||||
inTimeStamp, inBusNumber, inNumberFrames, &bufferList);
|
||||
|
||||
if (status == noErr
|
||||
&& bufferList.mBuffers[0].mData
|
||||
&& bufferList.mBuffers[0].mDataByteSize > 0)
|
||||
{
|
||||
actual_bytes = MIN(bufferSize,
|
||||
bufferList.mBuffers[0].mDataByteSize);
|
||||
|
||||
/* Use trylock so we never block the real-time thread. */
|
||||
if (slock_try_lock(microphone->fifo_lock))
|
||||
{
|
||||
if (FIFO_WRITE_AVAIL(microphone->sample_buffer) >= actual_bytes)
|
||||
fifo_write(microphone->sample_buffer,
|
||||
bufferList.mBuffers[0].mData, actual_bytes);
|
||||
#ifdef DEBUG
|
||||
else if (microphone->drop_count++ % 1000 == 0)
|
||||
RARCH_WARN("[CoreAudio] FIFO full, dropping %u bytes.\n",
|
||||
(unsigned)actual_bytes);
|
||||
#endif
|
||||
scond_signal(microphone->fifo_cond);
|
||||
slock_unlock(microphone->fifo_lock);
|
||||
}
|
||||
}
|
||||
#ifdef DEBUG
|
||||
else if (status != noErr)
|
||||
RARCH_ERR("[CoreAudio] Failed to render audio: %d.\n", (int)status);
|
||||
#endif
|
||||
|
||||
return noErr;
|
||||
}
|
||||
|
||||
/* Initialize CoreAudio microphone driver */
|
||||
static void *coreaudio_microphone_init(void)
|
||||
{
|
||||
/* No microphone is opened here: the frontend calls open_mic() for that,
|
||||
* and the resulting handle has its own, shorter lifetime. */
|
||||
return calloc(1, sizeof(coreaudio_microphone_driver_t));
|
||||
}
|
||||
|
||||
/* Free CoreAudio microphone driver */
|
||||
static void coreaudio_microphone_free(void *driver_context)
|
||||
{
|
||||
coreaudio_microphone_driver_t *drv =
|
||||
(coreaudio_microphone_driver_t*)driver_context;
|
||||
if (!drv)
|
||||
return;
|
||||
|
||||
/* The frontend closes every microphone before freeing the driver; don't
|
||||
* leak one should that ever stop being true. */
|
||||
if (drv->mic)
|
||||
coreaudio_microphone_close_mic(drv, drv->mic);
|
||||
|
||||
free(drv);
|
||||
}
|
||||
|
||||
/* Read samples from microphone */
|
||||
static int coreaudio_microphone_read(void *driver_context,
|
||||
void *microphone_context, void *buf, size_t size)
|
||||
{
|
||||
size_t avail, read_amt;
|
||||
coreaudio_microphone_t *microphone =
|
||||
(coreaudio_microphone_t*)microphone_context;
|
||||
|
||||
if (!microphone || !microphone->sample_buffer || !buf)
|
||||
return -1;
|
||||
|
||||
slock_lock(microphone->fifo_lock);
|
||||
|
||||
avail = FIFO_READ_AVAIL(microphone->sample_buffer);
|
||||
read_amt = MIN(avail, size);
|
||||
|
||||
/* In blocking mode, wait briefly for the callback to provide data. */
|
||||
if (read_amt == 0 && !microphone->nonblock)
|
||||
{
|
||||
scond_wait_timeout(microphone->fifo_cond,
|
||||
microphone->fifo_lock, 10000); /* 10 ms */
|
||||
avail = FIFO_READ_AVAIL(microphone->sample_buffer);
|
||||
read_amt = MIN(avail, size);
|
||||
}
|
||||
|
||||
if (read_amt > 0)
|
||||
fifo_read(microphone->sample_buffer, buf, read_amt);
|
||||
|
||||
slock_unlock(microphone->fifo_lock);
|
||||
|
||||
return (int)read_amt;
|
||||
}
|
||||
|
||||
/* Set non-blocking state */
|
||||
static void coreaudio_microphone_set_nonblock_state(void *driver_context, bool state)
|
||||
{
|
||||
coreaudio_microphone_driver_t *drv =
|
||||
(coreaudio_microphone_driver_t*)driver_context;
|
||||
if (!drv)
|
||||
return;
|
||||
|
||||
/* Kept on the driver so a mic opened later inherits it. */
|
||||
drv->nonblock = state;
|
||||
if (drv->mic)
|
||||
drv->mic->nonblock = state;
|
||||
}
|
||||
|
||||
/* Helper method to set audio format */
|
||||
static void coreaudio_microphone_set_format(coreaudio_microphone_t *microphone, bool use_float)
|
||||
{
|
||||
microphone->use_float = use_float;
|
||||
microphone->format.mSampleRate = microphone->sample_rate;
|
||||
microphone->format.mFormatID = kAudioFormatLinearPCM;
|
||||
microphone->format.mFormatFlags = use_float
|
||||
? (kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked)
|
||||
: (kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked);
|
||||
microphone->format.mFramesPerPacket = 1;
|
||||
microphone->format.mChannelsPerFrame = 1;
|
||||
microphone->format.mBitsPerChannel = use_float ? 32 : 16;
|
||||
microphone->format.mBytesPerFrame = microphone->format.mChannelsPerFrame * microphone->format.mBitsPerChannel / 8;
|
||||
microphone->format.mBytesPerPacket = microphone->format.mBytesPerFrame * microphone->format.mFramesPerPacket;
|
||||
|
||||
RARCH_LOG("[CoreAudio] Format setup: sample_rate=%d, bits=%d, bytes_per_frame=%d.\n",
|
||||
(int)microphone->format.mSampleRate,
|
||||
microphone->format.mBitsPerChannel,
|
||||
microphone->format.mBytesPerFrame);
|
||||
}
|
||||
|
||||
/* Open microphone device */
|
||||
static void *coreaudio_microphone_open_mic(void *driver_context,
|
||||
const char *device,
|
||||
unsigned rate,
|
||||
unsigned latency,
|
||||
unsigned *new_rate)
|
||||
{
|
||||
coreaudio_microphone_driver_t *drv =
|
||||
(coreaudio_microphone_driver_t*)driver_context;
|
||||
coreaudio_microphone_t *microphone = NULL;
|
||||
|
||||
if (!drv)
|
||||
return NULL;
|
||||
|
||||
/* Guard against calling open_mic twice without close_mic */
|
||||
if (drv->mic)
|
||||
{
|
||||
RARCH_WARN("[CoreAudio] Microphone already open, closing first.\n");
|
||||
coreaudio_microphone_close_mic(drv, drv->mic);
|
||||
}
|
||||
|
||||
if (!(microphone = (coreaudio_microphone_t*)calloc(1, sizeof(*microphone))))
|
||||
return NULL;
|
||||
|
||||
if (!(microphone->fifo_lock = slock_new()))
|
||||
goto error;
|
||||
if (!(microphone->fifo_cond = scond_new()))
|
||||
goto error;
|
||||
|
||||
microphone->sample_rate = rate;
|
||||
microphone->use_float = false;
|
||||
microphone->nonblock = drv->nonblock;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
/* Configure audio session */
|
||||
{
|
||||
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
|
||||
NSError *error = nil;
|
||||
AVAudioSessionCategoryOptions options =
|
||||
AVAudioSessionCategoryOptionAllowBluetoothA2DP;
|
||||
Float64 actualRate;
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
/* PlayAndRecord routes output to the receiver on iPhone unless
|
||||
* DefaultToSpeaker is set, which would make game audio quiet and thin
|
||||
* the moment a core asks for a microphone. tvOS has no receiver to be
|
||||
* routed to and marks the option unavailable, so it is iOS-only -
|
||||
* TARGET_OS_IPHONE covers tvOS as well and is too broad to gate it. */
|
||||
options |= AVAudioSessionCategoryOptionDefaultToSpeaker;
|
||||
#endif
|
||||
|
||||
/* AllowBluetooth (HFP) is deliberately not requested: it would make a
|
||||
* paired headset's microphone available, but only by dragging the whole
|
||||
* route down to narrowband mono. Keeping A2DP alone leaves game audio
|
||||
* at full quality on the headset and takes input from the built-in mic,
|
||||
* which is the better trade for an emulator. It is also the option
|
||||
* deprecated in the iOS 26 SDK in favour of AllowBluetoothHFP. */
|
||||
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord
|
||||
withOptions:options
|
||||
error:&error];
|
||||
if (error)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to set audio session category: %s.\n",
|
||||
[[error localizedDescription] UTF8String]);
|
||||
goto error;
|
||||
}
|
||||
|
||||
/* Let iOS negotiate the rate — don't restrict to 44100/48000. */
|
||||
[audioSession setPreferredSampleRate:rate error:&error];
|
||||
if (error)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to set preferred sample rate: %s.\n",
|
||||
[[error localizedDescription] UTF8String]);
|
||||
goto error;
|
||||
}
|
||||
|
||||
actualRate = [audioSession sampleRate];
|
||||
if (new_rate)
|
||||
*new_rate = (unsigned)actualRate;
|
||||
microphone->sample_rate = (int)actualRate;
|
||||
|
||||
RARCH_LOG("[CoreAudio] Using sample rate: %d Hz.\n", microphone->sample_rate);
|
||||
}
|
||||
#endif
|
||||
|
||||
coreaudio_microphone_set_format(microphone, false);
|
||||
|
||||
/* Calculate FIFO buffer size from latency */
|
||||
{
|
||||
size_t fifoBufferSize = (size_t)latency * microphone->sample_rate
|
||||
* microphone->format.mBytesPerFrame / 1000;
|
||||
if (fifoBufferSize == 0)
|
||||
{
|
||||
RARCH_WARN("[CoreAudio] FIFO size is 0, using 1024 byte fallback.\n");
|
||||
fifoBufferSize = 1024;
|
||||
}
|
||||
microphone->sample_buffer = fifo_new(fifoBufferSize);
|
||||
if (!microphone->sample_buffer)
|
||||
goto error;
|
||||
}
|
||||
|
||||
/* Initialize audio unit */
|
||||
{
|
||||
AudioComponentDescription desc = {
|
||||
.componentType = kAudioUnitType_Output,
|
||||
#if TARGET_OS_IPHONE
|
||||
.componentSubType = kAudioUnitSubType_RemoteIO,
|
||||
#else
|
||||
.componentSubType = kAudioUnitSubType_HALOutput,
|
||||
#endif
|
||||
.componentManufacturer = kAudioUnitManufacturer_Apple,
|
||||
.componentFlags = 0,
|
||||
.componentFlagsMask = 0
|
||||
};
|
||||
AudioComponent comp = AudioComponentFindNext(NULL, &desc);
|
||||
OSStatus status = AudioComponentInstanceNew(comp, µphone->audio_unit);
|
||||
if (status != noErr)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to create audio unit.\n");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
/* Enable input */
|
||||
{
|
||||
UInt32 flag = 1;
|
||||
OSStatus status = AudioUnitSetProperty(microphone->audio_unit,
|
||||
kAudioOutputUnitProperty_EnableIO,
|
||||
kAudioUnitScope_Input, 1,
|
||||
&flag, sizeof(flag));
|
||||
if (status != noErr)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to enable input.\n");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set format */
|
||||
coreaudio_microphone_set_format(microphone, false);
|
||||
{
|
||||
OSStatus status = AudioUnitSetProperty(microphone->audio_unit,
|
||||
kAudioUnitProperty_StreamFormat,
|
||||
kAudioUnitScope_Output, 1,
|
||||
µphone->format, sizeof(microphone->format));
|
||||
if (status != noErr)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to set format: %d.\n", (int)status);
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
/* Set callback */
|
||||
{
|
||||
AURenderCallbackStruct callback = { coreaudio_input_callback, microphone };
|
||||
OSStatus status = AudioUnitSetProperty(microphone->audio_unit,
|
||||
kAudioOutputUnitProperty_SetInputCallback,
|
||||
kAudioUnitScope_Global, 1,
|
||||
&callback, sizeof(callback));
|
||||
if (status != noErr)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to set callback.\n");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
/* Initialize audio unit */
|
||||
{
|
||||
OSStatus status = AudioUnitInitialize(microphone->audio_unit);
|
||||
if (status != noErr)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to initialize audio unit: %d.\n", (int)status);
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
/* Allocate callback buffer based on max frames per slice */
|
||||
{
|
||||
UInt32 max_frames = 4096;
|
||||
UInt32 max_frames_sz = sizeof(max_frames);
|
||||
AudioUnitGetProperty(microphone->audio_unit,
|
||||
kAudioUnitProperty_MaximumFramesPerSlice,
|
||||
kAudioUnitScope_Global, 0,
|
||||
&max_frames, &max_frames_sz);
|
||||
microphone->callback_buffer_size = max_frames
|
||||
* microphone->format.mBytesPerFrame;
|
||||
microphone->callback_buffer = calloc(1, microphone->callback_buffer_size);
|
||||
if (!microphone->callback_buffer)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to allocate callback buffer.\n");
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
/* Start audio unit */
|
||||
{
|
||||
OSStatus status = AudioOutputUnitStart(microphone->audio_unit);
|
||||
if (status != noErr)
|
||||
{
|
||||
RARCH_ERR("[CoreAudio] Failed to start audio unit: %d.\n", (int)status);
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
microphone->is_running = true;
|
||||
drv->mic = microphone;
|
||||
return microphone;
|
||||
|
||||
error:
|
||||
/* close_mic() performs exactly this teardown, and tolerates a handle that
|
||||
* was only partially built, so there is no second cleanup path to keep in
|
||||
* step with the one above. */
|
||||
coreaudio_microphone_close_mic(drv, microphone);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Close microphone */
|
||||
static void coreaudio_microphone_close_mic(void *driver_context, void *microphone_context)
|
||||
{
|
||||
coreaudio_microphone_driver_t *drv =
|
||||
(coreaudio_microphone_driver_t*)driver_context;
|
||||
coreaudio_microphone_t *microphone =
|
||||
(coreaudio_microphone_t*)microphone_context;
|
||||
if (!microphone)
|
||||
return;
|
||||
|
||||
/* Stopping the unit does not wait for a render callback that is already
|
||||
* in flight; disposing it does. Everything the callback dereferences must
|
||||
* therefore stay alive until after the dispose below. */
|
||||
if (microphone->is_running)
|
||||
{
|
||||
AudioOutputUnitStop(microphone->audio_unit);
|
||||
microphone->is_running = false;
|
||||
}
|
||||
|
||||
if (microphone->audio_unit)
|
||||
{
|
||||
AudioUnitUninitialize(microphone->audio_unit);
|
||||
AudioComponentInstanceDispose(microphone->audio_unit);
|
||||
microphone->audio_unit = nil;
|
||||
}
|
||||
|
||||
if (microphone->callback_buffer)
|
||||
{
|
||||
free(microphone->callback_buffer);
|
||||
microphone->callback_buffer = NULL;
|
||||
}
|
||||
|
||||
if (microphone->sample_buffer)
|
||||
{
|
||||
fifo_free(microphone->sample_buffer);
|
||||
microphone->sample_buffer = NULL;
|
||||
}
|
||||
|
||||
slock_free(microphone->fifo_lock);
|
||||
scond_free(microphone->fifo_cond);
|
||||
|
||||
if (drv && drv->mic == microphone)
|
||||
drv->mic = NULL;
|
||||
|
||||
free(microphone);
|
||||
}
|
||||
|
||||
/* Start microphone */
|
||||
static bool coreaudio_microphone_start_mic(void *driver_context, void *microphone_context)
|
||||
{
|
||||
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)microphone_context;
|
||||
if (!microphone)
|
||||
return false;
|
||||
|
||||
if (microphone->sample_buffer)
|
||||
{
|
||||
slock_lock(microphone->fifo_lock);
|
||||
fifo_clear(microphone->sample_buffer);
|
||||
slock_unlock(microphone->fifo_lock);
|
||||
}
|
||||
|
||||
{
|
||||
OSStatus status = AudioOutputUnitStart(microphone->audio_unit);
|
||||
if (status == noErr)
|
||||
{
|
||||
microphone->is_running = true;
|
||||
return true;
|
||||
}
|
||||
RARCH_ERR("[CoreAudio] Failed to start microphone: %d.\n", (int)status);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Stop microphone */
|
||||
static bool coreaudio_microphone_stop_mic(void *driver_context, void *microphone_context)
|
||||
{
|
||||
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)microphone_context;
|
||||
if (!microphone)
|
||||
return false;
|
||||
|
||||
if (microphone->is_running)
|
||||
{
|
||||
OSStatus status = AudioOutputUnitStop(microphone->audio_unit);
|
||||
if (status == noErr)
|
||||
{
|
||||
microphone->is_running = false;
|
||||
if (microphone->sample_buffer)
|
||||
{
|
||||
slock_lock(microphone->fifo_lock);
|
||||
fifo_clear(microphone->sample_buffer);
|
||||
slock_unlock(microphone->fifo_lock);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
RARCH_ERR("[CoreAudio] Failed to stop microphone: %d.\n", (int)status);
|
||||
}
|
||||
return true; /* Already stopped */
|
||||
}
|
||||
|
||||
/* Check if microphone is alive */
|
||||
static bool coreaudio_microphone_mic_alive(const void *driver_context, const void *microphone_context)
|
||||
{
|
||||
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)microphone_context;
|
||||
return microphone && microphone->is_running;
|
||||
}
|
||||
|
||||
/* Check if microphone uses float samples */
|
||||
static bool coreaudio_microphone_mic_use_float(const void *driver_context, const void *microphone_context)
|
||||
{
|
||||
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)microphone_context;
|
||||
return microphone && microphone->use_float;
|
||||
}
|
||||
|
||||
/* Get device list (not implemented for iOS) */
|
||||
static struct string_list *coreaudio_microphone_device_list_new(const void *driver_context)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Free device list (not implemented for iOS) */
|
||||
static void coreaudio_microphone_device_list_free(const void *driver_context, struct string_list *devices)
|
||||
{
|
||||
}
|
||||
|
||||
/* CoreAudio microphone driver structure */
|
||||
microphone_driver_t microphone_coreaudio = {
|
||||
coreaudio_microphone_init,
|
||||
coreaudio_microphone_free,
|
||||
coreaudio_microphone_read,
|
||||
coreaudio_microphone_set_nonblock_state,
|
||||
"coreaudio",
|
||||
coreaudio_microphone_device_list_new,
|
||||
coreaudio_microphone_device_list_free,
|
||||
coreaudio_microphone_open_mic,
|
||||
coreaudio_microphone_close_mic,
|
||||
coreaudio_microphone_mic_alive,
|
||||
coreaudio_microphone_start_mic,
|
||||
coreaudio_microphone_stop_mic,
|
||||
coreaudio_microphone_mic_use_float
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -64,12 +64,6 @@
|
||||
#include "../frontend/drivers/platform_darwin.m"
|
||||
#endif
|
||||
|
||||
#if defined(HAVE_COREAUDIO) && defined(HAVE_MICROPHONE)
|
||||
#if defined(OSX)
|
||||
#include "../audio/drivers/coreaudio_mic_macos.m"
|
||||
#else
|
||||
#include "../audio/drivers/coreaudio_mic_ios.m"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_CORELOCATION
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/* RetroArch - A frontend for libretro.
|
||||
* Copyright (C) 2010-2014 - Hans-Kristian Arntzen
|
||||
*
|
||||
* RetroArch 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 Found-
|
||||
* ation, either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* RetroArch 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 RetroArch.
|
||||
* If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef COCOA_AUDIO_SESSION_H
|
||||
#define COCOA_AUDIO_SESSION_H
|
||||
|
||||
#include <boolean.h>
|
||||
#include <retro_common_api.h>
|
||||
|
||||
RETRO_BEGIN_DECLS
|
||||
|
||||
/* The AVAudioSession is Objective-C and belongs to the Cocoa layer; the
|
||||
* audio drivers are C and stay C. On iOS and tvOS the microphone needs
|
||||
* the session in a record category before its unit will capture, and
|
||||
* the rate the session settles on is the rate the unit will run at.
|
||||
* Implemented in ui_cocoatouch.m. Returns false when the session
|
||||
* refused the category; *actual_rate is the session's rate afterwards,
|
||||
* 0 if unknown. */
|
||||
bool cocoa_audio_session_begin_record(unsigned preferred_rate,
|
||||
unsigned *actual_rate);
|
||||
|
||||
RETRO_END_DECLS
|
||||
|
||||
#endif
|
||||
@@ -40,6 +40,7 @@
|
||||
#include "../../audio/audio_driver.h"
|
||||
#ifdef HAVE_MICROPHONE
|
||||
#include "../../audio/microphone_driver.h"
|
||||
#include "cocoa/cocoa_audio_session.h"
|
||||
#endif
|
||||
#include "../../gfx/video_display_server.h"
|
||||
#include "../../configuration.h"
|
||||
@@ -688,6 +689,52 @@ enum
|
||||
return _documentsDirectory;
|
||||
}
|
||||
|
||||
/* The record-category half of the session, for the microphone driver;
|
||||
* see cocoa_audio_session.h. Moved here from the driver, which is C. */
|
||||
bool cocoa_audio_session_begin_record(unsigned preferred_rate,
|
||||
unsigned *actual_rate)
|
||||
{
|
||||
AVAudioSession *session = [AVAudioSession sharedInstance];
|
||||
NSError *error = nil;
|
||||
AVAudioSessionCategoryOptions options =
|
||||
AVAudioSessionCategoryOptionAllowBluetoothA2DP;
|
||||
|
||||
#if TARGET_OS_IOS
|
||||
/* PlayAndRecord routes output to the receiver on iPhone unless
|
||||
* DefaultToSpeaker is set, which would make game audio quiet and thin
|
||||
* the moment a core asks for a microphone. tvOS has no receiver to be
|
||||
* routed to and marks the option unavailable, so it is iOS-only -
|
||||
* TARGET_OS_IPHONE covers tvOS as well and is too broad to gate it. */
|
||||
options |= AVAudioSessionCategoryOptionDefaultToSpeaker;
|
||||
#endif
|
||||
|
||||
/* AllowBluetooth (HFP) is deliberately not requested: it would make a
|
||||
* paired headset's microphone available, but only by dragging the whole
|
||||
* route down to narrowband mono. Keeping A2DP alone leaves game audio
|
||||
* at full quality on the headset and takes input from the built-in mic,
|
||||
* which is the better trade for an emulator. It is also the option
|
||||
* deprecated in the iOS 26 SDK in favour of AllowBluetoothHFP. */
|
||||
[session setCategory:AVAudioSessionCategoryPlayAndRecord
|
||||
withOptions:options
|
||||
error:&error];
|
||||
if (error)
|
||||
{
|
||||
RARCH_ERR("[Cocoa] AVAudioSession record category: %s\n",
|
||||
[[error localizedDescription] UTF8String]);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Let the system negotiate the rate rather than restricting it. */
|
||||
[session setPreferredSampleRate:preferred_rate error:&error];
|
||||
if (error)
|
||||
RARCH_WARN("[Cocoa] AVAudioSession preferred sample rate %u: %s\n",
|
||||
preferred_rate, [[error localizedDescription] UTF8String]);
|
||||
|
||||
if (actual_rate)
|
||||
*actual_rate = (unsigned)[session sampleRate];
|
||||
return true;
|
||||
}
|
||||
|
||||
- (void)handleAudioSessionInterruption:(NSNotification *)notification
|
||||
{
|
||||
NSNumber *type = notification.userInfo[AVAudioSessionInterruptionTypeKey];
|
||||
|
||||
Reference in new issue
Block a user