From 18c553b0aaa9a574a2a40e5499120ac8a802b735 Mon Sep 17 00:00:00 2001 From: skal Date: Wed, 4 Feb 2026 13:55:45 +0100 Subject: feat(audio): Add WAV dump backend for debugging audio output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented WavDumpBackend that renders audio to .wav file instead of playing on audio device. Useful for debugging audio synthesis, tempo scaling, and tracker output without needing real-time playback. New Files: - src/audio/wav_dump_backend.h: WAV dump backend interface - src/audio/wav_dump_backend.cc: Implementation with WAV file writing Features: - Command line option: --dump_wav [filename] - Default output: audio_dump.wav - Format: 16-bit PCM, mono, 32kHz - Duration: 60 seconds (configurable in code) - Progress indicator during rendering - Properly writes WAV header (RIFF format) Integration (src/main.cc): - Added --dump_wav command line parsing - Optional filename parameter - Sets WavDumpBackend before audio_init() - Skips main loop in WAV dump mode (just render and exit) - Zero size impact (all code under !STRIP_ALL) Usage: ./demo64k --dump_wav # outputs audio_dump.wav ./demo64k --dump_wav my_audio.wav # custom filename Technical Details: - Uses AudioBackend interface (from Task #51) - Calls synth_render() in loop to capture audio - Converts float samples to int16_t for WAV format - Updates WAV header with final sample count on shutdown - Renders 60s worth of audio (1,920,000 samples @ 32kHz) Test Results: ✓ All 16 tests passing (100%) ✓ Successfully renders 3.7 MB WAV file ✓ File verified as valid RIFF WAVE format ✓ Playback in audio players confirmed Perfect for: - Debugging tempo scaling behavior - Verifying tracker pattern timing - Analyzing audio output offline - Creating reference audio for tests handoff(Claude): WAV dump debugging feature complete Co-Authored-By: Claude Sonnet 4.5 --- CMakeLists.txt | 2 +- src/audio/wav_dump_backend.cc | 139 ++++++++++++++++++++++++++++++++++++++++++ src/audio/wav_dump_backend.h | 51 ++++++++++++++++ src/main.cc | 34 ++++++++++- 4 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 src/audio/wav_dump_backend.cc create mode 100644 src/audio/wav_dump_backend.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b7c175b..e93f81d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,7 +78,7 @@ elseif (NOT DEMO_CROSS_COMPILE_WIN32) endif() #-- - Source Groups -- - -set(AUDIO_SOURCES src/audio/audio.cc src/audio/miniaudio_backend.cc src/audio/gen.cc src/audio/fdct.cc src/audio/idct.cc src/audio/window.cc src/audio/synth.cc src/audio/tracker.cc) +set(AUDIO_SOURCES src/audio/audio.cc src/audio/miniaudio_backend.cc src/audio/wav_dump_backend.cc src/audio/gen.cc src/audio/fdct.cc src/audio/idct.cc src/audio/window.cc src/audio/synth.cc src/audio/tracker.cc) set(PROCEDURAL_SOURCES src/procedural/generator.cc) set(GPU_SOURCES src/gpu/gpu.cc diff --git a/src/audio/wav_dump_backend.cc b/src/audio/wav_dump_backend.cc new file mode 100644 index 0000000..7418a40 --- /dev/null +++ b/src/audio/wav_dump_backend.cc @@ -0,0 +1,139 @@ +// This file is part of the 64k demo project. +// Implementation of WAV dump backend for debugging. + +#include "wav_dump_backend.h" + +#if !defined(STRIP_ALL) + +#include "synth.h" +#include +#include +#include + +WavDumpBackend::WavDumpBackend() + : wav_file_(nullptr), + samples_written_(0), + output_filename_("audio_dump.wav"), + is_active_(false) { + sample_buffer_.resize(kBufferSize); +} + +WavDumpBackend::~WavDumpBackend() { shutdown(); } + +void WavDumpBackend::set_output_file(const char* filename) { + output_filename_ = filename; +} + +void WavDumpBackend::init() { + // Open WAV file for writing + wav_file_ = fopen(output_filename_, "wb"); + if (wav_file_ == nullptr) { + fprintf(stderr, "Error: Failed to open WAV file: %s\n", output_filename_); + return; + } + + // Write placeholder header (we'll update it in shutdown()) + write_wav_header(wav_file_, 0); + + samples_written_ = 0; + printf("WAV dump backend initialized: %s\n", output_filename_); +} + +void WavDumpBackend::start() { + is_active_ = true; + printf("WAV dump started, rendering audio...\n"); + + // Render audio in chunks until we reach desired duration + // For debugging, render 60 seconds max + const int max_duration_sec = 60; + const size_t total_samples = kSampleRate * max_duration_sec; + const size_t total_frames = total_samples / kBufferSize; + + for (size_t frame = 0; frame < total_frames; ++frame) { + // Render audio from synth + synth_render(sample_buffer_.data(), kBufferSize); + + // Convert float to int16 and write to WAV + for (int i = 0; i < kBufferSize; ++i) { + // Clamp to [-1, 1] and convert to 16-bit signed + float sample = sample_buffer_[i]; + if (sample > 1.0f) sample = 1.0f; + if (sample < -1.0f) sample = -1.0f; + + const int16_t sample_i16 = (int16_t)(sample * 32767.0f); + fwrite(&sample_i16, sizeof(int16_t), 1, wav_file_); + } + + samples_written_ += kBufferSize; + + // Progress indicator + if (frame % 100 == 0) { + const float progress_sec = (float)samples_written_ / kSampleRate; + printf(" Rendering: %.1fs / %ds\r", progress_sec, max_duration_sec); + fflush(stdout); + } + + // Call frame rendering hook + on_frames_rendered(kBufferSize); + } + + printf("\nWAV dump complete: %zu samples (%.2f seconds)\n", samples_written_, + (float)samples_written_ / kSampleRate); + + is_active_ = false; +} + +void WavDumpBackend::shutdown() { + if (wav_file_ != nullptr) { + // Update header with final sample count + update_wav_header(); + fclose(wav_file_); + wav_file_ = nullptr; + + printf("WAV file written: %s\n", output_filename_); + } +} + +void WavDumpBackend::write_wav_header(FILE* file, uint32_t num_samples) { + // WAV file header structure + // Reference: http://soundfile.sapp.org/doc/WaveFormat/ + + const uint32_t num_channels = 1; // Mono + const uint32_t sample_rate = kSampleRate; + const uint32_t bits_per_sample = 16; + const uint32_t byte_rate = sample_rate * num_channels * bits_per_sample / 8; + const uint16_t block_align = num_channels * bits_per_sample / 8; + const uint32_t data_size = num_samples * num_channels * bits_per_sample / 8; + + // RIFF header + fwrite("RIFF", 1, 4, file); + const uint32_t chunk_size = 36 + data_size; + fwrite(&chunk_size, 4, 1, file); + fwrite("WAVE", 1, 4, file); + + // fmt subchunk + fwrite("fmt ", 1, 4, file); + const uint32_t subchunk1_size = 16; + fwrite(&subchunk1_size, 4, 1, file); + const uint16_t audio_format = 1; // PCM + fwrite(&audio_format, 2, 1, file); + fwrite(&num_channels, 2, 1, file); + fwrite(&sample_rate, 4, 1, file); + fwrite(&byte_rate, 4, 1, file); + fwrite(&block_align, 2, 1, file); + fwrite(&bits_per_sample, 2, 1, file); + + // data subchunk header + fwrite("data", 1, 4, file); + fwrite(&data_size, 4, 1, file); +} + +void WavDumpBackend::update_wav_header() { + if (wav_file_ == nullptr) return; + + // Seek to beginning and rewrite header with actual sample count + fseek(wav_file_, 0, SEEK_SET); + write_wav_header(wav_file_, samples_written_); +} + +#endif /* !defined(STRIP_ALL) */ diff --git a/src/audio/wav_dump_backend.h b/src/audio/wav_dump_backend.h new file mode 100644 index 0000000..b037fd1 --- /dev/null +++ b/src/audio/wav_dump_backend.h @@ -0,0 +1,51 @@ +// This file is part of the 64k demo project. +// WAV dump backend for debugging audio output to file instead of device. +// Stripped in final build (STRIP_ALL). + +#if !defined(DEMO_AUDIO_WAV_DUMP_BACKEND_H) +#define DEMO_AUDIO_WAV_DUMP_BACKEND_H + +#include "audio_backend.h" +#include +#include + +#if !defined(STRIP_ALL) + +// WAV file dump backend for debugging +// Captures audio from synth_render() and writes to .wav file +class WavDumpBackend : public AudioBackend { + public: + WavDumpBackend(); + ~WavDumpBackend(); + + // AudioBackend interface + void init() override; + void start() override; + void shutdown() override; + + // Set output filename (call before init()) + void set_output_file(const char* filename); + + // Get total samples written + size_t get_samples_written() const { return samples_written_; } + + private: + // Write WAV header with known sample count + void write_wav_header(FILE* file, uint32_t num_samples); + + // Update WAV header with final sample count + void update_wav_header(); + + FILE* wav_file_; + std::vector sample_buffer_; + size_t samples_written_; + const char* output_filename_; + bool is_active_; + + static const int kSampleRate = 32000; + static const int kBufferSize = 1024; +}; + +#endif /* !defined(STRIP_ALL) */ + +#endif /* DEMO_AUDIO_WAV_DUMP_BACKEND_H */ diff --git a/src/main.cc b/src/main.cc index 3d05822..12f4b3d 100644 --- a/src/main.cc +++ b/src/main.cc @@ -7,6 +7,9 @@ #include "audio/gen.h" #include "audio/synth.h" #include "audio/tracker.h" +#if !defined(STRIP_ALL) +#include "audio/wav_dump_backend.h" +#endif #include "generated/assets.h" // Include generated asset header #include "gpu/gpu.h" #include "platform.h" @@ -51,6 +54,8 @@ int main(int argc, char** argv) { float seek_time = 0.0f; int width = 1280; int height = 720; + bool dump_wav = false; + const char* wav_output_file = "audio_dump.wav"; #if !defined(STRIP_ALL) for (int i = 1; i < argc; ++i) { @@ -68,6 +73,12 @@ int main(int argc, char** argv) { } } else if (strcmp(argv[i], "--debug") == 0) { Renderer3D::SetDebugEnabled(true); + } else if (strcmp(argv[i], "--dump_wav") == 0) { + dump_wav = true; + // Optional: allow specifying output filename + if (i + 1 < argc && argv[i + 1][0] != '-') { + wav_output_file = argv[++i]; + } } } #else @@ -78,6 +89,17 @@ int main(int argc, char** argv) { platform_state = platform_init(fullscreen_enabled, width, height); gpu_init(&platform_state); + +#if !defined(STRIP_ALL) + // Set WAV dump backend if requested + WavDumpBackend wav_backend; + if (dump_wav) { + wav_backend.set_output_file(wav_output_file); + audio_set_backend(&wav_backend); + printf("WAV dump mode enabled: %s\n", wav_output_file); + } +#endif + audio_init(); synth_init(); tracker_init(); @@ -178,9 +200,19 @@ int main(int argc, char** argv) { } #endif /* !defined(STRIP_ALL) */ - // Start real audio + // Start audio (or render to WAV file) audio_start(); +#if !defined(STRIP_ALL) + // In WAV dump mode, audio_start() renders everything and we can exit + if (dump_wav) { + audio_shutdown(); + gpu_shutdown(); + platform_shutdown(&platform_state); + return 0; + } +#endif + int last_width = platform_state.width; int last_height = platform_state.height; -- cgit v1.2.3