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",
|
"gdi32.lib",
|
||||||
"shell32.lib",
|
"shell32.lib",
|
||||||
"ole32.lib",
|
"ole32.lib",
|
||||||
|
"advapi32.lib",
|
||||||
"dwmapi.lib",
|
"dwmapi.lib",
|
||||||
"winmm.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 "platform/platform.h"
|
||||||
#include "renderer/renderer.h"
|
#include "renderer/renderer.h"
|
||||||
#include "midi/midi.h"
|
#include "midi/midi.h"
|
||||||
|
#include "audio/audio.h"
|
||||||
#include "ui/ui_core.h"
|
#include "ui/ui_core.h"
|
||||||
#include "ui/ui_theme.h"
|
#include "ui/ui_theme.h"
|
||||||
#include "ui/ui_widgets.h"
|
#include "ui/ui_widgets.h"
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
#include "platform/platform_win32.cpp"
|
#include "platform/platform_win32.cpp"
|
||||||
#include "renderer/renderer_dx12.cpp"
|
#include "renderer/renderer_dx12.cpp"
|
||||||
#include "midi/midi_win32.cpp"
|
#include "midi/midi_win32.cpp"
|
||||||
|
#include "audio/audio_asio.cpp"
|
||||||
#include "menus.cpp"
|
#include "menus.cpp"
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
@@ -49,6 +51,7 @@ struct AppState {
|
|||||||
PlatformWindow *window;
|
PlatformWindow *window;
|
||||||
Renderer *renderer;
|
Renderer *renderer;
|
||||||
MidiEngine *midi;
|
MidiEngine *midi;
|
||||||
|
AudioEngine *audio;
|
||||||
UI_Context *ui;
|
UI_Context *ui;
|
||||||
S32 last_w, last_h;
|
S32 last_w, last_h;
|
||||||
B32 show_browser;
|
B32 show_browser;
|
||||||
@@ -77,6 +80,10 @@ struct AppState {
|
|||||||
S32 settings_theme_sel;
|
S32 settings_theme_sel;
|
||||||
B32 settings_vsync;
|
B32 settings_vsync;
|
||||||
B32 settings_autosave;
|
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("SettingsVsync", "V-Sync", &app->settings_vsync);
|
||||||
ui_checkbox("SettingsAutosave", "Autosave", &app->settings_autosave);
|
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) {
|
static void about_window_content(void *user_data) {
|
||||||
@@ -580,8 +661,9 @@ static void do_frame(AppState *app) {
|
|||||||
if (!renderer_begin_frame(app->renderer))
|
if (!renderer_begin_frame(app->renderer))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Update MIDI activity timers
|
// Update subsystems
|
||||||
midi_update(app->midi, dt);
|
midi_update(app->midi, dt);
|
||||||
|
audio_update(app->audio, dt);
|
||||||
|
|
||||||
// Gather input
|
// Gather input
|
||||||
PlatformInput input = platform_get_input(app->window);
|
PlatformInput input = platform_get_input(app->window);
|
||||||
@@ -630,6 +712,7 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MidiEngine *midi = midi_create();
|
MidiEngine *midi = midi_create();
|
||||||
|
AudioEngine *audio = audio_create(platform_get_native_handle(window));
|
||||||
|
|
||||||
// Initialize UI (Clay)
|
// Initialize UI (Clay)
|
||||||
ui_init_theme();
|
ui_init_theme();
|
||||||
@@ -644,6 +727,7 @@ int main(int argc, char **argv) {
|
|||||||
app.window = window;
|
app.window = window;
|
||||||
app.renderer = renderer;
|
app.renderer = renderer;
|
||||||
app.midi = midi;
|
app.midi = midi;
|
||||||
|
app.audio = audio;
|
||||||
app.ui = ui;
|
app.ui = ui;
|
||||||
app.last_w = w;
|
app.last_w = w;
|
||||||
app.last_h = h;
|
app.last_h = h;
|
||||||
@@ -675,6 +759,7 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exit_app:
|
exit_app:
|
||||||
|
audio_destroy(audio);
|
||||||
midi_destroy(midi);
|
midi_destroy(midi);
|
||||||
ui_destroy(ui);
|
ui_destroy(ui);
|
||||||
renderer_destroy(renderer);
|
renderer_destroy(renderer);
|
||||||
|
|||||||
Reference in New Issue
Block a user