Add base audio + asio implementation
This commit is contained in:
1
nob.c
1
nob.c
@@ -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
24
src/audio/audio.h
Normal 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
569
src/audio/audio_asio.cpp
Normal 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).
|
||||
}
|
||||
87
src/main.cpp
87
src/main.cpp
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user