diff --git a/nob.c b/nob.c index e041c36..098c39f 100644 --- a/nob.c +++ b/nob.c @@ -14,6 +14,7 @@ static const char *link_libs[] = { "gdi32.lib", "shell32.lib", "ole32.lib", + "advapi32.lib", "dwmapi.lib", "winmm.lib", }; diff --git a/src/audio/audio.h b/src/audio/audio.h new file mode 100644 index 0000000..5fd4694 --- /dev/null +++ b/src/audio/audio.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include + +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); diff --git a/src/audio/audio_asio.cpp b/src/audio/audio_asio.cpp new file mode 100644 index 0000000..ff28c8e --- /dev/null +++ b/src/audio/audio_asio.cpp @@ -0,0 +1,569 @@ +#include "audio/audio.h" +#include +#include +#include +#include + +#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). +} diff --git a/src/main.cpp b/src/main.cpp index 03a763b..d8a2d8e 100644 --- a/src/main.cpp +++ b/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);