//! Decoding and probing every supported format. //! //! WAV and AIFF are written here; the compressed fixtures in `tests/fixtures` are a 0.5 s, 440 Hz //! sine (amplitude 1/8) and a 6-frame 64×48 colour gradient we synthesised and encoded with //! `fixtures/make.sh` (CC0, see ATTRIBUTION.md). use std::sync::Arc; use std::time::Duration; use deckcraft_media::{Bytes, Container, MediaError, audio, probe, video}; fn fixture(name: &str) -> Bytes { let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures").join(name); Arc::new(std::fs::read(&p).unwrap_or_else(|e| panic!("{}: {e}", p.display()))) } fn sine(rate: u32, secs: f64) -> Vec { (0..(rate as f64 * secs) as usize).map(|i| (i as f64 * 440.0 * std::f64::consts::TAU / rate as f64).sin() as f32 * 0.125).collect() } fn wav(rate: u32, channels: u16, samples: &[f32]) -> Vec { let data: Vec = samples.iter().flat_map(|s| ((s * 32767.0) as i16).to_le_bytes()).collect(); let mut b = Vec::new(); b.extend_from_slice(b"RIFF"); b.extend_from_slice(&(36 + data.len() as u32).to_le_bytes()); b.extend_from_slice(b"WAVEfmt "); b.extend_from_slice(&16u32.to_le_bytes()); b.extend_from_slice(&1u16.to_le_bytes()); b.extend_from_slice(&channels.to_le_bytes()); b.extend_from_slice(&rate.to_le_bytes()); b.extend_from_slice(&(rate * channels as u32 * 2).to_le_bytes()); b.extend_from_slice(&(channels * 2).to_le_bytes()); b.extend_from_slice(&16u16.to_le_bytes()); b.extend_from_slice(b"data"); b.extend_from_slice(&(data.len() as u32).to_le_bytes()); b.extend_from_slice(&data); b } /// 80-bit IEEE extended sample rate for AIFF's COMM chunk. fn ext80(v: u32) -> [u8; 10] { let mut out = [0u8; 10]; let e = 31 - v.leading_zeros(); let exp = (16383 + e) as u16; out[..2].copy_from_slice(&exp.to_be_bytes()); let mant = (v as u64) << (63 - e); out[2..].copy_from_slice(&mant.to_be_bytes()); out } fn aiff(rate: u32, samples: &[f32]) -> Vec { let data: Vec = samples.iter().flat_map(|s| ((s * 32767.0) as i16).to_be_bytes()).collect(); let mut comm = Vec::new(); comm.extend_from_slice(&1u16.to_be_bytes()); comm.extend_from_slice(&(samples.len() as u32).to_be_bytes()); comm.extend_from_slice(&16u16.to_be_bytes()); comm.extend_from_slice(&ext80(rate)); let mut b = Vec::new(); b.extend_from_slice(b"FORM"); b.extend_from_slice(&((4 + 8 + comm.len() + 8 + 8 + data.len()) as u32).to_be_bytes()); b.extend_from_slice(b"AIFF"); b.extend_from_slice(b"COMM"); b.extend_from_slice(&(comm.len() as u32).to_be_bytes()); b.extend_from_slice(&comm); b.extend_from_slice(b"SSND"); b.extend_from_slice(&((8 + data.len()) as u32).to_be_bytes()); b.extend_from_slice(&[0; 8]); b.extend_from_slice(&data); b } /// Frequency of channel 0 from its zero crossings (with hysteresis, so codec noise around silence /// doesn't count), over the span between the first and last crossing. fn frequency(p: &audio::Pcm) -> f64 { let mut sign = 0i8; let mut crossings = vec![]; for i in 0..p.frames() { let s = p.at(i, 0); let now = if s > 0.03 { 1 } else if s < -0.03 { -1 } else { 0 }; if now != 0 && now != sign { if sign != 0 { crossings.push(i); } sign = now; } } match (crossings.first(), crossings.last()) { (Some(a), Some(b)) if b > a => (crossings.len() - 1) as f64 / 2.0 / ((b - a) as f64 / p.rate as f64), _ => 0.0, } } /// The tone decodes to ~0.5 s of a 440 Hz sine at amplitude 1/8. fn check_tone(name: &str, bytes: &Bytes, rate: u32, channels: u16) { let p = audio::decode(bytes).unwrap_or_else(|e| panic!("{name}: {e}")); assert_eq!((p.rate, p.channels), (rate, channels), "{name}"); assert!((p.duration() - 0.5).abs() < 0.06, "{name}: {} s", p.duration()); let f = frequency(&p); assert!((f - 440.0).abs() < 15.0, "{name}: {f} Hz"); let rms = p.rms(); // 1/8 / √2 (ffmpeg's mono → stereo upmix for the Vorbis fixture lowers it by another √2). assert!((0.055..0.105).contains(&rms), "{name}: rms {rms}"); let info = probe(bytes).unwrap_or_else(|e| panic!("{name}: probe {e}")); let a = info.audio.unwrap_or_else(|| panic!("{name}: no audio info")); assert!(a.decodable, "{name}"); assert_eq!((a.sample_rate, a.channels), (rate, channels), "{name}"); assert!((info.duration_ms as i64 - 500).abs() <= 60, "{name}: {} ms", info.duration_ms); assert!(info.video.is_none(), "{name}"); } #[test] fn wav_written_in_code() { let b: Bytes = Arc::new(wav(44_100, 1, &sine(44_100, 0.5))); assert_eq!(Container::sniff(&b), Container::Wav); check_tone("wav", &b, 44_100, 1); // Stereo. let st: Vec = sine(22_050, 0.5).iter().flat_map(|s| [*s, *s]).collect(); check_tone("wav stereo", &Arc::new(wav(22_050, 2, &st)), 22_050, 2); } #[test] fn aiff_written_in_code() { let b: Bytes = Arc::new(aiff(48_000, &sine(48_000, 0.5))); assert_eq!(Container::sniff(&b), Container::Aiff); check_tone("aiff", &b, 48_000, 1); } #[test] fn mp3() { let b = fixture("tone.mp3"); assert_eq!(Container::sniff(&b), Container::Mp3); check_tone("mp3", &b, 44_100, 1); } #[test] fn aac_m4a_and_adts() { check_tone("m4a", &fixture("tone.m4a"), 44_100, 1); let adts = fixture("tone.aac"); assert_eq!(Container::sniff(&adts), Container::Adts); check_tone("aac", &adts, 44_100, 1); } #[test] fn alac() { check_tone("alac", &fixture("tone-alac.m4a"), 44_100, 1); } #[test] fn flac() { check_tone("flac", &fixture("tone.flac"), 44_100, 1); } #[test] fn ogg_vorbis() { check_tone("vorbis", &fixture("tone.ogg"), 44_100, 2); } #[test] fn ogg_opus() { check_tone("opus", &fixture("tone.opus"), 48_000, 1); } #[test] fn wma_is_probed_but_not_decoded() { let b = fixture("tone.wma"); assert_eq!(Container::sniff(&b), Container::Asf); let info = probe(&b).expect("probe"); let a = info.audio.clone().expect("audio"); assert_eq!((a.codec.as_str(), a.sample_rate, a.channels, a.decodable), ("WMA 2", 44_100, 1, false)); assert!((info.duration_ms as i64 - 500).abs() <= 120, "{}", info.duration_ms); assert!(!info.playable()); assert!(matches!(audio::decode(&b), Err(MediaError::Unsupported(_)))); } #[test] fn garbage_is_an_error_not_a_panic() { let b: Bytes = Arc::new((0..4000u32).map(|i| (i * 7919 % 251) as u8).collect()); assert!(probe(&b).is_err()); assert!(audio::decode(&b).is_err()); assert!(video::VideoDecoder::open(b.clone()).is_err()); // Truncated real files too. for name in ["tone.mp3", "tone.m4a", "tone.opus", "clip-h264.mp4", "clip-vp9.webm"] { let full = fixture(name); let cut: Bytes = Arc::new(full[..full.len() / 3].to_vec()); let _ = probe(&cut); let _ = audio::decode(&cut); if let Ok(mut d) = video::VideoDecoder::open(cut) { while let Ok(Some(_)) = d.next_frame() {} } } } /// Mean colour of a 16×16 block in the middle of the frame. fn centre(f: &video::Frame) -> [f64; 3] { let (w, h) = (f.width as usize, f.height as usize); let mut s = [0.0; 3]; let mut n = 0.0; for y in h / 2 - 8..h / 2 + 8 { for x in w / 2 - 8..w / 2 + 8 { for (c, v) in s.iter_mut().enumerate() { *v += f.rgba[(y * w + x) * 4 + c] as f64; } n += 1.0; } } s.map(|v| v / n) } /// Our gradient's mean over the same block in frame `k`: R = (4x + 20k) mod 256, G = 5y mod 256, /// B = 40k mod 256. fn expected_centre(w: usize, h: usize, k: usize) -> [f64; 3] { let mut s = [0.0; 3]; let mut n = 0.0; for y in h / 2 - 8..h / 2 + 8 { for x in w / 2 - 8..w / 2 + 8 { s[0] += ((x * 4 + k * 20) % 256) as f64; s[1] += ((y * 5) % 256) as f64; s[2] += ((k * 40) % 256) as f64; n += 1.0; } } s.map(|v| v / n) } fn check_video(name: &str, w: u32, h: u32, frames: usize, codec: &str) { let b = fixture(name); let info = probe(&b).unwrap_or_else(|e| panic!("{name}: {e}")); let v = info.video.clone().unwrap_or_else(|| panic!("{name}: no video info")); assert_eq!((v.codec.as_str(), v.width, v.height, v.decodable), (codec, w, h, true), "{name}"); assert!((v.fps - 10.0).abs() < 0.6, "{name}: {} fps", v.fps); assert!((info.duration_ms as i64 - frames as i64 * 100).abs() <= 120, "{name}: {} ms", info.duration_ms); let mut d = video::VideoDecoder::open(b.clone()).unwrap_or_else(|e| panic!("{name}: {e}")); let mut got = vec![]; while let Some(f) = d.next_frame().unwrap_or_else(|e| panic!("{name}: {e}")) { assert_eq!((f.width, f.height, f.rgba.len()), (w, h, (w * h * 4) as usize), "{name}"); got.push(f); } assert_eq!(got.len(), frames, "{name}"); for (k, f) in got.iter().enumerate() { assert!((f.time - k as f64 * 0.1).abs() < 0.02, "{name}: frame {k} at {}", f.time); let (c, e) = (centre(f), expected_centre(w as usize, h as usize, k)); for i in 0..3 { assert!((c[i] - e[i]).abs() < 24.0, "{name}: frame {k} channel {i}: {c:?} vs {e:?}"); } } // Seeking lands on the frame shown at that time. d.seek(0.35); let f = d.next_frame().ok().flatten().unwrap_or_else(|| panic!("{name}: no frame after seek")); assert!((f.time - 0.3).abs() < 0.02, "{name}: seek gave {}", f.time); // Poster frames. let p = video::poster_frame(&b, 0.25).unwrap_or_else(|e| panic!("{name}: {e}")); assert!((p.time - 0.2).abs() < 0.02, "{name}: poster at {}", p.time); let png = video::poster_png(&b, 0.0).unwrap_or_else(|e| panic!("{name}: {e}")); assert!(png.starts_with(&[0x89, b'P', b'N', b'G'])); } #[test] fn h264_mp4_with_b_frames_and_aac_audio() { check_video("clip-h264.mp4", 64, 48, 5, "H.264"); // Its audio track decodes too. let b = fixture("clip-h264.mp4"); let info = probe(&b).expect("probe"); assert_eq!(info.audio.as_ref().map(|a| (a.codec.as_str(), a.decodable)), Some(("AAC", true))); let p = audio::decode(&b).expect("audio of the video"); assert!((frequency(&p) - 440.0).abs() < 15.0); } #[test] fn h264_mov() { check_video("clip-h264.mov", 64, 48, 6, "H.264"); assert_eq!(probe(&fixture("clip-h264.mov")).map(|i| i.container), Ok("mov")); } #[test] fn hevc_mp4() { check_video("clip-hevc.mp4", 64, 48, 6, "HEVC"); } #[test] fn vp9_webm_with_opus_audio() { check_video("clip-vp9.webm", 64, 48, 5, "VP9"); let b = fixture("clip-vp9.webm"); assert_eq!(probe(&b).ok().and_then(|i| i.audio).map(|a| (a.codec, a.sample_rate)), Some(("Opus".to_string(), 48_000))); let p = audio::decode(&b).expect("opus in webm"); assert_eq!(p.rate, 48_000); assert!((frequency(&p) - 440.0).abs() < 15.0); } #[test] fn av1_mkv() { check_video("clip-av1.mkv", 64, 64, 6, "AV1"); } #[test] fn video_feed_follows_the_clock() { let mut feed = deckcraft_media::VideoFeed::new(fixture("clip-h264.mov")).expect("feed"); assert_eq!(feed.size(), (64, 48)); let t = Duration::from_secs(5); assert!(feed.frame_at_blocking(0.0, t).is_some_and(|f| f.time == 0.0)); let f = feed.frame_at_blocking(0.25, t).map(|f| f.time); assert!(f.is_some_and(|v| (v - 0.2).abs() < 0.02), "{f:?}"); // Jumping back (a loop) seeks. let f = feed.frame_at_blocking(0.05, t).map(|f| f.time); assert_eq!(f, Some(0.0)); let f = feed.frame_at_blocking(0.55, t).map(|f| f.time); assert!(f.is_some_and(|v| (v - 0.5).abs() < 0.02), "{f:?}"); } #[test] fn player_plays_decoded_audio_through_the_mixer() { use deckcraft_media::{ClipParams, Player, VoiceSpec}; let b: Bytes = Arc::new(wav(48_000, 1, &sine(48_000, 0.5))); let mut p = Player::new(None); p.play(1, 1, &b, VoiceSpec { clip: ClipParams { volume: 0.5, ..Default::default() }, looping: false, duration: 0.5, audio: true }, None); assert!(p.wait_loaded(1, Duration::from_secs(5))); let mut buf = vec![0.0f32; 4800 * 2]; p.render(&mut buf, 2, 48_000); let rms = (buf.iter().map(|s| s * s).sum::() / buf.len() as f32).sqrt(); assert!((rms - 0.0884 * 0.5).abs() < 0.005, "{rms}"); assert!((p.status(1).map(|s| s.position).unwrap_or(0.0) - 0.1).abs() < 1e-6); }