#include "audio/audio.h" #include #include #include #include #include #pragma clang diagnostic ignored "-Wdeprecated-declarations" #define AUDIO_MAX_DEVICES 32 #define AUDIO_MAX_CHANNELS 32 #define AUDIO_TEST_TONE_HZ 440.0 #define AUDIO_TEST_TONE_SEC 2.0 #define AUDIO_PI 3.14159265358979323846 typedef struct CoreAudioDeviceInfo { AudioDeviceID device_id; } CoreAudioDeviceInfo; struct AudioEngine { AudioDeviceInfo devices[AUDIO_MAX_DEVICES]; CoreAudioDeviceInfo ca_devices[AUDIO_MAX_DEVICES]; S32 device_count; AUGraph graph; AudioUnit output_unit; S32 active_device_index; F64 sample_rate; S32 num_channels; // Test tone state (accessed from audio render thread) _Atomic S32 test_tone_active; _Atomic S32 test_tone_samples_remaining; F64 test_tone_phase; }; //////////////////////////////// // Audio render callback static AudioEngine *g_audio_engine = NULL; static OSStatus audio_render_callback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) { (void)inRefCon; (void)ioActionFlags; (void)inTimeStamp; (void)inBusNumber; AudioEngine *engine = g_audio_engine; if (!engine) { for (UInt32 buf = 0; buf < ioData->mNumberBuffers; buf++) memset(ioData->mBuffers[buf].mData, 0, ioData->mBuffers[buf].mDataByteSize); return noErr; } S32 tone_active = atomic_load(&engine->test_tone_active); if (!tone_active) { for (UInt32 buf = 0; buf < ioData->mNumberBuffers; buf++) memset(ioData->mBuffers[buf].mData, 0, ioData->mBuffers[buf].mDataByteSize); return noErr; } F64 phase = engine->test_tone_phase; F64 phase_inc = 2.0 * AUDIO_PI * AUDIO_TEST_TONE_HZ / engine->sample_rate; S32 remaining = atomic_load(&engine->test_tone_samples_remaining); S32 samples_to_gen = (remaining < (S32)inNumberFrames) ? remaining : (S32)inNumberFrames; // CoreAudio with kAudioFormatFlagIsNonInterleaved: each buffer = one channel for (UInt32 buf = 0; buf < ioData->mNumberBuffers; buf++) { F32 *out = (F32 *)ioData->mBuffers[buf].mData; F64 p = phase; for (UInt32 s = 0; s < inNumberFrames; s++) { if ((S32)s < samples_to_gen) { out[s] = (F32)(sin(p) * 0.5); // -6dB p += phase_inc; if (p >= 2.0 * AUDIO_PI) p -= 2.0 * AUDIO_PI; } else { out[s] = 0.0f; } } } // Advance phase using first channel's traversal phase += phase_inc * samples_to_gen; while (phase >= 2.0 * AUDIO_PI) phase -= 2.0 * AUDIO_PI; engine->test_tone_phase = phase; S32 new_remaining = atomic_fetch_sub(&engine->test_tone_samples_remaining, samples_to_gen); if (new_remaining <= samples_to_gen) { atomic_store(&engine->test_tone_active, 0); atomic_store(&engine->test_tone_samples_remaining, 0); } return noErr; } //////////////////////////////// // Device enumeration static void enumerate_output_devices(AudioEngine *engine) { engine->device_count = 0; AudioObjectPropertyAddress prop = { kAudioHardwarePropertyDevices, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain }; UInt32 data_size = 0; if (AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &prop, 0, NULL, &data_size) != noErr) return; S32 device_count = (S32)(data_size / sizeof(AudioDeviceID)); if (device_count <= 0) return; AudioDeviceID *device_ids = (AudioDeviceID *)malloc(data_size); if (AudioObjectGetPropertyData(kAudioObjectSystemObject, &prop, 0, NULL, &data_size, device_ids) != noErr) { free(device_ids); return; } for (S32 i = 0; i < device_count && engine->device_count < AUDIO_MAX_DEVICES; i++) { // Check if device has output channels AudioObjectPropertyAddress stream_prop = { kAudioDevicePropertyStreamConfiguration, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMain }; UInt32 stream_size = 0; if (AudioObjectGetPropertyDataSize(device_ids[i], &stream_prop, 0, NULL, &stream_size) != noErr) continue; AudioBufferList *buf_list = (AudioBufferList *)malloc(stream_size); if (AudioObjectGetPropertyData(device_ids[i], &stream_prop, 0, NULL, &stream_size, buf_list) != noErr) { free(buf_list); continue; } S32 output_channels = 0; for (UInt32 b = 0; b < buf_list->mNumberBuffers; b++) output_channels += (S32)buf_list->mBuffers[b].mNumberChannels; free(buf_list); if (output_channels == 0) continue; // Get device name AudioObjectPropertyAddress name_prop = { kAudioObjectPropertyName, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMain }; CFStringRef name_ref = NULL; UInt32 name_size = sizeof(name_ref); if (AudioObjectGetPropertyData(device_ids[i], &name_prop, 0, NULL, &name_size, &name_ref) != noErr) continue; S32 idx = engine->device_count++; CFStringGetCString(name_ref, engine->devices[idx].name, sizeof(engine->devices[idx].name), kCFStringEncodingUTF8); CFRelease(name_ref); engine->devices[idx].id = idx; engine->ca_devices[idx].device_id = device_ids[i]; } free(device_ids); } //////////////////////////////// // Public API AudioEngine *audio_create(void *hwnd) { (void)hwnd; AudioEngine *engine = (AudioEngine *)calloc(1, sizeof(AudioEngine)); engine->active_device_index = -1; atomic_store(&engine->test_tone_active, 0); atomic_store(&engine->test_tone_samples_remaining, 0); g_audio_engine = engine; enumerate_output_devices(engine); return engine; } void audio_destroy(AudioEngine *engine) { audio_close_device(engine); if (g_audio_engine == engine) g_audio_engine = NULL; free(engine); } void audio_refresh_devices(AudioEngine *engine) { audio_close_device(engine); enumerate_output_devices(engine); } S32 audio_get_device_count(AudioEngine *engine) { return engine->device_count; } AudioDeviceInfo *audio_get_device(AudioEngine *engine, S32 index) { if (index < 0 || index >= engine->device_count) return NULL; return &engine->devices[index]; } B32 audio_open_device(AudioEngine *engine, S32 index) { audio_close_device(engine); if (index < 0 || index >= engine->device_count) return false; AudioDeviceID device_id = engine->ca_devices[index].device_id; // Create AUGraph if (NewAUGraph(&engine->graph) != noErr) return false; // Add HAL output node AudioComponentDescription output_desc = {0}; output_desc.componentType = kAudioUnitType_Output; output_desc.componentSubType = kAudioUnitSubType_HALOutput; output_desc.componentManufacturer = kAudioUnitManufacturer_Apple; AUNode output_node; if (AUGraphAddNode(engine->graph, &output_desc, &output_node) != noErr) { DisposeAUGraph(engine->graph); engine->graph = NULL; return false; } if (AUGraphOpen(engine->graph) != noErr) { DisposeAUGraph(engine->graph); engine->graph = NULL; return false; } if (AUGraphNodeInfo(engine->graph, output_node, NULL, &engine->output_unit) != noErr) { DisposeAUGraph(engine->graph); engine->graph = NULL; return false; } // Set device if (AudioUnitSetProperty(engine->output_unit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, 0, &device_id, sizeof(device_id)) != noErr) { DisposeAUGraph(engine->graph); engine->graph = NULL; return false; } // Get device sample rate AudioObjectPropertyAddress rate_prop = { kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertyScopeOutput, kAudioObjectPropertyElementMain }; Float64 sample_rate = 44100.0; UInt32 rate_size = sizeof(sample_rate); AudioObjectGetPropertyData(device_id, &rate_prop, 0, NULL, &rate_size, &sample_rate); engine->sample_rate = sample_rate; // Set stream format: Float32, non-interleaved AudioStreamBasicDescription fmt = {0}; fmt.mSampleRate = sample_rate; fmt.mFormatID = kAudioFormatLinearPCM; fmt.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsNonInterleaved | kAudioFormatFlagIsPacked; fmt.mBitsPerChannel = 32; fmt.mChannelsPerFrame = 2; fmt.mFramesPerPacket = 1; fmt.mBytesPerFrame = 4; fmt.mBytesPerPacket = 4; engine->num_channels = 2; AudioUnitSetProperty(engine->output_unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &fmt, sizeof(fmt)); // Set render callback AURenderCallbackStruct cb = {0}; cb.inputProc = audio_render_callback; cb.inputProcRefCon = engine; if (AudioUnitSetProperty(engine->output_unit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &cb, sizeof(cb)) != noErr) { DisposeAUGraph(engine->graph); engine->graph = NULL; return false; } // Initialize and start if (AUGraphInitialize(engine->graph) != noErr) { DisposeAUGraph(engine->graph); engine->graph = NULL; return false; } if (AUGraphStart(engine->graph) != noErr) { DisposeAUGraph(engine->graph); engine->graph = NULL; return false; } engine->active_device_index = index; engine->test_tone_phase = 0.0; atomic_store(&engine->test_tone_active, 0); atomic_store(&engine->test_tone_samples_remaining, 0); return true; } void audio_close_device(AudioEngine *engine) { if (!engine->graph) return; atomic_store(&engine->test_tone_active, 0); atomic_store(&engine->test_tone_samples_remaining, 0); AUGraphStop(engine->graph); AUGraphUninitialize(engine->graph); DisposeAUGraph(engine->graph); engine->graph = NULL; engine->output_unit = NULL; engine->active_device_index = -1; engine->test_tone_phase = 0.0; } void audio_play_test_tone(AudioEngine *engine) { if (!engine->graph) return; engine->test_tone_phase = 0.0; S32 total_samples = (S32)(engine->sample_rate * AUDIO_TEST_TONE_SEC); atomic_store(&engine->test_tone_samples_remaining, total_samples); atomic_store(&engine->test_tone_active, 1); } B32 audio_is_test_tone_playing(AudioEngine *engine) { return atomic_load(&engine->test_tone_active) != 0; } void audio_update(AudioEngine *engine, F32 dt) { (void)engine; (void)dt; }