//! Synthetic sources for the conformance corpus, generated rather than committed. //! //! Each fixture is a Y4M stream (`C420jpeg`, progressive, 25 frames per second) whose //! every byte is an integer function of the frame index and the pixel position, so an //! independent implementation regenerates it byte for byte; the corpus pins each //! fixture's SHA-256. A fixture is a list of spans; a span shows one shot, or fades from //! one shot to another, for a number of frames. Shots read the global frame index `t`, //! so a shot that resumes after an interruption continues its motion. //! //! - `ramp`: `Y = lo + v·(hi − lo) / (period − 1)`, `v = (x·dx + y·dy + t·speed) mod //! period`; with `jitter` 1, `Y` moves by `noise mod 3 − 1`, clamped to 16..=235. //! - `blocks`: `Y = levels[((x + t·speed) / bw + y / bh) mod 3]`. //! - `noise`: `Y = lo + noise mod (hi − lo + 1)`. //! - A fade of `n` frames from shot `a` to shot `b`, at its `i`-th frame (from 0), is //! `(a·(n − i − 1) + b·(i + 1)) / n` per sample, chroma included. //! - A span marked *local* reads `t` as the frame's index within the span, so the span //! is the same frames wherever it appears: a bumper played twice. //! //! `noise(seed, x, y, t) = mix(seed ^ mix(x ^ mix(y ^ mix(t))))`, where `mix` is the //! 32-bit finaliser `v ^= v >> 16; v *= 0x7feb352d; v ^= v >> 15; v *= 0x846ca68b; //! v ^= v >> 16` (wrapping). Chroma is constant per shot. /// The fixtures this module generates. pub const NAMES: &[&str] = &["cuts-01", "kinds-01", "video-base-01", "video-staged-01"]; /// The fixtures the scene-cut, encode and step-choice corpora cover. pub const SCENECUT_NAMES: &[&str] = &["cuts-01", "kinds-01"]; fn mix(mut v: u32) -> u32 { v ^= v >> 16; v = v.wrapping_mul(0x7feb_352d); v ^= v >> 15; v = v.wrapping_mul(0x846c_a68b); v ^= v >> 16; v } /// The fixtures' noise. pub fn noise(seed: u32, x: u32, y: u32, t: u32) -> u32 { mix(seed ^ mix(x ^ mix(y ^ mix(t)))) } #[derive(Debug, Clone, Copy)] enum Shot { Ramp { lo: u32, hi: u32, dx: u32, dy: u32, speed: u32, period: u32, jitter: bool, seed: u32, cb: u8, cr: u8 }, Blocks { levels: [u8; 3], bw: u32, bh: u32, speed: u32, cb: u8, cr: u8 }, Noise { lo: u32, hi: u32, seed: u32, cb: u8, cr: u8 }, } impl Shot { fn y(&self, x: u32, y: u32, t: u32) -> u32 { match *self { Shot::Ramp { lo, hi, dx, dy, speed, period, jitter, seed, .. } => { let v = (x * dx + y * dy + t * speed) % period; let base = lo + v * (hi - lo) / (period - 1); if jitter { (base + noise(seed, x, y, t) % 3).saturating_sub(1).clamp(16, 235) } else { base } } Shot::Blocks { levels, bw, bh, speed, .. } => levels[(((x + t * speed) / bw + y / bh) % 3) as usize] as u32, Shot::Noise { lo, hi, seed, .. } => lo + noise(seed, x, y, t) % (hi - lo + 1), } } fn chroma(&self) -> (u32, u32) { match *self { Shot::Ramp { cb, cr, .. } | Shot::Blocks { cb, cr, .. } | Shot::Noise { cb, cr, .. } => (cb as u32, cr as u32), } } } struct Span { frames: u32, shot: Shot, fade_to: Option, local: bool, } #[allow(clippy::too_many_arguments)] const fn ramp(lo: u32, hi: u32, dx: u32, dy: u32, speed: u32, jitter: bool, seed: u32, cb: u8, cr: u8) -> Shot { Shot::Ramp { lo, hi, dx, dy, speed, period: 32, jitter, seed, cb, cr } } fn spans(name: &str) -> Option<(u32, u32, Vec)> { let s = |frames, shot| Span { frames, shot, fade_to: None, local: false }; let local = |frames, shot| Span { frames, shot, fade_to: None, local: true }; Some(match name { // Hard cuts; a flash of exactly 6 frames (the default lookahead) inside shot B; // a 40-frame fade from C to D; and two shots cut against each other, E and F. "cuts-01" => { let a = ramp(20, 100, 1, 1, 1, true, 1, 110, 140); let b = ramp(120, 220, 2, 1, 1, true, 2, 150, 120); let flash = Shot::Noise { lo: 228, hi: 235, seed: 3, cb: 128, cr: 128 }; let c = ramp(30, 90, 1, 2, 1, true, 4, 120, 120); let d = ramp(150, 230, 1, 0, 2, true, 5, 136, 136); let e = ramp(40, 120, 0, 1, 1, true, 6, 100, 150); let f = ramp(140, 200, 1, 0, 1, true, 7, 160, 110); (32, 18, vec![ s(60, a), s(30, b), s(6, flash), s(44, b), s(20, c), Span { frames: 40, shot: c, fade_to: Some(d), local: false }, s(30, d), s(30, e), s(30, f), s(30, e), s(30, f), ]) } // One shot of each kind, cut against each other, then a static shot longer than // the longest piece. "kinds-01" => (32, 18, vec![ s(40, ramp(20, 200, 1, 1, 0, false, 0, 128, 128)), s(40, Shot::Blocks { levels: [40, 120, 200], bw: 8, bh: 6, speed: 1, cb: 128, cr: 128 }), s(40, Shot::Noise { lo: 16, hi: 235, seed: 7, cb: 128, cr: 128 }), s(40, ramp(16, 80, 1, 1, 1, true, 8, 128, 128)), s(130, ramp(60, 180, 2, 1, 0, false, 0, 140, 116)), ]), // The base-only golden: three shots with a one-second bumper between them, the // same frames both times, so its pieces are one object. "video-base-01" => { let bumper = Shot::Blocks { levels: [40, 120, 200], bw: 8, bh: 6, speed: 1, cb: 100, cr: 160 }; (64, 36, vec![ s(60, ramp(20, 200, 1, 1, 1, true, 11, 110, 140)), local(25, bumper), s(50, ramp(30, 220, 2, 1, 1, true, 12, 150, 120)), local(25, bumper), s(40, ramp(16, 160, 1, 2, 1, true, 13, 128, 128)), ]) } // The staged golden: a moving shot, the bumper, a still shot (no motion, no // noise), the bumper again (the same frames both times), and a second moving // shot. "video-staged-01" => { let bumper = Shot::Blocks { levels: [40, 120, 200], bw: 8, bh: 6, speed: 1, cb: 100, cr: 160 }; (64, 36, vec![ s(50, ramp(20, 200, 1, 1, 1, true, 21, 110, 140)), local(25, bumper), s(40, ramp(60, 180, 2, 1, 0, false, 0, 140, 116)), local(25, bumper), s(60, ramp(30, 120, 1, 2, 1, true, 22, 150, 120)), ]) } _ => return None, }) } /// The fixture `name` as Y4M bytes, or `None` for a name [`NAMES`] does not hold. pub fn generate(name: &str) -> Option> { let (w, h, spans) = spans(name)?; let (cw, ch) = (w.div_ceil(2), h.div_ceil(2)); let mut out = format!("YUV4MPEG2 W{w} H{h} F25:1 Ip A1:1 C420jpeg\n").into_bytes(); let mut t = 0u32; for span in &spans { for i in 0..span.frames { out.extend_from_slice(b"FRAME\n"); let blend = |a: u32, b: u32| (a * (span.frames - i - 1) + b * (i + 1)) / span.frames; let st = if span.local { i } else { t }; for y in 0..h { for x in 0..w { let v = span.shot.y(x, y, st); let v = span.fade_to.map_or(v, |to| blend(v, to.y(x, y, st))); out.push(v as u8); } } let (cb, cr) = span.shot.chroma(); let (cb, cr) = span.fade_to.map_or((cb, cr), |to| { let (tb, tr) = to.chroma(); (blend(cb, tb), blend(cr, tr)) }); out.extend(std::iter::repeat_n(cb as u8, (cw * ch) as usize)); out.extend(std::iter::repeat_n(cr as u8, (cw * ch) as usize)); t += 1; } } Some(out) } #[cfg(test)] mod tests { use super::*; /// Known answers computed independently (Python, arbitrary-precision integers /// masked to 32 bits). #[test] fn mix_and_noise_known_answers() { assert_eq!(mix(0), 0); assert_eq!(mix(1), 0x6889_90c0); assert_eq!(mix(0xdead_beef), 0xe628_c683); assert_eq!(noise(3, 5, 7, 11), 3_929_262_924); } #[test] fn every_fixture_is_a_y4m_stream() { for n in NAMES { let b = generate(n).unwrap(); let (hd, frames) = crate::source::y4m::read_all(&b).unwrap(); let dims = if n.starts_with("video-") { (64, 36) } else { (32, 18) }; assert_eq!((hd.w, hd.h, hd.rate), (dims.0, dims.1, Some((25, 1))), "{n}"); assert!(frames.len() >= 200, "{n}"); } // The bumper is the same frames both times. let (_, f) = crate::source::y4m::read_all(&generate("video-base-01").unwrap()).unwrap(); assert_eq!(f[60..85], f[135..160]); assert_ne!(f[60..85], f[85..110]); let (_, f) = crate::source::y4m::read_all(&generate("video-staged-01").unwrap()).unwrap(); assert_eq!(f[50..75], f[115..140]); assert!(f[75..115].iter().all(|x| *x == f[75]), "the still shot does not change"); assert!(generate("nope").is_none()); } }