Death to C++
This commit is contained in:
342
src/audio/audio_coreaudio.c
Normal file
342
src/audio/audio_coreaudio.c
Normal file
@@ -0,0 +1,342 @@
|
||||
#include "audio/audio.h"
|
||||
#include <AudioToolbox/AudioToolbox.h>
|
||||
#include <CoreAudio/CoreAudio.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <stdatomic.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
Reference in New Issue
Block a user