summaryrefslogtreecommitdiff
path: root/src/audio
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
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')
-rw-r--r--src/audio/wav_dump_backend.cc139
-rw-r--r--src/audio/wav_dump_backend.h51
2 files changed, 190 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) */
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 <stdio.h>
+#include <vector>
+
+#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<float> 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 */