summaryrefslogtreecommitdiff
path: root/src/audio/backend/wav_dump_backend.cc
blob: 1158fb2e62110933faa58d972360c1a7798d34b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
// 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 <assert.h>
#include <stdio.h>
#include <string.h>

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) */