mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-27 12:07:02 -06:00
- crates/media: probe (duration, rate/channels, size/fps; WMA/WMV recognised), audio decode to f32 PCM (MP3, AAC M4A/ADTS, ALAC, WAV, AIFF, CAF, FLAC, Vorbis via symphonia; Opus with our decoder in Ogg/WebM/MKV), video decode (MP4/MOV/WebM/MKV) with YUV->RGBA, poster frames, trim/fade/volume, Player mixer + per-voice clock (device-clocked or wall-clocked), VideoFeed. - Codec crates ported from FilmCraft: bitstream, h264, hevc, vp9, av1, isobmff, matroska, ogg, opus. - insert.audio/insert.video probe and store duration/size and a poster frame; unplayable media is embedded with a warning. New media.play/pause/toggle/stop/seek/info/posterFrame. - UI: MediaHost (cpal output injected by the desktop app), control bar under media, video frames on canvas and in the show; autoplay, click to play, loop, rewind, across slides, hide, full screen. - Tests with in-code WAV/AIFF and CC0 self-generated fixtures (make.sh, ATTRIBUTION.md rows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
327 lines
11 KiB
Rust
327 lines
11 KiB
Rust
//! Block inter prediction process (8.5.2.4): separable 8-tap sub-sample interpolation with edge
|
|
//! clamping, for unscaled (step 16) and scaled references.
|
|
|
|
use crate::frame::Frame;
|
|
use crate::tables::SUBPEL_FILTERS;
|
|
|
|
/// Filter taps of `filter` (0..3) at sub-sample position `frac` (0..15).
|
|
#[inline(always)]
|
|
fn taps(filter: u8, frac: usize) -> &'static [i16] {
|
|
let o = (filter as usize * 16 + frac) * 8;
|
|
&SUBPEL_FILTERS[o..o + 8]
|
|
}
|
|
|
|
/// Plane `plane` of a reference frame, valid for x <= last_x, y <= last_y. Rows are read through
|
|
/// [`Frame::row`] / [`Frame::span`], which wait until the reference has published them.
|
|
pub struct RefPlane<'a> {
|
|
pub frame: &'a Frame,
|
|
pub plane: usize,
|
|
pub last_x: i32,
|
|
pub last_y: i32,
|
|
}
|
|
|
|
impl RefPlane<'_> {
|
|
#[inline]
|
|
fn row(&self, y: usize) -> &[u16] {
|
|
self.frame.row(self.plane, y)
|
|
}
|
|
}
|
|
|
|
/// Predict a `w` x `h` block whose top-left sample is at (`x`, `y`) in 1/16 sample units of the
|
|
/// reference, stepping `step_x` / `step_y` (16 = unscaled). Output samples go to `out` (stride
|
|
/// `w`). `tmp` must hold at least `(h * step_y / 16 + 8) * w` entries, `win` at least
|
|
/// `(w + 7) * (h + 7)` (edge-clamped source window).
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn predict(
|
|
r: &RefPlane,
|
|
x: i32,
|
|
y: i32,
|
|
step_x: i32,
|
|
step_y: i32,
|
|
w: usize,
|
|
h: usize,
|
|
filter: u8,
|
|
bit_depth: u8,
|
|
out: &mut [u16],
|
|
tmp: &mut [u16],
|
|
win: &mut [u16],
|
|
) {
|
|
if step_x == 16 && step_y == 16 {
|
|
predict_unscaled(r, x, y, w, h, filter, bit_depth, out, tmp, win);
|
|
} else {
|
|
predict_scaled(r, x, y, step_x, step_y, w, h, filter, bit_depth, out, tmp);
|
|
}
|
|
}
|
|
|
|
/// Copy the (w + 7) x (h + 7) source window starting at (x0 - 3, y0 - 3) with edge clamping.
|
|
fn fetch_window(r: &RefPlane, x0: i32, y0: i32, w: usize, h: usize, win: &mut [u16]) -> usize {
|
|
let ws = w + 7;
|
|
for row in 0..h + 7 {
|
|
let yy = (y0 - 3 + row as i32).clamp(0, r.last_y) as usize;
|
|
let src = r.row(yy);
|
|
let dst = &mut win[row * ws..row * ws + ws];
|
|
let xs = x0 - 3;
|
|
if xs >= 0 && xs + ws as i32 - 1 <= r.last_x {
|
|
dst.copy_from_slice(&src[xs as usize..xs as usize + ws]);
|
|
} else {
|
|
for (c, d) in dst.iter_mut().enumerate() {
|
|
*d = src[(xs + c as i32).clamp(0, r.last_x) as usize];
|
|
}
|
|
}
|
|
}
|
|
ws
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn predict_unscaled(r: &RefPlane, x: i32, y: i32, w: usize, h: usize, filter: u8, bit_depth: u8, out: &mut [u16], tmp: &mut [u16], win: &mut [u16]) {
|
|
let (x0, y0) = (x >> 4, y >> 4);
|
|
let (fx, fy) = ((x & 15) as usize, (y & 15) as usize);
|
|
let max = (1i32 << bit_depth) - 1;
|
|
let inside = x0 - 3 >= 0 && y0 - 3 >= 0 && x0 + w as i32 + 4 <= r.last_x && y0 + h as i32 + 4 <= r.last_y;
|
|
// Source view with origin at (x0 - 3, y0 - 3): the reference band itself when the window
|
|
// lies inside the picture and inside one band.
|
|
let span = if inside { r.frame.span(r.plane, (y0 - 3) as usize, (y0 + h as i32 + 5) as usize) } else { None };
|
|
let (src, ss, so): (&[u16], usize, usize) = match span {
|
|
Some((data, o)) => (data, r.frame.strides[r.plane], o + (x0 - 3) as usize),
|
|
None => {
|
|
let ws = fetch_window(r, x0, y0, w, h, win);
|
|
(&win[..], ws, 0)
|
|
}
|
|
};
|
|
match (fx, fy) {
|
|
(0, 0) => {
|
|
for i in 0..h {
|
|
let s = so + (i + 3) * ss + 3;
|
|
out[i * w..i * w + w].copy_from_slice(&src[s..s + w]);
|
|
}
|
|
}
|
|
(_, 0) => {
|
|
let f = taps(filter, fx);
|
|
for i in 0..h {
|
|
filter_h(&src[so + (i + 3) * ss..], f, max, &mut out[i * w..i * w + w]);
|
|
}
|
|
}
|
|
(0, _) => {
|
|
let f = taps(filter, fy);
|
|
for i in 0..h {
|
|
filter_v(&src[so + i * ss + 3..], ss, f, max, &mut out[i * w..i * w + w]);
|
|
}
|
|
}
|
|
_ => {
|
|
let fh = taps(filter, fx);
|
|
let fv = taps(filter, fy);
|
|
for i in 0..h + 7 {
|
|
filter_h(&src[so + i * ss..], fh, max, &mut tmp[i * w..i * w + w]);
|
|
}
|
|
for i in 0..h {
|
|
filter_v(&tmp[i * w..], w, fv, max, &mut out[i * w..i * w + w]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Horizontal 8-tap filter of one row: `o[j]` from `s[j..j + 8]`. The taps are the outer loop
|
|
/// so that the row vectorizes (same sums, same order of the exact integer additions).
|
|
#[inline(always)]
|
|
fn filter_h(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) {
|
|
match o.len() {
|
|
4 => filter_h_n::<4>(s, f, max, o),
|
|
8 => filter_h_n::<8>(s, f, max, o),
|
|
16 => filter_h_n::<16>(s, f, max, o),
|
|
32 => filter_h_n::<32>(s, f, max, o),
|
|
64 => filter_h_n::<64>(s, f, max, o),
|
|
_ => {
|
|
for (j, d) in o.iter_mut().enumerate() {
|
|
let acc: i32 = (0..8).map(|t| f[t] as i32 * s[j + t] as i32).sum();
|
|
*d = ((acc + 64) >> 7).clamp(0, max) as u16;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn filter_h_n<const W: usize>(s: &[u16], f: &[i16], max: i32, o: &mut [u16]) {
|
|
let Some(o) = o.first_chunk_mut::<W>() else {
|
|
return;
|
|
};
|
|
if max == 255 {
|
|
let mut acc = [64u16; W];
|
|
for t in 0..8 {
|
|
let c = f[t] as u16;
|
|
let Some(p) = s.get(t..).and_then(|s| s.first_chunk::<W>()) else {
|
|
return;
|
|
};
|
|
for j in 0..W {
|
|
acc[j] = acc[j].wrapping_add(c.wrapping_mul(p[j]));
|
|
}
|
|
}
|
|
narrow8(&acc, o);
|
|
return;
|
|
}
|
|
let mut acc = [64i32; W];
|
|
for t in 0..8 {
|
|
let c = f[t] as i32;
|
|
let Some(p) = s.get(t..).and_then(|s| s.first_chunk::<W>()) else {
|
|
return;
|
|
};
|
|
for j in 0..W {
|
|
acc[j] += c * p[j] as i32;
|
|
}
|
|
}
|
|
for j in 0..W {
|
|
o[j] = (acc[j] >> 7).clamp(0, max) as u16;
|
|
}
|
|
}
|
|
|
|
/// Round2(sum, 7) clipped to 8 bits, from 8-bit filter sums kept modulo 2^16 (8 lanes per
|
|
/// vector instead of 4). With 8-bit samples every sum of the VP9 filters (whose positive taps
|
|
/// add up to at most 182 and negative taps to at most -54) plus the rounding term 64 lies in
|
|
/// [-13706, 46474]: wrapped, the non-negative ones stay below 0xC000 and the negative ones
|
|
/// (which clip to 0) land above it.
|
|
#[inline(always)]
|
|
fn narrow8<const W: usize>(acc: &[u16; W], o: &mut [u16; W]) {
|
|
for j in 0..W {
|
|
o[j] = if acc[j] < 0xC000 { (acc[j] >> 7).min(255) } else { 0 };
|
|
}
|
|
}
|
|
|
|
/// Vertical 8-tap filter of one row: `o[j]` from `s[j + t * ss]`, t = 0..8.
|
|
#[inline(always)]
|
|
fn filter_v(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) {
|
|
match o.len() {
|
|
4 => filter_v_n::<4>(s, ss, f, max, o),
|
|
8 => filter_v_n::<8>(s, ss, f, max, o),
|
|
16 => filter_v_n::<16>(s, ss, f, max, o),
|
|
32 => filter_v_n::<32>(s, ss, f, max, o),
|
|
64 => filter_v_n::<64>(s, ss, f, max, o),
|
|
_ => {
|
|
for (j, d) in o.iter_mut().enumerate() {
|
|
let acc: i32 = (0..8).map(|t| f[t] as i32 * s[j + t * ss] as i32).sum();
|
|
*d = ((acc + 64) >> 7).clamp(0, max) as u16;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[inline(always)]
|
|
fn filter_v_n<const W: usize>(s: &[u16], ss: usize, f: &[i16], max: i32, o: &mut [u16]) {
|
|
let Some(o) = o.first_chunk_mut::<W>() else {
|
|
return;
|
|
};
|
|
if max == 255 {
|
|
let mut acc = [64u16; W];
|
|
for t in 0..8 {
|
|
let c = f[t] as u16;
|
|
let Some(p) = s.get(t * ss..).and_then(|s| s.first_chunk::<W>()) else {
|
|
return;
|
|
};
|
|
for j in 0..W {
|
|
acc[j] = acc[j].wrapping_add(c.wrapping_mul(p[j]));
|
|
}
|
|
}
|
|
narrow8(&acc, o);
|
|
return;
|
|
}
|
|
let mut acc = [64i32; W];
|
|
for t in 0..8 {
|
|
let c = f[t] as i32;
|
|
let Some(p) = s.get(t * ss..).and_then(|s| s.first_chunk::<W>()) else {
|
|
return;
|
|
};
|
|
for j in 0..W {
|
|
acc[j] += c * p[j] as i32;
|
|
}
|
|
}
|
|
for j in 0..W {
|
|
o[j] = (acc[j] >> 7).clamp(0, max) as u16;
|
|
}
|
|
}
|
|
|
|
/// General process of 8.5.2.4 (any step).
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn predict_scaled(
|
|
r: &RefPlane,
|
|
x: i32,
|
|
y: i32,
|
|
step_x: i32,
|
|
step_y: i32,
|
|
w: usize,
|
|
h: usize,
|
|
filter: u8,
|
|
bit_depth: u8,
|
|
out: &mut [u16],
|
|
tmp: &mut [u16],
|
|
) {
|
|
let max = (1i32 << bit_depth) - 1;
|
|
let ih = ((((h as i32 - 1) * step_y + 15) >> 4) + 8) as usize;
|
|
for row in 0..ih {
|
|
let yy = ((y >> 4) + row as i32 - 3).clamp(0, r.last_y) as usize;
|
|
let src = r.row(yy);
|
|
for c in 0..w {
|
|
let p = x + step_x * c as i32;
|
|
let f = taps(filter, (p & 15) as usize);
|
|
let mut acc = 0i32;
|
|
for t in 0..8 {
|
|
acc += f[t] as i32 * src[((p >> 4) + t as i32 - 3).clamp(0, r.last_x) as usize] as i32;
|
|
}
|
|
tmp[row * w + c] = ((acc + 64) >> 7).clamp(0, max) as u16;
|
|
}
|
|
}
|
|
for rr in 0..h {
|
|
let p = (y & 15) + step_y * rr as i32;
|
|
let f = taps(filter, (p & 15) as usize);
|
|
let base = (p >> 4) as usize;
|
|
for c in 0..w {
|
|
let mut acc = 0i32;
|
|
for t in 0..8 {
|
|
acc += f[t] as i32 * tmp[(base + t) * w + c] as i32;
|
|
}
|
|
out[rr * w + c] = ((acc + 64) >> 7).clamp(0, max) as u16;
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn scaled_path_with_unit_step_matches_unscaled() {
|
|
let (w, h) = (40usize, 30usize);
|
|
let data: Vec<u16> = (0..w * h).map(|i| ((i * 37 + (i / w) * 11) % 256) as u16).collect();
|
|
let info = crate::frame::FrameInfo {
|
|
width: w as u32,
|
|
height: h as u32,
|
|
ss_x: true,
|
|
ss_y: true,
|
|
bit_depth: 8,
|
|
color_space: 0,
|
|
color_range: false,
|
|
render_width: w as u32,
|
|
render_height: h as u32,
|
|
key: true,
|
|
intra_only: false,
|
|
};
|
|
// Planes are allocated in whole superblocks: copy the test picture into a 64x64 plane.
|
|
let mut full = vec![0u16; 64 * 64];
|
|
for y in 0..h {
|
|
full[y * 64..y * 64 + w].copy_from_slice(&data[y * w..y * w + w]);
|
|
}
|
|
let chroma = vec![0u16; 32 * 32];
|
|
let frame = Frame::from_planes(info, [&full, &chroma, &chroma]);
|
|
let r = RefPlane { frame: &frame, plane: 0, last_x: w as i32 - 1, last_y: h as i32 - 1 };
|
|
let mut tmp = vec![0u16; 80 * 80];
|
|
for filter in 0..4u8 {
|
|
for &(x, y) in &[(0, 0), (5, 3), (16 * 7 + 5, 16 * 9 + 11), (-40, -3), (16 * 36 + 1, 16 * 25 + 15), (16 * 10, 16 * 10 + 8)] {
|
|
for &(bw, bh) in &[(4usize, 4usize), (8, 8), (16, 8)] {
|
|
let mut a = vec![0u16; bw * bh];
|
|
let mut b = vec![0u16; bw * bh];
|
|
predict_unscaled(&r, x, y, bw, bh, filter, 8, &mut a, &mut tmp, &mut [0u16; 71 * 71]);
|
|
predict_scaled(&r, x, y, 16, 16, bw, bh, filter, 8, &mut b, &mut tmp);
|
|
assert_eq!(a, b, "filter {filter} pos ({x},{y}) {bw}x{bh}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|