1use std::path::Path;
4
5use sva_core::CliError;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum SampleEncoding {
11 Float,
12 Pcm(u8),
14}
15
16impl SampleEncoding {
17 pub fn of(bits: i32) -> SampleEncoding {
18 match u8::try_from(bits) {
19 Ok(bits) if bits <= 16 => SampleEncoding::Pcm(bits),
20 _ => SampleEncoding::Float,
21 }
22 }
23}
24
25pub fn write_channels(
26 planes: &[&[f32]],
27 sample_rate: u32,
28 path: &Path,
29 encoding: SampleEncoding,
30) -> Result<(), CliError> {
31 match encoding {
32 SampleEncoding::Float => write_as::<f32>(planes, sample_rate, path, 32, |s| s),
33 SampleEncoding::Pcm(bits) if bits <= 8 => {
34 write_as::<i8>(planes, sample_rate, path, 8, |s| quantize(s, bits, 8) as i8)
35 }
36 SampleEncoding::Pcm(bits) => write_as::<i16>(planes, sample_rate, path, 16, |s| {
37 quantize(s, bits, 16) as i16
38 }),
39 }
40}
41
42pub fn write_wav(
43 samples: &[f32],
44 sample_rate: u32,
45 path: &Path,
46 encoding: SampleEncoding,
47) -> Result<(), CliError> {
48 write_channels(&[samples], sample_rate, path, encoding)
49}
50
51pub(crate) fn quantize(sample: f32, bits: u8, word: u8) -> i32 {
53 let full = ((1i32 << (bits - 1)) - 1) as f32;
54 ((sample.clamp(-1.0, 1.0) * full).round() as i32) << (word - bits)
55}
56
57fn write_as<S: hound::Sample>(
58 planes: &[&[f32]],
59 sample_rate: u32,
60 path: &Path,
61 bits_per_sample: u16,
62 convert: impl Fn(f32) -> S,
63) -> Result<(), CliError> {
64 let sample_format = if bits_per_sample == 32 {
65 hound::SampleFormat::Float
66 } else {
67 hound::SampleFormat::Int
68 };
69 let spec = hound::WavSpec {
70 channels: planes.len().max(1) as u16,
71 sample_rate,
72 bits_per_sample,
73 sample_format,
74 };
75 let mut writer = hound::WavWriter::create(path, spec)
76 .map_err(|e| CliError::Io(format!("could not create {}: {e}", path.display())))?;
77 for i in 0..planes.first().map_or(0, |p| p.len()) {
78 for plane in planes {
79 writer.write_sample(convert(plane[i])).map_err(|e| {
80 CliError::Io(format!("could not write sample to {}: {e}", path.display()))
81 })?;
82 }
83 }
84 writer
85 .finalize()
86 .map_err(|e| CliError::Io(format!("could not finalize {}: {e}", path.display())))?;
87 Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93
94 fn tmp(name: &str) -> std::path::PathBuf {
95 let path =
96 std::env::temp_dir().join(format!("sva-cli-wav-{name}-{:x}.wav", std::process::id()));
97 let _ = std::fs::remove_file(&path);
98 path
99 }
100
101 #[test]
102 fn pcm16_writes_a_plain_format_tag_a_bare_riff_parser_can_read() {
103 let path = tmp("pcm16-tag");
104 write_wav(
105 &[0.5, -0.5, 1.5, -1.5],
106 44100,
107 &path,
108 SampleEncoding::Pcm(16),
109 )
110 .unwrap();
111
112 let bytes = std::fs::read(&path).unwrap();
113 assert_eq!(&bytes[0..4], b"RIFF");
114 assert_eq!(&bytes[8..12], b"WAVE");
115 assert_eq!(&bytes[12..16], b"fmt ");
116 let format_tag = u16::from_le_bytes([bytes[20], bytes[21]]);
117 assert_ne!(
118 format_tag, 0xFFFE,
119 "WAVE_FORMAT_EXTENSIBLE, which Python's `wave` refuses"
120 );
121 assert_eq!(format_tag, 1, "WAVE_FORMAT_PCM");
122
123 let mut reader = hound::WavReader::open(&path).unwrap();
124 assert_eq!(reader.spec().bits_per_sample, 16);
125 assert_eq!(reader.spec().sample_format, hound::SampleFormat::Int);
126 let samples: Vec<i16> = reader.samples::<i16>().map(Result::unwrap).collect();
127 assert_eq!(samples, vec![16384, -16384, i16::MAX, -i16::MAX]);
128
129 let _ = std::fs::remove_file(&path);
130 }
131
132 #[test]
133 fn float_still_carries_every_bit_and_still_reads_back_as_extensible() {
134 let path = tmp("float-tag");
135 let pcm = [0.125f32, -0.25, 0.75];
136 write_wav(&pcm, 8000, &path, SampleEncoding::Float).unwrap();
137
138 let bytes = std::fs::read(&path).unwrap();
139 let format_tag = u16::from_le_bytes([bytes[20], bytes[21]]);
140 assert_eq!(
141 format_tag, 0xFFFE,
142 "float always needs WAVEFORMATEXTENSIBLE in hound"
143 );
144
145 let mut reader = hound::WavReader::open(&path).unwrap();
146 assert_eq!(reader.spec().bits_per_sample, 32);
147 let samples: Vec<f32> = reader.samples::<f32>().map(Result::unwrap).collect();
148 assert_eq!(samples, pcm);
149
150 let _ = std::fs::remove_file(&path);
151 }
152}