Skip to main content

oxihuman_export/
wav_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! WAV PCM audio export (44-byte header + 16-bit samples).
6
7/// WAV export configuration.
8#[allow(dead_code)]
9pub struct WavConfig {
10    pub sample_rate: u32,
11    pub channels: u16,
12    pub bits_per_sample: u16,
13}
14
15impl Default for WavConfig {
16    fn default() -> Self {
17        Self {
18            sample_rate: 44100,
19            channels: 1,
20            bits_per_sample: 16,
21        }
22    }
23}
24
25/// A WAV file in memory.
26#[allow(dead_code)]
27pub struct WavFile {
28    pub config: WavConfig,
29    pub samples: Vec<i16>,
30}
31
32/// Build a 44-byte WAV header.
33#[allow(dead_code)]
34pub fn build_wav_header(config: &WavConfig, data_bytes: u32) -> Vec<u8> {
35    let byte_rate =
36        config.sample_rate * config.channels as u32 * (config.bits_per_sample as u32 / 8);
37    let block_align = config.channels * (config.bits_per_sample / 8);
38    let file_size = 36 + data_bytes;
39    let mut hdr = Vec::with_capacity(44);
40    hdr.extend_from_slice(b"RIFF");
41    hdr.extend_from_slice(&file_size.to_le_bytes());
42    hdr.extend_from_slice(b"WAVE");
43    hdr.extend_from_slice(b"fmt ");
44    hdr.extend_from_slice(&16u32.to_le_bytes());
45    hdr.extend_from_slice(&1u16.to_le_bytes());
46    hdr.extend_from_slice(&config.channels.to_le_bytes());
47    hdr.extend_from_slice(&config.sample_rate.to_le_bytes());
48    hdr.extend_from_slice(&byte_rate.to_le_bytes());
49    hdr.extend_from_slice(&block_align.to_le_bytes());
50    hdr.extend_from_slice(&config.bits_per_sample.to_le_bytes());
51    hdr.extend_from_slice(b"data");
52    hdr.extend_from_slice(&data_bytes.to_le_bytes());
53    hdr
54}
55
56/// Encode samples to 16-bit LE bytes.
57#[allow(dead_code)]
58pub fn encode_samples_i16(samples: &[i16]) -> Vec<u8> {
59    samples.iter().flat_map(|s| s.to_le_bytes()).collect()
60}
61
62/// Export WAV to byte buffer.
63#[allow(dead_code)]
64pub fn export_wav(wav: &WavFile) -> Vec<u8> {
65    let data = encode_samples_i16(&wav.samples);
66    let mut out = build_wav_header(&wav.config, data.len() as u32);
67    out.extend_from_slice(&data);
68    out
69}
70
71/// Create a silent WAV of the given duration.
72#[allow(dead_code)]
73pub fn silent_wav(config: WavConfig, duration_secs: f32) -> WavFile {
74    let n = (config.sample_rate as f32 * duration_secs * config.channels as f32) as usize;
75    WavFile {
76        config,
77        samples: vec![0i16; n],
78    }
79}
80
81/// Generate a sine wave in a WAV file.
82#[allow(dead_code)]
83pub fn sine_wav(config: WavConfig, freq_hz: f32, duration_secs: f32, amplitude: f32) -> WavFile {
84    let n = (config.sample_rate as f32 * duration_secs) as usize;
85    let sr = config.sample_rate as f32;
86    let samples: Vec<i16> = (0..n)
87        .map(|i| {
88            let t = i as f32 / sr;
89            let v = amplitude * (2.0 * std::f32::consts::PI * freq_hz * t).sin();
90            (v * i16::MAX as f32) as i16
91        })
92        .collect();
93    WavFile { config, samples }
94}
95
96/// Duration in seconds.
97#[allow(dead_code)]
98pub fn wav_duration(wav: &WavFile) -> f32 {
99    if wav.config.sample_rate == 0 || wav.config.channels == 0 {
100        return 0.0;
101    }
102    wav.samples.len() as f32 / (wav.config.sample_rate as f32 * wav.config.channels as f32)
103}
104
105/// Peak sample value.
106#[allow(dead_code)]
107pub fn wav_peak_amplitude(wav: &WavFile) -> i16 {
108    wav.samples.iter().map(|s| s.abs()).max().unwrap_or(0)
109}
110
111/// Byte size of exported WAV.
112#[allow(dead_code)]
113pub fn wav_export_size(wav: &WavFile) -> usize {
114    44 + wav.samples.len() * 2
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn header_is_44_bytes() {
123        let config = WavConfig::default();
124        let hdr = build_wav_header(&config, 0);
125        assert_eq!(hdr.len(), 44);
126    }
127
128    #[test]
129    fn header_starts_with_riff() {
130        let config = WavConfig::default();
131        let hdr = build_wav_header(&config, 0);
132        assert_eq!(&hdr[0..4], b"RIFF");
133    }
134
135    #[test]
136    fn encode_samples_byte_length() {
137        let samples = vec![0i16, 100, -100, i16::MAX];
138        let bytes = encode_samples_i16(&samples);
139        assert_eq!(bytes.len(), 8);
140    }
141
142    #[test]
143    fn export_wav_total_size() {
144        let wav = silent_wav(WavConfig::default(), 0.01);
145        let out = export_wav(&wav);
146        assert_eq!(out.len(), wav_export_size(&wav));
147    }
148
149    #[test]
150    fn silent_wav_all_zeros() {
151        let wav = silent_wav(WavConfig::default(), 0.01);
152        assert!(wav.samples.iter().all(|&s| s == 0));
153    }
154
155    #[test]
156    fn sine_wav_nonzero() {
157        let wav = sine_wav(WavConfig::default(), 440.0, 0.01, 0.5);
158        assert!(wav_peak_amplitude(&wav) > 0);
159    }
160
161    #[test]
162    fn wav_duration_correct() {
163        let config = WavConfig::default();
164        let dur = 0.5;
165        let wav = silent_wav(config, dur);
166        assert!((wav_duration(&wav) - dur).abs() < 0.001);
167    }
168
169    #[test]
170    fn wav_export_size_formula() {
171        let wav = silent_wav(WavConfig::default(), 0.01);
172        assert_eq!(wav_export_size(&wav), 44 + wav.samples.len() * 2);
173    }
174
175    #[test]
176    fn wave_contains_wave_marker() {
177        let config = WavConfig::default();
178        let hdr = build_wav_header(&config, 0);
179        assert_eq!(&hdr[8..12], b"WAVE");
180    }
181
182    #[test]
183    fn default_config_correct() {
184        let c = WavConfig::default();
185        assert_eq!(c.sample_rate, 44100);
186        assert_eq!(c.channels, 1);
187        assert_eq!(c.bits_per_sample, 16);
188    }
189}