/* 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 #import #include "audio/microphone_driver.h" #include "queues/fifo_queue.h" #include "verbosity.h" #include #include #include #include #include #include #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 };