summaryrefslogtreecommitdiff
path: root/src/audio/wav_dump_backend.cc
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-02-04 13:55:45 +0100
committerskal <pascal.massimino@gmail.com>2026-02-04 13:55:45 +0100
commit18c553b0aaa9a574a2a40e5499120ac8a802b735 (patch)
tree16676071ac1cb7659dd9625e57020e4c7ace4b40 /src/audio/wav_dump_backend.cc
parent6375468ea8d48a57f44e2d8bffd948e6a87ead89 (diff)
feat(audio): Add WAV dump backend for debugging audio output
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 <noreply@anthropic.com>
Diffstat (limited to 'src/audio/wav_dump_backend.cc')
-rw-r--r--src/audio/wav_dump_backend.cc139
1 files changed, 139 insertions, 0 deletions
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 <assert.h>
+#include <stdio.h>
+#include <string.h>
+
+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) */