487 lines
15 KiB
Objective-C
487 lines
15 KiB
Objective-C
/* 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 "audio/microphone_driver.h"
|
|
#include "queues/fifo_queue.h"
|
|
#include "verbosity.h"
|
|
#include <memory.h>
|
|
#include <stdatomic.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdlib.h>
|
|
|
|
#include "audio/audio_driver.h"
|
|
#include "../../verbosity.h"
|
|
|
|
typedef struct coreaudio_microphone
|
|
{
|
|
AudioUnit audio_unit; /* CoreAudio audio unit */
|
|
AudioStreamBasicDescription format; /* Audio format */
|
|
fifo_buffer_t *sample_buffer; /* Sample buffer */
|
|
bool is_running; /* Whether the microphone is running */
|
|
bool nonblock; /* Non-blocking mode flag */
|
|
int sample_rate; /* Current sample rate */
|
|
bool use_float; /* Whether to use float format */
|
|
} coreaudio_microphone_t;
|
|
|
|
/* Callback for receiving audio samples */
|
|
static OSStatus coreaudio_input_callback(
|
|
void *inRefCon,
|
|
AudioUnitRenderActionFlags *ioActionFlags,
|
|
const AudioTimeStamp *inTimeStamp,
|
|
UInt32 inBusNumber,
|
|
UInt32 inNumberFrames,
|
|
AudioBufferList *ioData)
|
|
{
|
|
OSStatus status;
|
|
AudioBufferList bufferList;
|
|
void *tempBuffer = NULL;
|
|
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)inRefCon;
|
|
/* Calculate required buffer size */
|
|
size_t bufferSize = inNumberFrames * microphone->format.mBytesPerFrame;
|
|
if (bufferSize == 0)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Invalid buffer size calculation.\n");
|
|
return kAudio_ParamError;
|
|
}
|
|
|
|
/* Allocate temporary buffer */
|
|
tempBuffer = malloc(bufferSize);
|
|
if (!tempBuffer)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to allocate temporary buffer.\n");
|
|
return kAudio_MemFullError;
|
|
}
|
|
|
|
/* Set up buffer list */
|
|
bufferList.mNumberBuffers = 1;
|
|
bufferList.mBuffers[0].mDataByteSize = (UInt32)bufferSize;
|
|
bufferList.mBuffers[0].mData = tempBuffer;
|
|
|
|
/* Render audio data */
|
|
status = AudioUnitRender(microphone->audio_unit,
|
|
ioActionFlags,
|
|
inTimeStamp,
|
|
inBusNumber,
|
|
inNumberFrames,
|
|
&bufferList);
|
|
|
|
/* Write to FIFO buffer */
|
|
if (status == noErr)
|
|
fifo_write(microphone->sample_buffer,
|
|
bufferList.mBuffers[0].mData,
|
|
bufferList.mBuffers[0].mDataByteSize);
|
|
else
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to render audio: %d.\n", status);
|
|
}
|
|
|
|
/* Clean up temporary buffer */
|
|
free(tempBuffer);
|
|
return status;
|
|
}
|
|
|
|
/* Initialize CoreAudio microphone driver */
|
|
static void *coreaudio_microphone_init(void)
|
|
{
|
|
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)calloc(1, sizeof(*microphone));
|
|
if (!microphone)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to allocate microphone driver.\n");
|
|
return NULL;
|
|
}
|
|
|
|
/* Default sample rate will be set during open_mic */
|
|
microphone->sample_rate = 0;
|
|
microphone->nonblock = false;
|
|
microphone->use_float = false;
|
|
|
|
return microphone;
|
|
}
|
|
|
|
/* Free CoreAudio microphone driver */
|
|
static void coreaudio_microphone_free(void *driver_context)
|
|
{
|
|
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)driver_context;
|
|
if (microphone)
|
|
{
|
|
if (microphone->audio_unit && microphone->is_running)
|
|
{
|
|
AudioOutputUnitStop(microphone->audio_unit);
|
|
microphone->is_running = false;
|
|
}
|
|
|
|
/* TODO: This crashes, though we protect calls around `audio_unit` nil! */
|
|
#if 0
|
|
if (microphone->audio_unit)
|
|
{
|
|
AudioComponentInstanceDispose(microphone->audio_unit);
|
|
microphone->audio_unit = nil;
|
|
}
|
|
#endif
|
|
if (microphone->sample_buffer)
|
|
fifo_free(microphone->sample_buffer);
|
|
free(microphone);
|
|
}
|
|
}
|
|
|
|
/* Read samples from microphone */
|
|
static int coreaudio_microphone_read(void *driver_context,
|
|
void *microphone_context, void *buf, size_t size)
|
|
{
|
|
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)driver_context;
|
|
size_t avail, read_amt;
|
|
|
|
if (!microphone || !buf)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Invalid parameters in read.\n");
|
|
return -1;
|
|
}
|
|
|
|
avail = FIFO_READ_AVAIL(microphone->sample_buffer);
|
|
read_amt = MIN(avail, size);
|
|
|
|
if (microphone->nonblock && read_amt == 0)
|
|
return 0; /* Return immediately in non-blocking mode */
|
|
|
|
if (read_amt > 0)
|
|
{
|
|
fifo_read(microphone->sample_buffer, buf, read_amt);
|
|
#if DEBUG
|
|
RARCH_LOG("[CoreAudio] Read %zu bytes from microphone.\n", read_amt);
|
|
#endif
|
|
}
|
|
|
|
return (int)read_amt;
|
|
}
|
|
|
|
/* Set non-blocking state */
|
|
static void coreaudio_microphone_set_nonblock_state(void *driver_context, bool state)
|
|
{
|
|
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)driver_context;
|
|
if (microphone)
|
|
microphone->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; /* Store the format choice */
|
|
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_t *microphone = (coreaudio_microphone_t*)driver_context;
|
|
if (!microphone)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Invalid driver context.\n");
|
|
return NULL;
|
|
}
|
|
|
|
/* Initialize handle fields */
|
|
microphone->sample_rate = rate;
|
|
microphone->use_float = false; /* Default to integer format */
|
|
|
|
/* Validate requested sample rate */
|
|
if (rate != 44100 && rate != 48000)
|
|
{
|
|
RARCH_WARN("[CoreAudio] Requested sample rate %u not supported, defaulting to 48000.\n", rate);
|
|
rate = 48000;
|
|
}
|
|
|
|
#if TARGET_OS_IPHONE
|
|
/* Configure audio session */
|
|
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
|
|
NSError *error = nil;
|
|
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
|
|
if (error)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to set audio session category: %s.\n", [[error localizedDescription] UTF8String]);
|
|
return NULL;
|
|
}
|
|
|
|
/*/ Set preferred sample rate */
|
|
[audioSession setPreferredSampleRate:rate error:&error];
|
|
if (error)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to set preferred sample rate: %s.\n", [[error localizedDescription] UTF8String]);
|
|
return NULL;
|
|
}
|
|
|
|
/* Get actual sample rate */
|
|
Float64 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);
|
|
#else
|
|
|
|
#endif
|
|
|
|
/* Set format using helper method */
|
|
coreaudio_microphone_set_format(microphone, false); /* Default to 16-bit integer */
|
|
|
|
/* Calculate FIFO buffer size */
|
|
size_t fifoBufferSize = (latency * microphone->sample_rate * microphone->format.mBytesPerFrame) / 1000;
|
|
if (fifoBufferSize == 0)
|
|
{
|
|
RARCH_WARN("[CoreAudio] Calculated FIFO buffer size is 0 for latency: %u, sample_rate: %d, bytes_per_frame: %d.\n",
|
|
latency, microphone->sample_rate, microphone->format.mBytesPerFrame);
|
|
fifoBufferSize = 1024; /* Default to a reasonable buffer size */
|
|
}
|
|
|
|
RARCH_LOG("[CoreAudio] FIFO buffer size: %zu bytes.\n", fifoBufferSize);
|
|
|
|
/* Create sample buffer */
|
|
microphone->sample_buffer = fifo_new(fifoBufferSize);
|
|
if (!microphone->sample_buffer)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to create sample buffer.\n");
|
|
return NULL;
|
|
}
|
|
|
|
/* 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;
|
|
status = AudioUnitSetProperty(microphone->audio_unit,
|
|
kAudioOutputUnitProperty_EnableIO,
|
|
kAudioUnitScope_Input,
|
|
1, /* Input bus */
|
|
&flag,
|
|
sizeof(flag));
|
|
if (status != noErr)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to enable input.\n");
|
|
goto error;
|
|
}
|
|
|
|
/* Set format using helper method */
|
|
coreaudio_microphone_set_format(microphone, false); /* Default to 16-bit integer */
|
|
status = AudioUnitSetProperty(microphone->audio_unit,
|
|
kAudioUnitProperty_StreamFormat,
|
|
kAudioUnitScope_Output,
|
|
1, /* Input bus */
|
|
µphone->format,
|
|
sizeof(microphone->format));
|
|
if (status != noErr)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to set format: %d.\n", status);
|
|
goto error;
|
|
}
|
|
|
|
/* Set callback */
|
|
AURenderCallbackStruct callback = { coreaudio_input_callback, microphone };
|
|
status = AudioUnitSetProperty(microphone->audio_unit,
|
|
kAudioOutputUnitProperty_SetInputCallback,
|
|
kAudioUnitScope_Global,
|
|
1, /* Input bus */
|
|
&callback,
|
|
sizeof(callback));
|
|
if (status != noErr)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to set callback.\n");
|
|
goto error;
|
|
}
|
|
|
|
/* Initialize audio unit */
|
|
status = AudioUnitInitialize(microphone->audio_unit);
|
|
if (status != noErr)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to initialize audio unit: %d.\n", status);
|
|
goto error;
|
|
}
|
|
|
|
/* Start audio unit */
|
|
status = AudioOutputUnitStart(microphone->audio_unit);
|
|
if (status != noErr)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to start audio unit: %d.\n", status);
|
|
goto error;
|
|
}
|
|
|
|
return microphone;
|
|
|
|
error:
|
|
if (microphone)
|
|
{
|
|
if (microphone->audio_unit)
|
|
{
|
|
AudioComponentInstanceDispose(microphone->audio_unit);
|
|
microphone->audio_unit = nil;
|
|
}
|
|
free(microphone);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* Close microphone */
|
|
static void coreaudio_microphone_close_mic(void *driver_context, void *microphone_context)
|
|
{
|
|
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)microphone_context;
|
|
if (microphone)
|
|
{
|
|
if (microphone->is_running)
|
|
AudioOutputUnitStop(microphone->audio_unit);
|
|
|
|
if (microphone->audio_unit)
|
|
{
|
|
AudioComponentInstanceDispose(microphone->audio_unit);
|
|
microphone->audio_unit = nil;
|
|
}
|
|
if (microphone->sample_buffer)
|
|
fifo_free(microphone->sample_buffer);
|
|
free(microphone);
|
|
}
|
|
else
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to close microphone.\n");
|
|
}
|
|
}
|
|
|
|
/* Start microphone */
|
|
static bool coreaudio_microphone_start_mic(void *driver_context, void *microphone_context)
|
|
{
|
|
RARCH_LOG("[CoreAudio] Starting microphone.\n");
|
|
coreaudio_microphone_t *microphone = (coreaudio_microphone_t*)microphone_context;
|
|
if (!microphone)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to start microphone.\n");
|
|
return false;
|
|
}
|
|
RARCH_LOG("[CoreAudio] Starting audio unit...\n");
|
|
|
|
OSStatus status = AudioOutputUnitStart(microphone->audio_unit);
|
|
if (status == noErr)
|
|
{
|
|
RARCH_LOG("[CoreAudio] Audio unit started successfully.\n");
|
|
microphone->is_running = true;
|
|
return true;
|
|
}
|
|
RARCH_ERR("[CoreAudio] Failed to start microphone: %d.\n", 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)
|
|
{
|
|
RARCH_ERR("[CoreAudio] Failed to stop microphone.\n");
|
|
return false;
|
|
}
|
|
|
|
if (microphone->is_running)
|
|
{
|
|
OSStatus status = AudioOutputUnitStop(microphone->audio_unit);
|
|
if (status == noErr)
|
|
{
|
|
microphone->is_running = false;
|
|
return true;
|
|
}
|
|
RARCH_ERR("[CoreAudio] Failed to stop microphone: %d.\n", 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 CoreAudio) */
|
|
static struct string_list *coreaudio_microphone_device_list_new(const void *driver_context)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
/* Free device list (not implemented for CoreAudio) */
|
|
static void coreaudio_microphone_device_list_free(const void *driver_context, struct string_list *devices)
|
|
{
|
|
}
|
|
|
|
/* Check if microphone is using float format */
|
|
static bool coreaudio_microphone_use_float(const void *driver_context, const void *microphone_context)
|
|
{
|
|
coreaudio_microphone_t *microphone = (coreaudio_microphone_t *)microphone_context;
|
|
if (!microphone)
|
|
return false;
|
|
|
|
return microphone->use_float;
|
|
}
|
|
|
|
/* 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
|
|
};
|