Add base audio + asio implementation

This commit is contained in:
2026-03-03 02:55:38 -05:00
parent b523e233cb
commit 7e298faadd
4 changed files with 680 additions and 1 deletions

1
nob.c
View File

@@ -14,6 +14,7 @@ static const char *link_libs[] = {
"gdi32.lib",
"shell32.lib",
"ole32.lib",
"advapi32.lib",
"dwmapi.lib",
"winmm.lib",
};

24
src/audio/audio.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <stdint.h>
#include <stdbool.h>
struct AudioEngine;
struct AudioDeviceInfo {
char name[128];
int32_t id; // index into engine's device list
};
AudioEngine *audio_create(void *hwnd);
void audio_destroy(AudioEngine *engine);
void audio_refresh_devices(AudioEngine *engine);
int32_t audio_get_device_count(AudioEngine *engine);
AudioDeviceInfo*audio_get_device(AudioEngine *engine, int32_t index);
bool audio_open_device(AudioEngine *engine, int32_t index);
void audio_close_device(AudioEngine *engine);
void audio_play_test_tone(AudioEngine *engine);
bool audio_is_test_tone_playing(AudioEngine *engine);
void audio_update(AudioEngine *engine, float dt);

569
src/audio/audio_asio.cpp Normal file
View File

@@ -0,0 +1,569 @@
#include "audio/audio.h"
#include <windows.h>
#include <objbase.h>
#include <string.h>
#include <math.h>
#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
////////////////////////////////
// ASIO type definitions (minimal set for a host)
typedef long ASIOError;
typedef long ASIOBool;
typedef long long int ASIOSamples;
typedef long long int ASIOTimeStamp;
enum {
ASE_OK = 0,
ASE_SUCCESS = 0x3f4847a0,
ASE_NotPresent = -1000,
ASE_HWMalfunction = -999,
ASE_InvalidParameter = -998,
ASE_InvalidMode = -997,
ASE_SPNotAdvancing = -996,
ASE_NoClock = -995,
ASE_NoMemory = -994,
};
enum ASIOSampleType {
ASIOSTInt16MSB = 0,
ASIOSTInt24MSB = 1,
ASIOSTInt32MSB = 2,
ASIOSTFloat32MSB = 3,
ASIOSTFloat64MSB = 4,
ASIOSTInt32MSB16 = 8,
ASIOSTInt32MSB18 = 9,
ASIOSTInt32MSB20 = 10,
ASIOSTInt32MSB24 = 11,
ASIOSTInt16LSB = 16,
ASIOSTInt24LSB = 17,
ASIOSTInt32LSB = 18,
ASIOSTFloat32LSB = 19,
ASIOSTFloat64LSB = 20,
ASIOSTInt32LSB16 = 24,
ASIOSTInt32LSB18 = 25,
ASIOSTInt32LSB20 = 26,
ASIOSTInt32LSB24 = 27,
};
struct ASIOClockSource {
long index;
long channel;
long group;
ASIOBool isCurrentSource;
char name[32];
};
struct ASIOChannelInfo {
long channel;
ASIOBool isInput;
ASIOBool isActive;
long channelGroup;
ASIOSampleType type;
char name[32];
};
struct ASIOBufferInfo {
ASIOBool isInput;
long channelNum;
void *buffers[2]; // double buffer
};
struct ASIOTimeCode {
double speed;
ASIOSamples timeCodeSamples;
unsigned long flags;
char future[64];
};
struct AsioTimeInfo {
double speed;
ASIOTimeStamp systemTime;
ASIOSamples samplePosition;
double sampleRate;
unsigned long flags;
char reserved[12];
};
struct ASIOTime {
long reserved[4];
AsioTimeInfo timeInfo;
ASIOTimeCode timeCode;
};
struct ASIOCallbacks {
void (*bufferSwitch)(long doubleBufferIndex, ASIOBool directProcess);
void (*sampleRateDidChange)(double sRate);
long (*asioMessage)(long selector, long value, void *message, double *opt);
ASIOTime *(*bufferSwitchTimeInfo)(ASIOTime *params, long doubleBufferIndex, ASIOBool directProcess);
};
// ASIO message selectors
enum {
kAsioSelectorSupported = 1,
kAsioEngineVersion,
kAsioResetRequest,
kAsioBufferSizeChange,
kAsioResyncRequest,
kAsioLatenciesChanged,
kAsioSupportsTimeInfo,
kAsioSupportsTimeCode,
kAsioSupportsInputMonitor,
};
////////////////////////////////
// IASIO COM interface
// Standard ASIO vtable — inherits IUnknown
class IASIO : public IUnknown {
public:
virtual ASIOBool init(void *sysHandle) = 0;
virtual void getDriverName(char *name) = 0;
virtual long getDriverVersion() = 0;
virtual void getErrorMessage(char *string) = 0;
virtual ASIOError start() = 0;
virtual ASIOError stop() = 0;
virtual ASIOError getChannels(long *numInputChannels, long *numOutputChannels) = 0;
virtual ASIOError getLatencies(long *inputLatency, long *outputLatency) = 0;
virtual ASIOError getBufferSize(long *minSize, long *maxSize, long *preferredSize, long *granularity) = 0;
virtual ASIOError canSampleRate(double sampleRate) = 0;
virtual ASIOError getSampleRate(double *sampleRate) = 0;
virtual ASIOError setSampleRate(double sampleRate) = 0;
virtual ASIOError getClockSources(ASIOClockSource *clocks, long *numSources) = 0;
virtual ASIOError setClockSource(long reference) = 0;
virtual ASIOError getSamplePosition(ASIOSamples *sPos, ASIOTimeStamp *tStamp) = 0;
virtual ASIOError getChannelInfo(ASIOChannelInfo *info) = 0;
virtual ASIOError createBuffers(ASIOBufferInfo *bufferInfos, long numChannels, long bufferSize, ASIOCallbacks *callbacks) = 0;
virtual ASIOError disposeBuffers() = 0;
virtual ASIOError controlPanel() = 0;
virtual ASIOError future(long selector, void *opt) = 0;
virtual ASIOError outputReady() = 0;
};
////////////////////////////////
// Internal state
struct AsioDriverInfo {
char name[128];
CLSID clsid;
};
struct AudioEngine {
void *hwnd; // HWND for ASIO init
// Device enumeration
AudioDeviceInfo devices[AUDIO_MAX_DEVICES];
AsioDriverInfo drivers[AUDIO_MAX_DEVICES];
int32_t device_count;
// Active driver
IASIO *driver;
int32_t active_device_index; // -1 = none
// Buffer state
ASIOBufferInfo buffer_infos[AUDIO_MAX_CHANNELS];
long num_output_channels;
long num_input_channels;
long buffer_size;
double sample_rate;
ASIOSampleType output_sample_type;
ASIOCallbacks callbacks;
// Test tone state (accessed from callback thread)
volatile LONG test_tone_active;
volatile LONG test_tone_samples_remaining;
double test_tone_phase; // written only from callback thread
};
////////////////////////////////
// Global engine pointer for ASIO callbacks (standard pattern — callbacks have no user-data param)
static AudioEngine *g_audio_engine = nullptr;
////////////////////////////////
// Sample writing helper
static void write_sample(void *dest, ASIOSampleType type, double value) {
// Clamp to [-1, 1]
if (value > 1.0) value = 1.0;
if (value < -1.0) value = -1.0;
switch (type) {
case ASIOSTInt16LSB: {
int16_t s = (int16_t)(value * 32767.0);
memcpy(dest, &s, 2);
} break;
case ASIOSTInt24LSB: {
int32_t s = (int32_t)(value * 8388607.0);
uint8_t *d = (uint8_t *)dest;
d[0] = (uint8_t)(s & 0xFF);
d[1] = (uint8_t)((s >> 8) & 0xFF);
d[2] = (uint8_t)((s >> 16) & 0xFF);
} break;
case ASIOSTInt32LSB: {
int32_t s = (int32_t)(value * 2147483647.0);
memcpy(dest, &s, 4);
} break;
case ASIOSTFloat32LSB: {
float f = (float)value;
memcpy(dest, &f, 4);
} break;
case ASIOSTFloat64LSB: {
memcpy(dest, &value, 8);
} break;
default: {
// Unsupported type — write silence (4 bytes of zero)
uint32_t z = 0;
memcpy(dest, &z, 4);
} break;
}
}
static int sample_type_size(ASIOSampleType type) {
switch (type) {
case ASIOSTInt16LSB: return 2;
case ASIOSTInt24LSB: return 3;
case ASIOSTInt32LSB: return 4;
case ASIOSTFloat32LSB: return 4;
case ASIOSTFloat64LSB: return 8;
default: return 4;
}
}
////////////////////////////////
// ASIO callbacks (static — called from driver thread)
static void asio_buffer_switch(long doubleBufferIndex, ASIOBool directProcess) {
(void)directProcess;
AudioEngine *engine = g_audio_engine;
if (!engine) return;
long buf_size = engine->buffer_size;
ASIOSampleType type = engine->output_sample_type;
int bytes_per_sample = sample_type_size(type);
LONG tone_active = InterlockedCompareExchange(&engine->test_tone_active, 0, 0);
for (long ch = 0; ch < engine->num_output_channels; ch++) {
void *buf = engine->buffer_infos[ch].buffers[doubleBufferIndex];
if (!buf) continue;
if (tone_active) {
// Only generate tone, don't branch per-sample for active check
} else {
// Silence
memset(buf, 0, (size_t)(buf_size * bytes_per_sample));
}
}
if (tone_active) {
double phase = engine->test_tone_phase;
double phase_inc = 2.0 * AUDIO_PI * AUDIO_TEST_TONE_HZ / engine->sample_rate;
LONG remaining = InterlockedCompareExchange(&engine->test_tone_samples_remaining, 0, 0);
long samples_to_gen = (remaining < buf_size) ? (long)remaining : buf_size;
for (long s = 0; s < buf_size; s++) {
double sample_val = 0.0;
if (s < samples_to_gen) {
sample_val = sin(phase) * 0.5; // -6dB to avoid clipping
phase += phase_inc;
if (phase >= 2.0 * AUDIO_PI) phase -= 2.0 * AUDIO_PI;
}
// Write to all output channels (stereo or more)
for (long ch = 0; ch < engine->num_output_channels; ch++) {
void *buf = engine->buffer_infos[ch].buffers[doubleBufferIndex];
if (!buf) continue;
uint8_t *dest = (uint8_t *)buf + (size_t)(s * bytes_per_sample);
write_sample(dest, type, sample_val);
}
}
engine->test_tone_phase = phase;
LONG new_remaining = InterlockedExchangeAdd(&engine->test_tone_samples_remaining, -samples_to_gen);
if (new_remaining <= samples_to_gen) {
InterlockedExchange(&engine->test_tone_active, 0);
InterlockedExchange(&engine->test_tone_samples_remaining, 0);
}
}
}
static void asio_sample_rate_changed(double sRate) {
AudioEngine *engine = g_audio_engine;
if (engine) {
engine->sample_rate = sRate;
}
}
static long asio_message(long selector, long value, void *message, double *opt) {
(void)value;
(void)message;
(void)opt;
switch (selector) {
case kAsioSelectorSupported:
// Report which selectors we support
if (value == kAsioEngineVersion ||
value == kAsioResetRequest ||
value == kAsioSupportsTimeInfo)
return 1;
return 0;
case kAsioEngineVersion:
return 2; // ASIO 2.0
case kAsioResetRequest:
return 1;
case kAsioLatenciesChanged:
return 1;
case kAsioSupportsTimeInfo:
return 0; // We use bufferSwitch, not bufferSwitchTimeInfo
default:
return 0;
}
}
static ASIOTime *asio_buffer_switch_time_info(ASIOTime *params, long doubleBufferIndex, ASIOBool directProcess) {
asio_buffer_switch(doubleBufferIndex, directProcess);
return params;
}
////////////////////////////////
// Registry enumeration
static void enumerate_asio_drivers(AudioEngine *engine) {
engine->device_count = 0;
HKEY asio_key;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SOFTWARE\\ASIO", 0, KEY_READ, &asio_key) != ERROR_SUCCESS)
return;
char subkey_name[256];
DWORD subkey_name_len;
for (DWORD i = 0; engine->device_count < AUDIO_MAX_DEVICES; i++) {
subkey_name_len = sizeof(subkey_name);
if (RegEnumKeyExA(asio_key, i, subkey_name, &subkey_name_len, nullptr, nullptr, nullptr, nullptr) != ERROR_SUCCESS)
break;
HKEY driver_key;
if (RegOpenKeyExA(asio_key, subkey_name, 0, KEY_READ, &driver_key) != ERROR_SUCCESS)
continue;
// Read CLSID string
char clsid_str[64] = {};
DWORD clsid_len = sizeof(clsid_str);
DWORD type = 0;
if (RegQueryValueExA(driver_key, "CLSID", nullptr, &type, (LPBYTE)clsid_str, &clsid_len) != ERROR_SUCCESS ||
type != REG_SZ) {
RegCloseKey(driver_key);
continue;
}
// Parse CLSID string to GUID
CLSID clsid;
wchar_t clsid_wide[64];
MultiByteToWideChar(CP_ACP, 0, clsid_str, -1, clsid_wide, 64);
if (CLSIDFromString(clsid_wide, &clsid) != S_OK) {
RegCloseKey(driver_key);
continue;
}
int32_t idx = engine->device_count++;
strncpy_s(engine->devices[idx].name, sizeof(engine->devices[idx].name), subkey_name, _TRUNCATE);
engine->devices[idx].id = idx;
engine->drivers[idx].clsid = clsid;
strncpy_s(engine->drivers[idx].name, sizeof(engine->drivers[idx].name), subkey_name, _TRUNCATE);
RegCloseKey(driver_key);
}
RegCloseKey(asio_key);
}
////////////////////////////////
// Public API
AudioEngine *audio_create(void *hwnd) {
AudioEngine *engine = new AudioEngine();
memset(engine, 0, sizeof(*engine));
engine->hwnd = hwnd;
engine->active_device_index = -1;
g_audio_engine = engine;
CoInitialize(nullptr);
enumerate_asio_drivers(engine);
return engine;
}
void audio_destroy(AudioEngine *engine) {
audio_close_device(engine);
if (g_audio_engine == engine) g_audio_engine = nullptr;
delete engine;
}
void audio_refresh_devices(AudioEngine *engine) {
audio_close_device(engine);
enumerate_asio_drivers(engine);
}
int32_t audio_get_device_count(AudioEngine *engine) {
return engine->device_count;
}
AudioDeviceInfo *audio_get_device(AudioEngine *engine, int32_t index) {
if (index < 0 || index >= engine->device_count)
return nullptr;
return &engine->devices[index];
}
bool audio_open_device(AudioEngine *engine, int32_t index) {
// Close any existing device first
audio_close_device(engine);
if (index < 0 || index >= engine->device_count)
return false;
// Create COM instance of the ASIO driver
IASIO *driver = nullptr;
HRESULT hr = CoCreateInstance(engine->drivers[index].clsid,
nullptr, CLSCTX_INPROC_SERVER,
engine->drivers[index].clsid,
(void **)&driver);
if (FAILED(hr) || !driver)
return false;
// Initialize the driver
if (!driver->init(engine->hwnd)) {
driver->Release();
return false;
}
// Query channel counts
long num_in = 0, num_out = 0;
if (driver->getChannels(&num_in, &num_out) != ASE_OK || num_out <= 0) {
driver->Release();
return false;
}
// Query buffer size
long min_size = 0, max_size = 0, preferred_size = 0, granularity = 0;
if (driver->getBufferSize(&min_size, &max_size, &preferred_size, &granularity) != ASE_OK) {
driver->Release();
return false;
}
// Query sample rate
double sample_rate = 0;
if (driver->getSampleRate(&sample_rate) != ASE_OK || sample_rate <= 0) {
// Try setting a common rate
if (driver->setSampleRate(44100.0) == ASE_OK) {
sample_rate = 44100.0;
} else {
driver->Release();
return false;
}
}
// Query output channel sample type
ASIOChannelInfo chan_info = {};
chan_info.channel = 0;
chan_info.isInput = 0;
if (driver->getChannelInfo(&chan_info) != ASE_OK) {
driver->Release();
return false;
}
// Limit output channels
if (num_out > AUDIO_MAX_CHANNELS) num_out = AUDIO_MAX_CHANNELS;
// Setup buffer infos for output channels only
memset(engine->buffer_infos, 0, sizeof(engine->buffer_infos));
for (long ch = 0; ch < num_out; ch++) {
engine->buffer_infos[ch].isInput = 0;
engine->buffer_infos[ch].channelNum = ch;
engine->buffer_infos[ch].buffers[0] = nullptr;
engine->buffer_infos[ch].buffers[1] = nullptr;
}
// Setup callbacks
engine->callbacks.bufferSwitch = asio_buffer_switch;
engine->callbacks.sampleRateDidChange = asio_sample_rate_changed;
engine->callbacks.asioMessage = asio_message;
engine->callbacks.bufferSwitchTimeInfo = asio_buffer_switch_time_info;
// Create buffers
if (driver->createBuffers(engine->buffer_infos, num_out, preferred_size, &engine->callbacks) != ASE_OK) {
driver->Release();
return false;
}
// Store state
engine->driver = driver;
engine->active_device_index = index;
engine->num_output_channels = num_out;
engine->num_input_channels = num_in;
engine->buffer_size = preferred_size;
engine->sample_rate = sample_rate;
engine->output_sample_type = chan_info.type;
engine->test_tone_active = 0;
engine->test_tone_samples_remaining = 0;
engine->test_tone_phase = 0.0;
// Start the driver
if (driver->start() != ASE_OK) {
driver->disposeBuffers();
driver->Release();
engine->driver = nullptr;
engine->active_device_index = -1;
return false;
}
// Notify driver that output is ready (optional, some drivers benefit)
driver->outputReady();
return true;
}
void audio_close_device(AudioEngine *engine) {
if (!engine->driver) return;
InterlockedExchange(&engine->test_tone_active, 0);
InterlockedExchange(&engine->test_tone_samples_remaining, 0);
engine->driver->stop();
engine->driver->disposeBuffers();
engine->driver->Release();
engine->driver = nullptr;
engine->active_device_index = -1;
engine->test_tone_phase = 0.0;
}
void audio_play_test_tone(AudioEngine *engine) {
if (!engine->driver) return;
engine->test_tone_phase = 0.0;
LONG total_samples = (LONG)(engine->sample_rate * AUDIO_TEST_TONE_SEC);
InterlockedExchange(&engine->test_tone_samples_remaining, total_samples);
InterlockedExchange(&engine->test_tone_active, 1);
}
bool audio_is_test_tone_playing(AudioEngine *engine) {
return InterlockedCompareExchange(&engine->test_tone_active, 0, 0) != 0;
}
void audio_update(AudioEngine *engine, float dt) {
(void)engine;
(void)dt;
// Currently no per-frame work needed on the main thread.
// The ASIO callback handles all audio processing.
// This hook exists for future use (e.g., level metering).
}

