// 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 #include #include WavDumpBackend::WavDumpBackend() : wav_file_(nullptr), samples_written_(0), clipped_samples_(0), output_filename_("audio_dump.wav"), is_active_(false), duration_sec_(0.0f) { 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; clipped_samples_ = 0; printf("WAV dump backend initialized: %s\n", output_filename_); } void WavDumpBackend::start() { is_active_ = true; printf("WAV dump started (passive mode - frontend drives rendering)\n"); } void WavDumpBackend::write_audio(const float* samples, int num_samples) { if (!is_active_ || wav_file_ == nullptr) { return; } // CRITICAL: This method must match MiniaudioBackend's sample handling // behavior to ensure WAV dumps accurately reflect live audio output. // // Current behavior (verified 2026-02-07): // - MiniaudioBackend passes float samples directly to miniaudio without // clamping (see miniaudio_backend.cc:140) // - Miniaudio internally converts float→int16 and handles overflow // - We replicate this: no clamping, count out-of-range samples for // diagnostics // // If miniaudio's sample handling changes (e.g., they add clamping or // different overflow behavior), this code MUST be updated to match. // Verify by checking: src/audio/miniaudio_backend.cc data_callback() for (int i = 0; i < num_samples; ++i) { float sample = samples[i]; // Track samples outside [-1.0, 1.0] range for diagnostic reporting // This helps identify audio distortion issues during development if (sample > 1.0f || sample < -1.0f) { clipped_samples_++; } // Convert float→int16 with same overflow behavior as miniaudio // Values outside [-1.0, 1.0] will wrap/clip during conversion const int16_t sample_i16 = (int16_t)(sample * 32767.0f); fwrite(&sample_i16, sizeof(int16_t), 1, wav_file_); } samples_written_ += num_samples; } void WavDumpBackend::shutdown() { if (wav_file_ != nullptr) { // Update header with final sample count update_wav_header(); fclose(wav_file_); wav_file_ = nullptr; const float duration_sec = (float)samples_written_ / (kSampleRate * 2); printf("WAV file written: %s (%.2f seconds, %zu samples)\n", output_filename_, duration_sec, samples_written_); // Report clipping diagnostics if (clipped_samples_ > 0) { const float clip_percent = (float)clipped_samples_ / (float)samples_written_ * 100.0f; printf(" WARNING: %zu samples clipped (%.2f%% of total)\n", clipped_samples_, clip_percent); printf(" This indicates audio distortion - consider reducing volume\n"); } else { printf(" ✓ No clipping detected\n"); } } is_active_ = false; } float WavDumpBackend::get_realtime_peak() { // WAV dump: No real-time playback, return 0 // Could optionally track peak of last written chunk if needed return 0.0f; } 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 = 2; // Stereo (matches miniaudio config) 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) */