1use std::path::Path;
4
5use sva_core::CliError;
6use symphonia::core::codecs::audio::AudioDecoderOptions;
7use symphonia::core::errors::Error;
8use symphonia::core::formats::probe::Hint;
9use symphonia::core::formats::{FormatOptions, TrackType};
10use symphonia::core::io::MediaSourceStream;
11use symphonia::core::meta::MetadataOptions;
12
13type Planes = (Vec<Vec<f32>>, u32);
15
16pub const READS: [&str; 8] = ["wav", "mp3", "m4a", "mp4", "aac", "flac", "ogg", "oga"];
18
19pub const UNREADABLE: &str = sva_core::RECORDING_UNREADABLE;
21
22pub fn read_recording(path: &Path) -> Result<Planes, CliError> {
25 let extension = path
26 .extension()
27 .and_then(|e| e.to_str())
28 .map(str::to_ascii_lowercase)
29 .unwrap_or_default();
30 match extension.as_str() {
31 "wav" => wav(path),
32 known if READS.contains(&known) => decoded(path, known),
33 _ => Err(CliError::usage(
34 UNREADABLE,
35 format!(
36 "`{}` is in no format this reads; it reads `.{}`",
37 path.display(),
38 READS.join("`, `.")
39 ),
40 )),
41 }
42}
43
44fn missing(path: &Path, e: &std::io::Error) -> CliError {
46 match e.kind() {
47 std::io::ErrorKind::NotFound => {
48 CliError::NotFound(format!("no such file: {}", path.display()))
49 }
50 _ => CliError::Io(format!("could not open {}: {e}", path.display())),
51 }
52}
53
54fn wav(path: &Path) -> Result<Planes, CliError> {
55 let mut reader = hound::WavReader::open(path).map_err(|e| match e {
56 hound::Error::IoError(io) => missing(path, &io),
57 e => CliError::usage(
58 UNREADABLE,
59 format!("could not read {}: {e}", path.display()),
60 ),
61 })?;
62 let spec = reader.spec();
63 let channels = spec.channels as usize;
64 if channels == 0 {
65 return Err(CliError::usage(
66 UNREADABLE,
67 format!("{} declares 0 channels", path.display()),
68 ));
69 }
70 let mut planes: Vec<Vec<f32>> = vec![Vec::new(); channels];
71 let bail = |e: hound::Error| CliError::Io(format!("could not read {}: {e}", path.display()));
72 match spec.sample_format {
73 hound::SampleFormat::Float => {
74 for (i, sample) in reader.samples::<f32>().enumerate() {
75 planes[i % channels].push(sample.map_err(bail)?);
76 }
77 }
78 hound::SampleFormat::Int => {
79 let full_scale = ((1i64 << (spec.bits_per_sample - 1)) - 1) as f32;
81 for (i, sample) in reader.samples::<i32>().enumerate() {
82 planes[i % channels].push(sample.map_err(bail)? as f32 / full_scale);
83 }
84 }
85 }
86 Ok((planes, spec.sample_rate))
87}
88
89fn decoded(path: &Path, extension: &str) -> Result<Planes, CliError> {
90 let file = std::fs::File::open(path).map_err(|e| missing(path, &e))?;
91 let refused = |why: String| CliError::usage(UNREADABLE, why);
92 let unread = |e: Error| refused(format!("could not decode `{}`: {e}", path.display()));
93 let mut hint = Hint::new();
94 hint.with_extension(extension);
95 let stream = MediaSourceStream::new(Box::new(file), Default::default());
96 let mut format = symphonia::default::get_probe()
97 .probe(
98 &hint,
99 stream,
100 FormatOptions::default(),
101 MetadataOptions::default(),
102 )
103 .map_err(unread)?;
104 let track = format
105 .default_track(TrackType::Audio)
106 .ok_or_else(|| refused(format!("`{}` holds no audio track", path.display())))?;
107 let id = track.id;
108 let params = track
109 .codec_params
110 .as_ref()
111 .and_then(|p| p.audio())
112 .ok_or_else(|| refused(format!("`{}` names no audio codec", path.display())))?;
113 let scale = match (extension, params.bits_per_sample) {
115 ("flac", Some(bits)) if (2..=32).contains(&bits) => {
116 let full = 2f64.powi(bits as i32 - 1);
117 (full / (full - 1.0)) as f32
118 }
119 _ => 1.0,
120 };
121 let mut decoder = symphonia::default::get_codecs()
122 .make_audio_decoder(params, &AudioDecoderOptions::default())
123 .map_err(unread)?;
124 let (mut planes, mut rate): (Vec<Vec<f32>>, Option<u32>) = (Vec::new(), None);
125 let mut block: Vec<Vec<f32>> = Vec::new();
126 while let Some(packet) = format.next_packet().map_err(unread)? {
127 if packet.track_id != id {
128 continue;
129 }
130 let audio = decoder.decode(&packet).map_err(unread)?;
131 rate.get_or_insert(audio.spec().rate());
132 audio.copy_to_vecs_planar(&mut block);
133 planes.resize_with(block.len(), Vec::new);
134 for (plane, part) in planes.iter_mut().zip(&block) {
135 plane.extend(part.iter().map(|v| v * scale));
136 }
137 }
138 let rate = rate.ok_or_else(|| refused(format!("`{}` holds no audio", path.display())))?;
139 if matches!(extension, "m4a" | "mp4") {
140 let bytes = std::fs::read(path).map_err(|e| missing(path, &e))?;
141 if let Some((skip, keep)) = edit(&bytes, rate) {
142 for plane in &mut planes {
143 plane.drain(..skip.min(plane.len()));
144 if let Some(keep) = keep {
145 plane.truncate(keep);
146 }
147 }
148 }
149 }
150 Ok((planes, rate))
151}
152
153fn boxes(data: &[u8], from: usize, to: usize) -> Vec<([u8; 4], usize, usize)> {
155 let mut out = Vec::new();
156 let mut at = from;
157 while at + 8 <= to {
158 let size = u32::from_be_bytes(data[at..at + 4].try_into().expect("4 bytes")) as usize;
159 let kind: [u8; 4] = data[at + 4..at + 8].try_into().expect("4 bytes");
160 let (body, end) = match size {
161 0 => (at + 8, to),
162 1 if at + 16 <= to => {
163 let wide = u64::from_be_bytes(data[at + 8..at + 16].try_into().expect("8 bytes"));
164 (at + 16, at.saturating_add(wide as usize))
165 }
166 n => (at + 8, at.saturating_add(n)),
167 };
168 if end > to || end < body {
169 break;
170 }
171 out.push((kind, body, end));
172 at = end;
173 }
174 out
175}
176
177fn word(data: &[u8], at: usize) -> Option<u64> {
178 Some(u64::from(u32::from_be_bytes(
179 data.get(at..at + 4)?.try_into().ok()?,
180 )))
181}
182
183fn wide(data: &[u8], at: usize) -> Option<u64> {
184 Some(u64::from_be_bytes(data.get(at..at + 8)?.try_into().ok()?))
185}
186
187fn timescale(data: &[u8], body: usize) -> Option<u64> {
189 word(data, body + if data.get(body)? == &1 { 20 } else { 12 })
190}
191
192fn edit(data: &[u8], rate: u32) -> Option<(usize, Option<usize>)> {
196 let find = |within: (usize, usize), kind: &[u8; 4]| {
197 boxes(data, within.0, within.1)
198 .into_iter()
199 .find(|(k, ..)| k == kind)
200 .map(|(_, b, e)| (b, e))
201 };
202 let moov = find((0, data.len()), b"moov")?;
203 let movie = timescale(data, find(moov, b"mvhd")?.0)?;
204 for (kind, body, end) in boxes(data, moov.0, moov.1) {
205 if &kind != b"trak" {
206 continue;
207 }
208 let mdia = find((body, end), b"mdia")?;
209 if data.get(find(mdia, b"hdlr")?.0 + 8..)?.get(..4)? != b"soun" {
210 continue;
211 }
212 let media = timescale(data, find(mdia, b"mdhd")?.0)?;
213 let elst = find(find((body, end), b"edts")?, b"elst")?.0;
214 let v1 = data.get(elst)? == &1;
215 let frames = |count: u64, scale: u64| {
216 (count as f64 * f64::from(rate) / scale as f64).round() as usize
217 };
218 for entry in 0..word(data, elst + 4)? as usize {
219 let at = elst + 8 + entry * if v1 { 20 } else { 12 };
220 let (duration, start) = match v1 {
221 true => (wide(data, at)?, wide(data, at + 8)? as i64),
222 false => (
223 word(data, at)?,
224 i64::from(word(data, at + 4)? as u32 as i32),
225 ),
226 };
227 if let Ok(start) = u64::try_from(start) {
228 let keep = (duration > 0).then(|| frames(duration, movie));
229 return Some((frames(start, media), keep));
230 }
231 }
232 return None;
233 }
234 None
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use crate::wav::{SampleEncoding, write_channels};
241
242 fn tmp(name: &str) -> std::path::PathBuf {
243 let path = std::env::temp_dir().join(format!(
244 "sva-cli-recording-{name}-{:x}.wav",
245 std::process::id()
246 ));
247 let _ = std::fs::remove_file(&path);
248 path
249 }
250
251 #[test]
253 fn reads_round_trips_a_float_wav_exactly_at_its_own_rate() {
254 let path = tmp("read-float-roundtrip");
255 let left = [0.5f32, -0.25, 0.75, -1.0];
256 let right = [-0.5f32, 0.25, -0.75, 1.0];
257 write_channels(&[&left, &right], 48_000, &path, SampleEncoding::Float).unwrap();
258
259 let (planes, sample_rate) = read_recording(&path).unwrap();
260 assert_eq!(sample_rate, 48_000, "no resampling, ever");
261 assert_eq!(planes, vec![left.to_vec(), right.to_vec()]);
262
263 let _ = std::fs::remove_file(&path);
264 }
265
266 #[test]
268 fn reads_unquantizes_pcm16_within_one_step() {
269 let path = tmp("read-pcm16-roundtrip");
270 let pcm = [0.5f32, -0.5, 0.1, -0.9];
271 write_channels(&[&pcm], 44_100, &path, SampleEncoding::Pcm(16)).unwrap();
272
273 let (planes, sample_rate) = read_recording(&path).unwrap();
274 let mono = &planes[0];
275 assert_eq!(sample_rate, 44_100);
276 assert_eq!(mono.len(), pcm.len());
277 for (got, want) in mono.iter().zip(&pcm) {
278 assert!(
279 (got - want).abs() < 1.0 / f32::from(i16::MAX),
280 "{got} vs {want}"
281 );
282 }
283
284 let _ = std::fs::remove_file(&path);
285 }
286
287 fn write_int(path: &std::path::Path, bits_per_sample: u16, values: &[i32], sample_rate: u32) {
288 let spec = hound::WavSpec {
289 channels: 1,
290 sample_rate,
291 bits_per_sample,
292 sample_format: hound::SampleFormat::Int,
293 };
294 let mut writer = hound::WavWriter::create(path, spec).unwrap();
295 for &v in values {
296 writer.write_sample(v).unwrap();
297 }
298 writer.finalize().unwrap();
299 }
300
301 #[test]
304 fn reads_unquantizes_every_int_bit_depth_hound_can_decode() {
305 let path = tmp("read-8bit");
306 write_int(&path, 8, &[127, -128, 0], 22_050);
307 let (planes, sample_rate) = read_recording(&path).unwrap();
308 assert_eq!(sample_rate, 22_050);
309 assert_eq!(planes.len(), 1);
310 assert!((planes[0][0] - 1.0).abs() < 0.01, "{:?}", planes[0]);
311 assert!((planes[0][1] + 1.0).abs() < 0.01, "{:?}", planes[0]);
312 assert_eq!(planes[0][2], 0.0);
313 let _ = std::fs::remove_file(&path);
314
315 let path = tmp("read-24bit");
316 let full_scale = (1i64 << 23) - 1;
317 write_int(
318 &path,
319 24,
320 &[full_scale as i32, -(full_scale as i32), 0],
321 96_000,
322 );
323 let (planes, sample_rate) = read_recording(&path).unwrap();
324 assert_eq!(sample_rate, 96_000);
325 assert_eq!(planes[0], vec![1.0, -1.0, 0.0]);
326 let _ = std::fs::remove_file(&path);
327 }
328
329 #[test]
332 fn reads_de_interleaves_every_component_into_its_own_plane() {
333 let path = tmp("read-three-channel");
334 let a = [0.1f32, 0.2];
335 let b = [0.3f32, 0.4];
336 let c = [0.5f32, 0.6];
337 write_channels(&[&a, &b, &c], 44_100, &path, SampleEncoding::Float).unwrap();
338
339 let (planes, _) = read_recording(&path).unwrap();
340 assert_eq!(planes, vec![a.to_vec(), b.to_vec(), c.to_vec()]);
341
342 let _ = std::fs::remove_file(&path);
343 }
344
345 #[test]
346 fn reads_reports_a_missing_file_as_not_found_not_a_panic() {
347 let path = tmp("read-missing");
348 assert!(matches!(read_recording(&path), Err(CliError::NotFound(_))));
349 }
350
351 fn boxed(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
353 let mut out = ((body.len() + 8) as u32).to_be_bytes().to_vec();
354 out.extend(kind);
355 out.extend(body);
356 out
357 }
358
359 fn movie(edits: &[(u32, i32)]) -> Vec<u8> {
362 let scaled = |scale: u32| [&[0u8; 12][..], &scale.to_be_bytes()].concat();
363 let mut elst = vec![0, 0, 0, 0];
364 elst.extend((edits.len() as u32).to_be_bytes());
365 for (duration, start) in edits {
366 elst.extend(duration.to_be_bytes());
367 elst.extend(start.to_be_bytes());
368 elst.extend([0, 1, 0, 0]);
369 }
370 let hdlr = [&[0u8; 8][..], b"soun"].concat();
371 let mdia = [boxed(b"mdhd", &scaled(48_000)), boxed(b"hdlr", &hdlr)].concat();
372 let trak = [
373 boxed(b"edts", &boxed(b"elst", &elst)),
374 boxed(b"mdia", &mdia),
375 ]
376 .concat();
377 boxed(
378 b"moov",
379 &[boxed(b"mvhd", &scaled(1000)), boxed(b"trak", &trak)].concat(),
380 )
381 }
382
383 #[test]
384 fn an_edit_list_skips_the_priming_and_keeps_the_duration_it_states() {
385 assert_eq!(
386 edit(&movie(&[(5000, 1024)]), 48_000),
387 Some((1024, Some(240_000)))
388 );
389 assert_eq!(
390 edit(&movie(&[(5000, 1024)]), 44_100),
391 Some((941, Some(220_500)))
392 );
393 }
394
395 #[test]
396 fn an_empty_edit_is_passed_over_and_an_unstated_duration_keeps_every_frame() {
397 assert_eq!(
398 edit(&movie(&[(20, -1), (5000, 2112)]), 48_000),
399 Some((2112, Some(240_000)))
400 );
401 assert_eq!(edit(&movie(&[(0, 1024)]), 48_000), Some((1024, None)));
402 assert_eq!(edit(&movie(&[(20, -1)]), 48_000), None);
403 }
404
405 #[test]
406 fn a_format_it_does_not_read_refuses_naming_every_one_it_does() {
407 let refused = read_recording(Path::new("take.opus")).unwrap_err();
408 assert!(
409 matches!(
410 refused,
411 CliError::Usage {
412 code: UNREADABLE,
413 ..
414 }
415 ),
416 "{refused:?}"
417 );
418 for extension in READS {
419 assert!(
420 refused.message().contains(&format!("`.{extension}`")),
421 "{refused:?}"
422 );
423 }
424 }
425}