1use std::path::Path;
4
5use sva_core::CliError;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum SampleEncoding {
11 Float,
12 Pcm16,
13}
14
15pub fn write_channels(
16 planes: &[&[f32]],
17 sample_rate: u32,
18 path: &Path,
19 encoding: SampleEncoding,
20) -> Result<(), CliError> {
21 match encoding {
22 SampleEncoding::Float => write_as::<f32>(planes, sample_rate, path, 32, |s| s),
23 SampleEncoding::Pcm16 => write_as::<i16>(planes, sample_rate, path, 16, quantize),
24 }
25}
26
27pub fn write_wav(
28 samples: &[f32],
29 sample_rate: u32,
30 path: &Path,
31 encoding: SampleEncoding,
32) -> Result<(), CliError> {
33 write_channels(&[samples], sample_rate, path, encoding)
34}
35
36fn quantize(sample: f32) -> i16 {
37 (sample.clamp(-1.0, 1.0) * f32::from(i16::MAX)).round() as i16
38}
39
40fn unopened(path: &Path, e: &hound::Error) -> CliError {
42 match e {
43 hound::Error::IoError(io) if io.kind() == std::io::ErrorKind::NotFound => {
44 CliError::NotFound(format!("no such file: {}", path.display()))
45 }
46 e => CliError::Io(format!("could not open {}: {e}", path.display())),
47 }
48}
49
50pub fn read_channels(path: &Path) -> Result<(Vec<Vec<f32>>, u32), CliError> {
53 let mut reader = hound::WavReader::open(path).map_err(|e| unopened(path, &e))?;
54 let spec = reader.spec();
55 let channels = spec.channels as usize;
56 if channels == 0 {
57 return Err(CliError::Io(format!(
58 "{} declares 0 channels",
59 path.display()
60 )));
61 }
62 let mut planes: Vec<Vec<f32>> = vec![Vec::new(); channels];
63 let bail = |e: hound::Error| CliError::Io(format!("could not read {}: {e}", path.display()));
64 match spec.sample_format {
65 hound::SampleFormat::Float => {
66 for (i, sample) in reader.samples::<f32>().enumerate() {
67 planes[i % channels].push(sample.map_err(bail)?);
68 }
69 }
70 hound::SampleFormat::Int => {
71 let full_scale = ((1i64 << (spec.bits_per_sample - 1)) - 1) as f32;
72 for (i, sample) in reader.samples::<i32>().enumerate() {
73 planes[i % channels].push(unquantize(sample.map_err(bail)?, full_scale));
74 }
75 }
76 }
77 Ok((planes, spec.sample_rate))
78}
79
80pub fn read_wav(path: &Path) -> Result<(Vec<f32>, u32), CliError> {
81 let (mut planes, sample_rate) = read_channels(path)?;
82 Ok((planes.remove(0), sample_rate))
83}
84
85fn unquantize(sample: i32, full_scale: f32) -> f32 {
88 sample as f32 / full_scale
89}
90
91fn write_as<S: hound::Sample>(
92 planes: &[&[f32]],
93 sample_rate: u32,
94 path: &Path,
95 bits_per_sample: u16,
96 convert: impl Fn(f32) -> S,
97) -> Result<(), CliError> {
98 let sample_format = if bits_per_sample == 32 {
99 hound::SampleFormat::Float
100 } else {
101 hound::SampleFormat::Int
102 };
103 let spec = hound::WavSpec {
104 channels: planes.len().max(1) as u16,
105 sample_rate,
106 bits_per_sample,
107 sample_format,
108 };
109 let mut writer = hound::WavWriter::create(path, spec)
110 .map_err(|e| CliError::Io(format!("could not create {}: {e}", path.display())))?;
111 for i in 0..planes.first().map_or(0, |p| p.len()) {
112 for plane in planes {
113 writer.write_sample(convert(plane[i])).map_err(|e| {
114 CliError::Io(format!("could not write sample to {}: {e}", path.display()))
115 })?;
116 }
117 }
118 writer
119 .finalize()
120 .map_err(|e| CliError::Io(format!("could not finalize {}: {e}", path.display())))?;
121 Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 fn tmp(name: &str) -> std::path::PathBuf {
129 let path =
130 std::env::temp_dir().join(format!("sva-cli-wav-{name}-{:x}.wav", std::process::id()));
131 let _ = std::fs::remove_file(&path);
132 path
133 }
134
135 #[test]
136 fn pcm16_writes_a_plain_format_tag_a_bare_riff_parser_can_read() {
137 let path = tmp("pcm16-tag");
138 write_wav(&[0.5, -0.5, 1.5, -1.5], 44100, &path, SampleEncoding::Pcm16).unwrap();
139
140 let bytes = std::fs::read(&path).unwrap();
141 assert_eq!(&bytes[0..4], b"RIFF");
142 assert_eq!(&bytes[8..12], b"WAVE");
143 assert_eq!(&bytes[12..16], b"fmt ");
144 let format_tag = u16::from_le_bytes([bytes[20], bytes[21]]);
145 assert_ne!(
146 format_tag, 0xFFFE,
147 "WAVE_FORMAT_EXTENSIBLE, which Python's `wave` refuses"
148 );
149 assert_eq!(format_tag, 1, "WAVE_FORMAT_PCM");
150
151 let mut reader = hound::WavReader::open(&path).unwrap();
152 assert_eq!(reader.spec().bits_per_sample, 16);
153 assert_eq!(reader.spec().sample_format, hound::SampleFormat::Int);
154 let samples: Vec<i16> = reader.samples::<i16>().map(Result::unwrap).collect();
155 assert_eq!(samples, vec![16384, -16384, i16::MAX, -i16::MAX]);
156
157 let _ = std::fs::remove_file(&path);
158 }
159
160 #[test]
161 fn float_still_carries_every_bit_and_still_reads_back_as_extensible() {
162 let path = tmp("float-tag");
163 let pcm = [0.125f32, -0.25, 0.75];
164 write_wav(&pcm, 8000, &path, SampleEncoding::Float).unwrap();
165
166 let bytes = std::fs::read(&path).unwrap();
167 let format_tag = u16::from_le_bytes([bytes[20], bytes[21]]);
168 assert_eq!(
169 format_tag, 0xFFFE,
170 "float always needs WAVEFORMATEXTENSIBLE in hound"
171 );
172
173 let mut reader = hound::WavReader::open(&path).unwrap();
174 assert_eq!(reader.spec().bits_per_sample, 32);
175 let samples: Vec<f32> = reader.samples::<f32>().map(Result::unwrap).collect();
176 assert_eq!(samples, pcm);
177
178 let _ = std::fs::remove_file(&path);
179 }
180
181 #[test]
184 fn read_channels_round_trips_a_float_wav_exactly_at_its_own_rate() {
185 let path = tmp("read-float-roundtrip");
186 let left = [0.5f32, -0.25, 0.75, -1.0];
187 let right = [-0.5f32, 0.25, -0.75, 1.0];
188 write_channels(&[&left, &right], 48_000, &path, SampleEncoding::Float).unwrap();
189
190 let (planes, sample_rate) = read_channels(&path).unwrap();
191 assert_eq!(sample_rate, 48_000, "no resampling, ever");
192 assert_eq!(planes, vec![left.to_vec(), right.to_vec()]);
193
194 let _ = std::fs::remove_file(&path);
195 }
196
197 #[test]
200 fn read_channels_unquantizes_pcm16_within_one_step() {
201 let path = tmp("read-pcm16-roundtrip");
202 let pcm = [0.5f32, -0.5, 0.1, -0.9];
203 write_wav(&pcm, 44_100, &path, SampleEncoding::Pcm16).unwrap();
204
205 let (mono, sample_rate) = read_wav(&path).unwrap();
206 assert_eq!(sample_rate, 44_100);
207 assert_eq!(mono.len(), pcm.len());
208 for (got, want) in mono.iter().zip(&pcm) {
209 assert!(
210 (got - want).abs() < 1.0 / f32::from(i16::MAX),
211 "{got} vs {want}"
212 );
213 }
214
215 let _ = std::fs::remove_file(&path);
216 }
217
218 fn write_int(path: &std::path::Path, bits_per_sample: u16, values: &[i32], sample_rate: u32) {
219 let spec = hound::WavSpec {
220 channels: 1,
221 sample_rate,
222 bits_per_sample,
223 sample_format: hound::SampleFormat::Int,
224 };
225 let mut writer = hound::WavWriter::create(path, spec).unwrap();
226 for &v in values {
227 writer.write_sample(v).unwrap();
228 }
229 writer.finalize().unwrap();
230 }
231
232 #[test]
235 fn read_channels_unquantizes_every_int_bit_depth_hound_can_decode() {
236 let path = tmp("read-8bit");
237 write_int(&path, 8, &[127, -128, 0], 22_050);
238 let (planes, sample_rate) = read_channels(&path).unwrap();
239 assert_eq!(sample_rate, 22_050);
240 assert_eq!(planes.len(), 1);
241 assert!((planes[0][0] - 1.0).abs() < 0.01, "{:?}", planes[0]);
242 assert!((planes[0][1] + 1.0).abs() < 0.01, "{:?}", planes[0]);
243 assert_eq!(planes[0][2], 0.0);
244 let _ = std::fs::remove_file(&path);
245
246 let path = tmp("read-24bit");
247 let full_scale = (1i64 << 23) - 1;
248 write_int(
249 &path,
250 24,
251 &[full_scale as i32, -(full_scale as i32), 0],
252 96_000,
253 );
254 let (planes, sample_rate) = read_channels(&path).unwrap();
255 assert_eq!(sample_rate, 96_000);
256 assert_eq!(planes[0], vec![1.0, -1.0, 0.0]);
257 let _ = std::fs::remove_file(&path);
258 }
259
260 #[test]
263 fn read_channels_de_interleaves_every_component_into_its_own_plane() {
264 let path = tmp("read-three-channel");
265 let a = [0.1f32, 0.2];
266 let b = [0.3f32, 0.4];
267 let c = [0.5f32, 0.6];
268 write_channels(&[&a, &b, &c], 44_100, &path, SampleEncoding::Float).unwrap();
269
270 let (planes, _) = read_channels(&path).unwrap();
271 assert_eq!(planes, vec![a.to_vec(), b.to_vec(), c.to_vec()]);
272
273 let _ = std::fs::remove_file(&path);
274 }
275
276 #[test]
277 fn read_channels_reports_a_missing_file_as_not_found_not_a_panic() {
278 let path = tmp("read-missing");
279 assert!(matches!(read_channels(&path), Err(CliError::NotFound(_))));
280 }
281}