View File

@@ -5,6 +5,7 @@
#include "platform/platform.h"
#include "renderer/renderer.h"
#include "midi/midi.h"
#include "audio/audio.h"
#include "ui/ui_core.h"
#include "ui/ui_theme.h"
#include "ui/ui_widgets.h"
@@ -16,6 +17,7 @@
#include "platform/platform_win32.cpp"
#include "renderer/renderer_dx12.cpp"
#include "midi/midi_win32.cpp"
#include "audio/audio_asio.cpp"
#include "menus.cpp"
////////////////////////////////
@@ -49,6 +51,7 @@ struct AppState {
PlatformWindow *window;
Renderer *renderer;
MidiEngine *midi;
AudioEngine *audio;
UI_Context *ui;
S32 last_w, last_h;
B32 show_browser;
@@ -77,6 +80,10 @@ struct AppState {
S32 settings_theme_sel;
B32 settings_vsync;
B32 settings_autosave;
// Audio device selection
S32 audio_device_sel; // dropdown index: 0 = None, 1+ = device
S32 audio_device_prev; // previous selection for change detection
};
////////////////////////////////
@@ -490,6 +497,80 @@ static void settings_window_content(void *user_data) {
ui_checkbox("SettingsVsync", "V-Sync", &app->settings_vsync);
ui_checkbox("SettingsAutosave", "Autosave", &app->settings_autosave);
// Separator
CLAY(CLAY_ID("SettingsSep1"),
.layout = { .sizing = { .width = CLAY_SIZING_GROW(), .height = CLAY_SIZING_FIXED(1) },
.padding = { 0, 0, 6, 6 } },
.backgroundColor = g_theme.border
) {}
// Audio device dropdown
ui_label("SettingsLblAudio", "Audio Device");
static const char *audio_options[AUDIO_MAX_DEVICES + 1];
int32_t audio_count = audio_get_device_count(app->audio);
audio_options[0] = "None";
for (int32_t i = 0; i < audio_count && i < AUDIO_MAX_DEVICES; i++) {
AudioDeviceInfo *dev = audio_get_device(app->audio, i);
audio_options[i + 1] = dev ? dev->name : "???";
}
CLAY(CLAY_ID("SettingsAudioWrap"),
.layout = { .sizing = { .width = CLAY_SIZING_FIXED(220), .height = CLAY_SIZING_FIT() } }
) {
ui_dropdown("SettingsAudio", audio_options, audio_count + 1, &app->audio_device_sel);
}
// Handle device selection change
if (app->audio_device_sel != app->audio_device_prev) {
audio_close_device(app->audio);
if (app->audio_device_sel > 0) {
audio_open_device(app->audio, app->audio_device_sel - 1);
}
app->audio_device_prev = app->audio_device_sel;
}
// Test tone button + status
CLAY(CLAY_ID("SettingsAudioBtnRow"),
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIT() },
.childGap = 8,
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
.layoutDirection = CLAY_LEFT_TO_RIGHT,
}
) {
bool device_open = (app->audio_device_sel > 0);
bool tone_playing = audio_is_test_tone_playing(app->audio);
if (device_open && !tone_playing) {
if (ui_button("BtnTestTone", "Play Test Tone")) {
audio_play_test_tone(app->audio);
}
} else {
// Disabled button appearance
Clay_ElementId btn_eid = CLAY_ID("BtnTestToneDisabled");
CLAY(btn_eid,
.layout = {
.sizing = { .width = CLAY_SIZING_FIT(), .height = CLAY_SIZING_FIXED(28) },
.padding = { 12, 12, 0, 0 },
.childAlignment = { .y = CLAY_ALIGN_Y_CENTER },
},
.backgroundColor = Clay_Color{50, 50, 50, 255},
.cornerRadius = CLAY_CORNER_RADIUS(3)
) {
static Clay_TextElementConfig disabled_text = {};
disabled_text.textColor = Clay_Color{100, 100, 100, 255};
disabled_text.fontSize = 15;
disabled_text.wrapMode = CLAY_TEXT_WRAP_NONE;
CLAY_TEXT(CLAY_STRING("Play Test Tone"), &disabled_text);
}
}
if (tone_playing) {
ui_label("SettingsLblPlaying", "Playing...");
}
}
}
static void about_window_content(void *user_data) {
@@ -580,8 +661,9 @@ static void do_frame(AppState *app) {
if (!renderer_begin_frame(app->renderer))
return;
// Update MIDI activity timers
// Update subsystems
midi_update(app->midi, dt);
audio_update(app->audio, dt);
// Gather input
PlatformInput input = platform_get_input(app->window);
@@ -630,6 +712,7 @@ int main(int argc, char **argv) {
}
MidiEngine *midi = midi_create();
AudioEngine *audio = audio_create(platform_get_native_handle(window));
// Initialize UI (Clay)
ui_init_theme();
@@ -644,6 +727,7 @@ int main(int argc, char **argv) {
app.window = window;
app.renderer = renderer;
app.midi = midi;
app.audio = audio;
app.ui = ui;
app.last_w = w;
app.last_h = h;
@@ -675,6 +759,7 @@ int main(int argc, char **argv) {
}
exit_app:
audio_destroy(audio);
midi_destroy(midi);
ui_destroy(ui);
renderer_destroy(renderer);