M9.5: filmcraft-prores decoder + encoder
Clean-room ProRes (SMPTE RDD 36) codec: 422 Proxy/LT/Standard/HQ and 4444/4444 XQ decode incl. alpha and interlaced fields, slice-parallel; progressive encoder with frame-level quantiser rate control targeting Apple nominal rates. Oracle tests vs ffmpeg (within 1 LSB on all 29 fixtures), round-trip, fuzz and ignored perf tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
691864e9e7
commit
eae1c8ad92
|
|
@ -1236,6 +1236,15 @@ dependencies = [
|
|||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-prores"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"rayon",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-render"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ filmcraft-geom = { path = "crates/geom" }
|
|||
filmcraft-color = { path = "crates/color" }
|
||||
filmcraft-bitstream = { path = "crates/bitstream" }
|
||||
filmcraft-isobmff = { path = "crates/isobmff" }
|
||||
filmcraft-prores = { path = "crates/prores" }
|
||||
filmcraft-frame = { path = "crates/frame" }
|
||||
filmcraft-media = { path = "crates/media" }
|
||||
filmcraft-project = { path = "crates/project" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "filmcraft-prores"
|
||||
description = "Clean-room Apple ProRes (SMPTE RDD 36) decoder and encoder"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["threads"]
|
||||
threads = ["dep:rayon"]
|
||||
|
||||
[dependencies]
|
||||
filmcraft-bitstream = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
rayon = { workspace = true, optional = true }
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
# filmcraft-prores
|
||||
|
||||
Clean-room Apple ProRes decoder and encoder in pure Rust, written from the public SMPTE RDD 36
|
||||
bitstream description. Layer L0: depends only on `filmcraft-bitstream`, `thiserror` and (optional,
|
||||
feature `threads`, default on) `rayon`. Builds for `wasm32-unknown-unknown`; no `unsafe`.
|
||||
|
||||
## API
|
||||
|
||||
```rust
|
||||
use filmcraft_prores::*;
|
||||
|
||||
// Decode one QuickTime sample (one ProRes frame).
|
||||
let frame: Frame = decode_frame(&sample)?; // 10-bit 4:2:2 / 12-bit 4:4:4
|
||||
let f10 = decode_frame_with(&sample, &DecodeOptions { bit_depth: Some(10), threads: true })?;
|
||||
decode_frame_into(&sample, &DecodeOptions::default(), &mut frame_buf)?; // reuses planes
|
||||
let hdr: FrameHeader = probe(&sample)?; // header only
|
||||
|
||||
// Encode (progressive).
|
||||
let mut enc = Encoder::new(Profile::Hq, 1920, 1080); // or Encoder::with_config(EncoderConfig { .. })
|
||||
let sample: Vec<u8> = enc.encode(&frame)?; // the QuickTime sample for fourcc Profile::fourcc()
|
||||
```
|
||||
|
||||
`Frame { width, height, chroma: ChromaFormat::{Yuv422, Yuv444}, bit_depth, y, cb, cr: Vec<u16>,
|
||||
alpha: Option<Vec<u16>>, interlace: Interlace, color: ColorInfo { primaries, transfer, matrix },
|
||||
aspect_ratio, frame_rate_code }` holds planar samples with no row padding (`chroma_width()` is
|
||||
`ceil(width/2)` for 4:2:2). The colour codes are H.273 code points straight from the frame header.
|
||||
|
||||
`Profile` = `Proxy` (`apco`), `Lt` (`apcs`), `Standard` (`apcn`), `Hq` (`apch`), `P4444` (`ap4h`),
|
||||
`P4444Xq` (`ap4x`). The frame itself doesn't say which profile it is (only the container fourcc
|
||||
does), so the decoder doesn't need to know.
|
||||
|
||||
## Decoder
|
||||
|
||||
- Frame header (dimensions, chroma format, interlace mode, aspect/frame-rate codes, colour
|
||||
description, alpha type, optional luma/chroma quantisation matrices), picture header(s), slice
|
||||
table, slices.
|
||||
- Entropy decoding: RDD 36's adaptive Golomb-Rice/Exp-Golomb codebooks. DC is coded as the first
|
||||
value plus sign-predicted differences. AC is run/level coded, interleaved across all blocks of a
|
||||
slice component.
|
||||
- Progressive and interlaced scan orders. Interlaced frames are two field pictures (top or bottom
|
||||
field first) of `ceil(height/32)` macroblock rows each.
|
||||
- Dequantisation `F = level × qmat[i] × qscale(qidx)` (qscale = qidx up to 128, then
|
||||
`128 + 4·(qidx−128)`).
|
||||
- IDCT: the exact 2-D orthonormal inverse DCT, evaluated separably in `f32`. It uses even/odd
|
||||
symmetry, skips zero rows and fills DC-only blocks directly. Its error is below 2⁻⁸ in the
|
||||
12-bit domain. The IDCT output `f` is the 12-bit-domain sample minus 2048. The output at depth
|
||||
`d` is `floor(f·2^(d−12) + 2^(d−1) + ½)`, clipped to `[4, 2^d − 5]` for d ≥ 10 (the four
|
||||
lowest/highest codes are SDI reserved values; this matches common decoders at 10 and 12 bits)
|
||||
and to `[1, 2^d − 2]` below 10 bits.
|
||||
- 4:2:2 chroma blocks per macroblock are top/bottom. 4:4:4 chroma blocks run TL, BL, TR, BR,
|
||||
while luma runs TL, TR, BL, BR.
|
||||
- Alpha (8- or 16-bit coding): lossless difference/run coding in raster order over each slice's
|
||||
16 lines. The starting value is all-ones. Output is scaled to the output depth: 8-bit coding is
|
||||
bit-replicated, 16-bit coding is truncated.
|
||||
- Default output depth: 10 bits for 4:2:2 and 12 bits for 4:4:4. Any depth from 8 to 16 is
|
||||
available on request.
|
||||
- Slice-parallel: each macroblock row, covering both fields of interlaced frames, is decoded by a
|
||||
rayon task directly into the output planes.
|
||||
- Robustness: every size field is validated before allocation, codewords longer than 56 bits are
|
||||
rejected, and arithmetic is overflow-safe. Mutated streams return `Err`, never panic (see
|
||||
`tests/fuzz.rs`).
|
||||
|
||||
## Encoder
|
||||
|
||||
- Progressive ProRes 422 (Proxy/LT/Standard/HQ) and 4444/4444 XQ. For 4444 the frame's alpha
|
||||
plane is coded with 16-bit alpha coding by default (`EncoderConfig::encode_alpha`).
|
||||
- Input: planar `u16` at any depth from 8 to 16 (normally 10). The chroma format must match the
|
||||
profile. Edge macroblocks are padded by replicating the edge.
|
||||
- Flat quantisation matrices (all 4, not transmitted), 8 macroblocks per slice by default
|
||||
(`log2_slice_mbs`), encoder id `fmcr`.
|
||||
- Rate control targets Apple's published nominal rates (Mbit/s at 1920×1080, 29.97 fps: 45, 102,
|
||||
147, 220, 330, 500). These are converted to bits per macroblock (184, 417, 601, 900, 1349,
|
||||
2045), so frame targets scale with frame area (`Encoder::target_frame_bytes`). Alpha comes on
|
||||
top of the target. Every slice is transformed once. Then a frame-level binary search finds the
|
||||
smallest uniform quantiser index whose frame fits the budget, and as many slices as the rest of
|
||||
the budget allows get one finer step. Detailed content lands within 0.1% of the target. Simple
|
||||
content uses qidx 1 and comes in under the target. Every slice is kept within the 16-bit
|
||||
size fields.
|
||||
|
||||
## Accuracy (vs ffmpeg as an external oracle)
|
||||
|
||||
`tests/oracle_decode.rs` decodes 29 fixtures made by ffmpeg's `prores_ks` and `prores_aw`
|
||||
encoders. They cover every profile, testsrc2/mandelbrot/SMPTE bars at 1920×1080, 1918×1080,
|
||||
1280×720, 640×360, 352×288, 70×38 and 333×201, interlaced 720×486 TFF/BFF and 1080i, 2-MB slices,
|
||||
and 4444 with 8- and 16-bit alpha. The results are compared with ffmpeg's decode at the native
|
||||
depth (10-bit 4:2:2, 12-bit 4:4:4). **Every fixture is within ±1 LSB.** Mean absolute error is
|
||||
0.0001–0.03 LSB for 422 and up to 0.15 LSB for 12-bit XQ, and 97–99.99% of samples are
|
||||
bit-identical (86% for 12-bit XQ mandelbrot). Alpha is always bit-exact. The remaining
|
||||
differences are the reference decoder's integer-IDCT rounding. This decoder is closer to the
|
||||
exact IDCT and does not reproduce that approximation.
|
||||
|
||||
`tests/oracle_encode.rs` encodes 1920×1080 frames with every profile (testsrc2, mandelbrot and
|
||||
noisy content; 4444 with an alpha disc) and writes them into a minimal test-only MOV. ffmpeg then
|
||||
decodes them with `-xerror`, with no errors, and agrees with this decoder within ±1. HQ PSNR is
|
||||
69.9/69.2 dB (Y/C) on mandelbrot at 100% of the target rate. Busy content lands at 99.9–100.4%
|
||||
of the target.
|
||||
|
||||
## Performance
|
||||
|
||||
Measured with `cargo test --release -p filmcraft-prores --test perf -- --ignored --nocapture` on
|
||||
an M4 Pro (14 cores) that was heavily loaded by other jobs, so these are lower bounds. A 1080p HQ
|
||||
frame at the full nominal rate (918 KB) decoded at up to 318 fps threaded and 53 fps on one
|
||||
thread. A prores_ks HQ mandelbrot frame (657 KB) decoded at 390 fps threaded. Encoding a 1080p
|
||||
HQ frame takes about 40 ms threaded.
|
||||
|
||||
## Gaps
|
||||
|
||||
- The encoder is progressive-only (the decoder handles interlaced), uses flat quantisation
|
||||
matrices (no perceptual matrices), and has no trellis/RDO.
|
||||
- Output is not bit-exact with ffmpeg's integer IDCT; it stays within ±1 LSB of it (see above).
|
||||
- The 4:2:2 decoder output is fixed at the coded 10-bit precision unless another depth is
|
||||
requested; there is no direct output of packed formats (v210, etc.).
|
||||
- No container handling: frames are QuickTime samples, and demuxing/muxing lives in
|
||||
`filmcraft-isobmff`.
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
//! Fast MSB-first bit reader/writer for slice payloads (the hot path of the codec).
|
||||
//!
|
||||
//! Frame and picture headers use `filmcraft_bitstream`; the entropy-coded slice data uses these
|
||||
//! specialised helpers, which read 64-bit windows and decode a whole codeword per call.
|
||||
|
||||
use crate::tables::Codebook;
|
||||
|
||||
/// Bit reader over one slice component with a 64-bit MSB-aligned cache. Reads past the end
|
||||
/// yield zero bits; callers check [`Reader::overrun`] once a component has been consumed.
|
||||
pub(crate) struct Reader<'a> {
|
||||
data: &'a [u8],
|
||||
/// Next byte of `data` to load into the cache (may run past the end: zeros are loaded).
|
||||
next: usize,
|
||||
/// Cached bits, MSB first; bits below `avail` are zero.
|
||||
cache: u64,
|
||||
avail: u32,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
#[inline]
|
||||
pub fn new(data: &'a [u8]) -> Self {
|
||||
let mut r = Self { data, next: 0, cache: 0, avail: 0 };
|
||||
r.refill();
|
||||
r
|
||||
}
|
||||
|
||||
/// Ensure at least 56 valid bits are cached.
|
||||
#[inline(always)]
|
||||
fn refill(&mut self) {
|
||||
if self.avail >= 56 {
|
||||
return;
|
||||
}
|
||||
if let Some(s) = self.data.get(self.next..self.next + 8) {
|
||||
let w = u64::from_be_bytes([s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]]);
|
||||
self.cache |= w >> self.avail;
|
||||
self.next += ((63 - self.avail) >> 3) as usize;
|
||||
self.avail |= 56;
|
||||
} else {
|
||||
while self.avail <= 56 {
|
||||
let b = self.data.get(self.next).copied().unwrap_or(0) as u64;
|
||||
self.cache |= b << (56 - self.avail);
|
||||
self.avail += 8;
|
||||
self.next += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn consume(&mut self, n: u32) {
|
||||
// n <= 56 <= avail
|
||||
self.cache = if n >= 64 { 0 } else { self.cache << n };
|
||||
self.avail -= n;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn consumed(&self) -> usize {
|
||||
self.next * 8 - self.avail as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn bits_left(&self) -> isize {
|
||||
(self.data.len() * 8) as isize - self.consumed() as isize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn overrun(&self) -> bool {
|
||||
self.consumed() > self.data.len() * 8
|
||||
}
|
||||
|
||||
/// True when every remaining bit is zero (the component's padding), or nothing is left.
|
||||
#[inline(always)]
|
||||
pub fn only_padding_left(&mut self) -> bool {
|
||||
let left = self.bits_left();
|
||||
if left > 56 {
|
||||
return false;
|
||||
}
|
||||
if left <= 0 {
|
||||
return true;
|
||||
}
|
||||
self.refill();
|
||||
self.cache >> (64 - left as u32) == 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn read(&mut self, n: u32) -> u32 {
|
||||
debug_assert!(n <= 32);
|
||||
if n == 0 {
|
||||
return 0;
|
||||
}
|
||||
self.refill();
|
||||
let v = (self.cache >> (64 - n)) as u32;
|
||||
self.consume(n);
|
||||
v
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read_bit(&mut self) -> bool {
|
||||
if self.avail == 0 {
|
||||
self.refill();
|
||||
}
|
||||
let b = self.cache >> 63 == 1;
|
||||
self.consume(1);
|
||||
b
|
||||
}
|
||||
|
||||
/// Decode one codeword; `None` for codewords longer than 56 bits (never produced by valid
|
||||
/// streams).
|
||||
#[inline(always)]
|
||||
pub fn read_cw(&mut self, cb: Codebook) -> Option<u32> {
|
||||
self.refill();
|
||||
let w = self.cache;
|
||||
let q = w.leading_zeros();
|
||||
let rq = cb.rice_q_max as u32;
|
||||
if q <= rq {
|
||||
let rice = cb.rice as u32;
|
||||
let len = q + 1 + rice;
|
||||
let low = if rice == 0 { 0 } else { ((w << (q + 1)) >> (64 - rice)) as u32 };
|
||||
self.consume(len);
|
||||
Some((q << rice) | low)
|
||||
} else {
|
||||
let exp = cb.exp as u32;
|
||||
let len = 2 * q - rq + exp;
|
||||
if len > 56 {
|
||||
return None;
|
||||
}
|
||||
let code = w >> (64 - len);
|
||||
self.consume(len);
|
||||
let v = code - (1u64 << exp) + (((rq + 1) as u64) << cb.rice);
|
||||
u32::try_from(v).ok()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of bits of the codeword for `val` in codebook `cb`.
|
||||
#[inline]
|
||||
pub(crate) fn cw_len(cb: Codebook, val: u32) -> u32 {
|
||||
let rq = cb.rice_q_max as u32;
|
||||
let rice = cb.rice as u32;
|
||||
let rice_span = (rq + 1) << rice;
|
||||
if val < rice_span {
|
||||
(val >> rice) + 1 + rice
|
||||
} else {
|
||||
let exp = cb.exp as u32;
|
||||
let v = (val - rice_span) as u64 + (1u64 << exp);
|
||||
let nbits = 64 - v.leading_zeros(); // n + exp + 1
|
||||
let n = nbits - exp - 1;
|
||||
(rq + 1 + n) + nbits
|
||||
}
|
||||
}
|
||||
|
||||
/// A bit sink: either a real writer or a bit counter (for rate control trials).
|
||||
pub(crate) trait Sink {
|
||||
fn put(&mut self, val: u32, bits: u32);
|
||||
fn put_cw(&mut self, cb: Codebook, val: u32);
|
||||
}
|
||||
|
||||
/// Counts bits without storing them.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Counter(pub u64);
|
||||
|
||||
impl Sink for Counter {
|
||||
#[inline]
|
||||
fn put(&mut self, _val: u32, bits: u32) {
|
||||
self.0 += bits as u64;
|
||||
}
|
||||
#[inline]
|
||||
fn put_cw(&mut self, cb: Codebook, val: u32) {
|
||||
self.0 += cw_len(cb, val) as u64;
|
||||
}
|
||||
}
|
||||
|
||||
/// MSB-first bit writer.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Writer {
|
||||
pub buf: Vec<u8>,
|
||||
acc: u64,
|
||||
n: u32,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
/// Pad with zero bits to a byte boundary and return the bytes.
|
||||
pub fn finish(mut self) -> Vec<u8> {
|
||||
if self.n > 0 {
|
||||
let pad = 8 - self.n;
|
||||
self.put(0, pad);
|
||||
}
|
||||
self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl Sink for Writer {
|
||||
#[inline]
|
||||
fn put(&mut self, val: u32, bits: u32) {
|
||||
debug_assert!(bits <= 32);
|
||||
if bits == 0 {
|
||||
return;
|
||||
}
|
||||
let v = (val as u64) & ((1u64 << bits) - 1);
|
||||
self.acc = (self.acc << bits) | v;
|
||||
self.n += bits;
|
||||
while self.n >= 8 {
|
||||
self.n -= 8;
|
||||
self.buf.push((self.acc >> self.n) as u8);
|
||||
}
|
||||
self.acc &= (1u64 << self.n) - 1;
|
||||
}
|
||||
|
||||
fn put_cw(&mut self, cb: Codebook, val: u32) {
|
||||
let rq = cb.rice_q_max as u32;
|
||||
let rice = cb.rice as u32;
|
||||
let rice_span = (rq + 1) << rice;
|
||||
if val < rice_span {
|
||||
let q = val >> rice;
|
||||
// q zeros, a one, then `rice` low bits
|
||||
self.put(0, q);
|
||||
self.put(1, 1);
|
||||
self.put(val & ((1 << rice) - 1), rice);
|
||||
} else {
|
||||
let exp = cb.exp as u32;
|
||||
let v = (val - rice_span) as u64 + (1u64 << exp);
|
||||
let nbits = 64 - v.leading_zeros();
|
||||
let n = nbits - exp - 1;
|
||||
let zeros = rq + 1 + n;
|
||||
self.put(0, zeros.min(32));
|
||||
if zeros > 32 {
|
||||
self.put(0, zeros - 32);
|
||||
}
|
||||
if nbits > 32 {
|
||||
self.put((v >> 32) as u32, nbits - 32);
|
||||
self.put(v as u32, 32);
|
||||
} else {
|
||||
self.put(v as u32, nbits);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tables::*;
|
||||
|
||||
#[test]
|
||||
fn codeword_roundtrip_all_codebooks() {
|
||||
let mut books = vec![FIRST_DC_CB];
|
||||
books.extend_from_slice(&DC_CB);
|
||||
books.extend_from_slice(&RUN_CB);
|
||||
books.extend_from_slice(&LEVEL_CB);
|
||||
let vals: Vec<u32> = (0..300).chain([1000, 4095, 65535, 1 << 20]).collect();
|
||||
for cb in books {
|
||||
let mut w = Writer::new();
|
||||
let mut total = 0;
|
||||
for &v in &vals {
|
||||
w.put_cw(cb, v);
|
||||
total += cw_len(cb, v);
|
||||
}
|
||||
let bits = w.buf.len() as u32 * 8 + w.n;
|
||||
assert_eq!(bits, total);
|
||||
let data = w.finish();
|
||||
let mut r = Reader::new(&data);
|
||||
for &v in &vals {
|
||||
assert_eq!(r.read_cw(cb), Some(v), "{cb:?}");
|
||||
}
|
||||
assert!(!r.overrun());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exp_golomb5_equivalence() {
|
||||
// FIRST_DC_CB is Exp-Golomb order 5: value 32 is `01` + 6 bits (000000)
|
||||
assert_eq!(cw_len(FIRST_DC_CB, 0), 6);
|
||||
assert_eq!(cw_len(FIRST_DC_CB, 31), 6);
|
||||
assert_eq!(cw_len(FIRST_DC_CB, 32), 8);
|
||||
assert_eq!(cw_len(FIRST_DC_CB, 95), 8);
|
||||
assert_eq!(cw_len(FIRST_DC_CB, 96), 10);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
//! 8×8 orthonormal DCT-II / DCT-III (IDCT) in single precision.
|
||||
//!
|
||||
//! The reconstruction IDCT is defined as the exact real-valued 2-D inverse DCT
|
||||
//! `f(x,y) = Σ_u Σ_v C(u)C(v)/4 · F(v,u) · cos((2x+1)uπ/16) · cos((2y+1)vπ/16)`
|
||||
//! evaluated separably (rows then columns) in `f32`. For ProRes coefficient ranges the `f32`
|
||||
//! evaluation error is below 2⁻⁸ in the 12-bit sample domain, so the rounded output equals the
|
||||
//! rounded exact IDCT except when the exact value lies within that distance of a rounding tie.
|
||||
|
||||
use crate::tables::BASIS;
|
||||
|
||||
/// In-place inverse DCT. `block` holds dequantised coefficients in raster order
|
||||
/// (`block[v*8 + u]`, `v` = vertical frequency) and receives samples (`block[y*8 + x]`).
|
||||
///
|
||||
/// Each 1-D pass uses the even/odd symmetry of the basis (`B[u][7-x] = ±B[u][x]` for even/odd
|
||||
/// `u`) and skips all-zero rows; DC-only blocks are filled directly.
|
||||
#[inline]
|
||||
pub(crate) fn idct8x8(block: &mut [f32; 64]) {
|
||||
let mut t = [[0f32; 8]; 8];
|
||||
let mut nz = [false; 8];
|
||||
let mut dc_only = true;
|
||||
// Horizontal pass: t[r][x] = Σ_u F[r][u] · B[u][x]
|
||||
for r in 0..8 {
|
||||
let c: &[f32; 8] = block[r * 8..r * 8 + 8].try_into().unwrap();
|
||||
if c[1..].iter().all(|&v| v == 0.0) {
|
||||
if c[0] != 0.0 {
|
||||
t[r] = [c[0] * BASIS[0][0]; 8];
|
||||
nz[r] = true;
|
||||
dc_only &= r == 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
dc_only = false;
|
||||
nz[r] = true;
|
||||
let mut out = [0f32; 8];
|
||||
for x in 0..4 {
|
||||
let e = c[0] * BASIS[0][x] + c[2] * BASIS[2][x] + c[4] * BASIS[4][x] + c[6] * BASIS[6][x];
|
||||
let o = c[1] * BASIS[1][x] + c[3] * BASIS[3][x] + c[5] * BASIS[5][x] + c[7] * BASIS[7][x];
|
||||
out[x] = e + o;
|
||||
out[7 - x] = e - o;
|
||||
}
|
||||
t[r] = out;
|
||||
}
|
||||
if dc_only {
|
||||
*block = [t[0][0] * BASIS[0][0]; 64];
|
||||
return;
|
||||
}
|
||||
// Vertical pass: f[y][x] = Σ_v t[v][x] · B[v][y]
|
||||
for y in 0..4 {
|
||||
let mut e = [0f32; 8];
|
||||
let mut o = [0f32; 8];
|
||||
for v in [0, 2, 4, 6] {
|
||||
if nz[v] {
|
||||
let b = BASIS[v][y];
|
||||
for x in 0..8 {
|
||||
e[x] += t[v][x] * b;
|
||||
}
|
||||
}
|
||||
}
|
||||
for v in [1, 3, 5, 7] {
|
||||
if nz[v] {
|
||||
let b = BASIS[v][y];
|
||||
for x in 0..8 {
|
||||
o[x] += t[v][x] * b;
|
||||
}
|
||||
}
|
||||
}
|
||||
for x in 0..8 {
|
||||
block[y * 8 + x] = e[x] + o[x];
|
||||
block[(7 - y) * 8 + x] = e[x] - o[x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In-place forward DCT (samples in, coefficients out; same layout as [`idct8x8`]).
|
||||
pub(crate) fn fdct8x8(block: &mut [f32; 64]) {
|
||||
let mut t = [[0f32; 8]; 8];
|
||||
// Horizontal: t[y][u] = Σ_x f[y][x] · B[u][x]
|
||||
for y in 0..8 {
|
||||
for u in 0..8 {
|
||||
let mut s = 0f32;
|
||||
for x in 0..8 {
|
||||
s += block[y * 8 + x] * BASIS[u][x];
|
||||
}
|
||||
t[y][u] = s;
|
||||
}
|
||||
}
|
||||
// Vertical: F[v][u] = Σ_y t[y][u] · B[v][y]
|
||||
for v in 0..8 {
|
||||
let mut acc = [0f32; 8];
|
||||
for y in 0..8 {
|
||||
let b = BASIS[v][y];
|
||||
for u in 0..8 {
|
||||
acc[u] += t[y][u] * b;
|
||||
}
|
||||
}
|
||||
block[v * 8..v * 8 + 8].copy_from_slice(&acc);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn reference_idct(c: &[f32; 64]) -> [f64; 64] {
|
||||
let mut out = [0f64; 64];
|
||||
for y in 0..8 {
|
||||
for x in 0..8 {
|
||||
let mut s = 0f64;
|
||||
for v in 0..8 {
|
||||
for u in 0..8 {
|
||||
let cu = if u == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 };
|
||||
let cv = if v == 0 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 };
|
||||
s += cu * cv / 4.0
|
||||
* c[v * 8 + u] as f64
|
||||
* (((2 * x + 1) * u) as f64 * std::f64::consts::PI / 16.0).cos()
|
||||
* (((2 * y + 1) * v) as f64 * std::f64::consts::PI / 16.0).cos();
|
||||
}
|
||||
}
|
||||
out[y * 8 + x] = s;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idct_matches_double_reference() {
|
||||
let mut seed = 12345u32;
|
||||
let mut rnd = || {
|
||||
seed = seed.wrapping_mul(1664525).wrapping_add(1013904223);
|
||||
seed >> 8
|
||||
};
|
||||
for _ in 0..200 {
|
||||
let mut c = [0f32; 64];
|
||||
for v in c.iter_mut() {
|
||||
if rnd() % 3 == 0 {
|
||||
*v = ((rnd() % 8192) as i32 - 4096) as f32;
|
||||
}
|
||||
}
|
||||
let r = reference_idct(&c);
|
||||
let mut b = c;
|
||||
idct8x8(&mut b);
|
||||
for i in 0..64 {
|
||||
assert!((b[i] as f64 - r[i]).abs() < 4e-3, "{} vs {}", b[i], r[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fdct_inverts_idct() {
|
||||
let mut b = [0f32; 64];
|
||||
for (i, v) in b.iter_mut().enumerate() {
|
||||
*v = ((i * 37) % 200) as f32 - 100.0;
|
||||
}
|
||||
let orig = b;
|
||||
fdct8x8(&mut b);
|
||||
idct8x8(&mut b);
|
||||
for i in 0..64 {
|
||||
assert!((b[i] - orig[i]).abs() < 1e-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,498 @@
|
|||
//! ProRes frame decoding.
|
||||
|
||||
use crate::bits::Reader;
|
||||
use crate::dct::idct8x8;
|
||||
use crate::header::{FrameHeader, PictureHeader, row_slice_widths};
|
||||
use crate::tables::*;
|
||||
use crate::{AlphaType, ChromaFormat, Error, Frame, Interlace, Result};
|
||||
|
||||
/// Decoder options.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DecodeOptions {
|
||||
/// Output bit depth (8..=16). `None`: 10 for 4:2:2, 12 for 4:4:4 (the precision ProRes
|
||||
/// codes each format at).
|
||||
pub bit_depth: Option<u8>,
|
||||
/// Decode slices in parallel (requires the `threads` feature).
|
||||
pub threads: bool,
|
||||
}
|
||||
|
||||
impl Default for DecodeOptions {
|
||||
fn default() -> Self {
|
||||
DecodeOptions { bit_depth: None, threads: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse only the frame header (cheap; for probing geometry and format).
|
||||
pub fn probe(data: &[u8]) -> Result<FrameHeader> {
|
||||
FrameHeader::parse(data).map(|(h, _, _)| h)
|
||||
}
|
||||
|
||||
/// Decode one ProRes frame (the contents of one QuickTime sample) with default options.
|
||||
pub fn decode_frame(data: &[u8]) -> Result<Frame> {
|
||||
decode_frame_with(data, &DecodeOptions::default())
|
||||
}
|
||||
|
||||
/// Maps IDCT output (12-bit-domain signed samples) to the output bit depth.
|
||||
#[derive(Clone, Copy)]
|
||||
struct OutConv {
|
||||
scale: f32,
|
||||
bias: f32,
|
||||
min: i32,
|
||||
max: i32,
|
||||
}
|
||||
|
||||
impl OutConv {
|
||||
fn new(depth: u8) -> OutConv {
|
||||
// The IDCT produces `f`, where the 12-bit sample is `f + 2048`.
|
||||
let scale = (2f32).powi(depth as i32 - 12);
|
||||
// Samples are clipped away from the extremes: the four lowest and highest codes are
|
||||
// excluded at 10 bits and above (0..=3 and 1020..=1023 are the SDI timing-reference codes
|
||||
// at 10 bits; the same margin is kept at 12 bits, matching common decoders), and the
|
||||
// single lowest/highest code at 8 and 9 bits.
|
||||
let reserved = if depth >= 10 { 4 } else { 1 };
|
||||
OutConv { scale, bias: (1u32 << (depth - 1)) as f32 + 0.5, min: reserved, max: (1i32 << depth) - 1 - reserved }
|
||||
}
|
||||
#[inline(always)]
|
||||
fn conv(&self, v: f32) -> u16 {
|
||||
// Truncation equals floor here: negative values clamp to `min` (> 0) either way.
|
||||
((v * self.scale + self.bias) as i32).clamp(self.min, self.max) as u16
|
||||
}
|
||||
}
|
||||
|
||||
struct Ctx {
|
||||
width: usize,
|
||||
cwidth: usize,
|
||||
chroma444: bool,
|
||||
qmat_luma: [u8; 64],
|
||||
qmat_chroma: [u8; 64],
|
||||
scan: &'static [u8; 64],
|
||||
alpha: AlphaType,
|
||||
conv: OutConv,
|
||||
depth: u8,
|
||||
}
|
||||
|
||||
/// Output rows belonging to one macroblock row of every coded picture.
|
||||
struct Band<'a> {
|
||||
y: &'a mut [u16],
|
||||
cb: &'a mut [u16],
|
||||
cr: &'a mut [u16],
|
||||
a: Option<&'a mut [u16]>,
|
||||
rows: usize,
|
||||
}
|
||||
|
||||
struct Picture {
|
||||
/// Line parity within the frame (0 = even lines) for field pictures.
|
||||
parity: usize,
|
||||
/// Byte ranges of each slice, row-major.
|
||||
slices: Vec<(usize, usize)>,
|
||||
}
|
||||
|
||||
struct Scratch {
|
||||
coeffs: Vec<[f32; 64]>,
|
||||
alpha: Vec<u16>,
|
||||
}
|
||||
|
||||
impl Scratch {
|
||||
fn new() -> Scratch {
|
||||
Scratch { coeffs: vec![[0f32; 64]; 32], alpha: vec![0; 16 * 128] }
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode one ProRes frame.
|
||||
pub fn decode_frame_with(data: &[u8], opts: &DecodeOptions) -> Result<Frame> {
|
||||
let mut f = Frame::new(0, 0, ChromaFormat::Yuv422, 10, false);
|
||||
decode_frame_into(data, opts, &mut f)?;
|
||||
Ok(f)
|
||||
}
|
||||
|
||||
/// Decode one ProRes frame into `out`, reusing its plane allocations (the fastest way to decode
|
||||
/// a sequence). On error the contents of `out` are unspecified.
|
||||
pub fn decode_frame_into(data: &[u8], opts: &DecodeOptions, out: &mut Frame) -> Result<()> {
|
||||
let (hdr, mut off, frame_size) = FrameHeader::parse(data)?;
|
||||
let data = &data[..frame_size];
|
||||
let depth = opts.bit_depth.unwrap_or(match hdr.chroma {
|
||||
ChromaFormat::Yuv422 => 10,
|
||||
ChromaFormat::Yuv444 => 12,
|
||||
});
|
||||
if !(8..=16).contains(&depth) {
|
||||
return Err(Error::Unsupported("output bit depth"));
|
||||
}
|
||||
let width = hdr.width as usize;
|
||||
let height = hdr.height as usize;
|
||||
let cwidth = hdr.chroma.chroma_width(hdr.width as u32) as usize;
|
||||
let interlaced = hdr.interlace != Interlace::Progressive;
|
||||
let mb_width = width.div_ceil(16);
|
||||
let mb_height = if interlaced { height.div_ceil(32) } else { height.div_ceil(16) };
|
||||
|
||||
let npics = if interlaced { 2 } else { 1 };
|
||||
let mut pics = Vec::with_capacity(npics);
|
||||
for p in 0..npics {
|
||||
let ph = PictureHeader::parse(data.get(off..).ok_or(Error::Truncated)?)?;
|
||||
let pic_end = off.checked_add(ph.picture_size).ok_or(Error::Truncated)?;
|
||||
if pic_end > data.len() || ph.picture_size < ph.header_size {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
let widths = row_slice_widths(mb_width, ph.log2_slice_mbs);
|
||||
let nslices = widths.len() * mb_height;
|
||||
if ph.num_slices != 0 && ph.num_slices != nslices {
|
||||
return Err(Error::Invalid("slice count does not match frame geometry"));
|
||||
}
|
||||
let table = off + ph.header_size;
|
||||
let mut pos = table + 2 * nslices;
|
||||
if pos > pic_end {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
let mut slices = Vec::with_capacity(nslices);
|
||||
for i in 0..nslices {
|
||||
let sz = u16::from_be_bytes([data[table + 2 * i], data[table + 2 * i + 1]]) as usize;
|
||||
// Every slice carries at least its 6-byte header.
|
||||
if sz < 6 || pos + sz > pic_end {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
slices.push((pos, sz));
|
||||
pos += sz;
|
||||
}
|
||||
let parity = match (hdr.interlace, p) {
|
||||
(Interlace::BottomFieldFirst, 0) | (Interlace::TopFieldFirst, 1) => 1,
|
||||
_ => 0,
|
||||
};
|
||||
pics.push((Picture { parity, slices }, widths));
|
||||
off = pic_end;
|
||||
}
|
||||
|
||||
let ctx = Ctx {
|
||||
width,
|
||||
cwidth,
|
||||
chroma444: hdr.chroma == ChromaFormat::Yuv444,
|
||||
qmat_luma: hdr.qmat_luma,
|
||||
qmat_chroma: hdr.qmat_chroma,
|
||||
scan: if interlaced { &SCAN_INTERLACED } else { &SCAN_PROGRESSIVE },
|
||||
alpha: hdr.alpha,
|
||||
conv: OutConv::new(depth),
|
||||
depth,
|
||||
};
|
||||
|
||||
let take = |v: &mut Vec<u16>, n: usize| {
|
||||
let mut v = std::mem::take(v);
|
||||
v.resize(n, 0);
|
||||
v
|
||||
};
|
||||
let mut y = take(&mut out.y, width * height);
|
||||
let mut cb = take(&mut out.cb, cwidth * height);
|
||||
let mut cr = take(&mut out.cr, cwidth * height);
|
||||
let mut alpha = match (hdr.alpha, out.alpha.take()) {
|
||||
(AlphaType::None, _) => None,
|
||||
(_, a) => Some(take(&mut a.unwrap_or_default(), width * height)),
|
||||
};
|
||||
|
||||
let band_h = if interlaced { 32 } else { 16 };
|
||||
let mut bands: Vec<Band> = Vec::with_capacity(mb_height);
|
||||
{
|
||||
let mut yc = y.chunks_mut(band_h * width);
|
||||
let mut cbc = cb.chunks_mut(band_h * cwidth);
|
||||
let mut crc = cr.chunks_mut(band_h * cwidth);
|
||||
let mut ac = alpha.as_mut().map(|a| a.chunks_mut(band_h * width));
|
||||
while let (Some(y), Some(cb), Some(cr)) = (yc.next(), cbc.next(), crc.next()) {
|
||||
let rows = y.len() / width;
|
||||
let a = ac.as_mut().and_then(|it| it.next());
|
||||
bands.push(Band { y, cb, cr, a, rows });
|
||||
}
|
||||
}
|
||||
|
||||
let step = if interlaced { 2 } else { 1 };
|
||||
let run_band = |(row, band): (usize, &mut Band), scratch: &mut Scratch| -> Result<()> {
|
||||
for (pi, (pic, widths)) in pics.iter().enumerate() {
|
||||
let mut mb_x = 0;
|
||||
for (k, &w) in widths.iter().enumerate() {
|
||||
let si = row * widths.len() + k;
|
||||
let (start, len) = pic.slices[si];
|
||||
decode_slice(&ctx, &data[start..start + len], mb_x, w, band, step, pic.parity, scratch).map_err(|_| Error::Slice { picture: pi, slice: si })?;
|
||||
mb_x += w;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
#[cfg(feature = "threads")]
|
||||
if opts.threads {
|
||||
use rayon::prelude::*;
|
||||
bands.par_iter_mut().enumerate().try_for_each_init(Scratch::new, |s, b| run_band(b, s))?;
|
||||
} else {
|
||||
let mut s = Scratch::new();
|
||||
bands.iter_mut().enumerate().try_for_each(|b| run_band(b, &mut s))?;
|
||||
}
|
||||
#[cfg(not(feature = "threads"))]
|
||||
{
|
||||
let mut s = Scratch::new();
|
||||
bands.iter_mut().enumerate().try_for_each(|b| run_band(b, &mut s))?;
|
||||
}
|
||||
drop(bands);
|
||||
|
||||
*out = Frame {
|
||||
width: hdr.width as u32,
|
||||
height: hdr.height as u32,
|
||||
chroma: hdr.chroma,
|
||||
bit_depth: depth,
|
||||
y,
|
||||
cb,
|
||||
cr,
|
||||
alpha,
|
||||
interlace: hdr.interlace,
|
||||
color: hdr.color,
|
||||
aspect_ratio: hdr.aspect_ratio,
|
||||
frame_rate_code: hdr.frame_rate_code,
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn be16(d: &[u8], i: usize) -> usize {
|
||||
u16::from_be_bytes([d[i], d[i + 1]]) as usize
|
||||
}
|
||||
|
||||
/// Dequantisation factors (matrix × scale) for a slice.
|
||||
fn qfactors(qmat: &[u8; 64], qs: u32) -> [f32; 64] {
|
||||
let mut q = [0f32; 64];
|
||||
for i in 0..64 {
|
||||
q[i] = (qmat[i] as u32 * qs) as f32;
|
||||
}
|
||||
q
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn decode_slice(ctx: &Ctx, d: &[u8], mb_x: usize, mbs: usize, band: &mut Band, step: usize, parity: usize, s: &mut Scratch) -> std::result::Result<(), ()> {
|
||||
if d.len() < 6 {
|
||||
return Err(());
|
||||
}
|
||||
let hsize = (d[0] >> 3) as usize;
|
||||
let has_alpha = ctx.alpha != AlphaType::None;
|
||||
if hsize < if has_alpha { 8 } else { 6 } || hsize > d.len() {
|
||||
return Err(());
|
||||
}
|
||||
let qs = qscale(d[1]);
|
||||
let ysize = be16(d, 2);
|
||||
let cbsize = be16(d, 4);
|
||||
let body = d.len() - hsize;
|
||||
let crsize = if has_alpha { be16(d, 6) } else { body.checked_sub(ysize + cbsize).ok_or(())? };
|
||||
if ysize + cbsize + crsize > body {
|
||||
return Err(());
|
||||
}
|
||||
let mut p = hsize;
|
||||
let ydata = &d[p..p + ysize];
|
||||
p += ysize;
|
||||
let cbdata = &d[p..p + cbsize];
|
||||
p += cbsize;
|
||||
let crdata = &d[p..p + crsize];
|
||||
p += crsize;
|
||||
let adata = &d[p..];
|
||||
|
||||
let log2_mbs = mbs.trailing_zeros();
|
||||
let x0 = mb_x * 16;
|
||||
|
||||
// Luma
|
||||
let ql = qfactors(&ctx.qmat_luma, qs);
|
||||
let nb = 4 * mbs;
|
||||
decode_component(ydata, log2_mbs + 2, ctx.scan, &ql, &mut s.coeffs[..nb])?;
|
||||
for (b, blk) in s.coeffs[..nb].iter_mut().enumerate() {
|
||||
idct8x8(blk);
|
||||
let (mb, i) = (b >> 2, b & 3);
|
||||
put_block(blk, band.y, ctx.width, x0 + mb * 16 + (i & 1) * 8, (i >> 1) * 8, band.rows, step, parity, ctx.conv);
|
||||
}
|
||||
|
||||
// Chroma
|
||||
let qc = qfactors(&ctx.qmat_chroma, qs);
|
||||
let (cnb, clog2) = if ctx.chroma444 { (4 * mbs, log2_mbs + 2) } else { (2 * mbs, log2_mbs + 1) };
|
||||
for (data, plane) in [(cbdata, &mut *band.cb), (crdata, &mut *band.cr)] {
|
||||
decode_component(data, clog2, ctx.scan, &qc, &mut s.coeffs[..cnb])?;
|
||||
for (b, blk) in s.coeffs[..cnb].iter_mut().enumerate() {
|
||||
idct8x8(blk);
|
||||
let (bx, by) = if ctx.chroma444 {
|
||||
// 4:4:4 chroma blocks run down each 8-wide column pair: TL, BL, TR, BR.
|
||||
let (mb, i) = (b >> 2, b & 3);
|
||||
(x0 + mb * 16 + (i >> 1) * 8, (i & 1) * 8)
|
||||
} else {
|
||||
let (mb, i) = (b >> 1, b & 1);
|
||||
(mb_x * 8 + mb * 8, i * 8)
|
||||
};
|
||||
put_block(blk, plane, ctx.cwidth, bx, by, band.rows, step, parity, ctx.conv);
|
||||
}
|
||||
}
|
||||
|
||||
// Alpha
|
||||
if let Some(a) = band.a.as_deref_mut() {
|
||||
let w = mbs * 16;
|
||||
let n = w * 16;
|
||||
let buf = &mut s.alpha[..n];
|
||||
decode_alpha(adata, ctx.alpha, buf)?;
|
||||
let depth = ctx.depth as u32;
|
||||
let conv = |v: u16| -> u16 {
|
||||
let v = v as u32;
|
||||
(match ctx.alpha {
|
||||
AlphaType::Bits8 => {
|
||||
if depth >= 8 {
|
||||
let r = (v << (depth - 8)) | (v >> (16 - depth).min(8));
|
||||
r & ((1 << depth) - 1)
|
||||
} else {
|
||||
v
|
||||
}
|
||||
}
|
||||
_ => v >> (16 - depth),
|
||||
}) as u16
|
||||
};
|
||||
for py in 0..16 {
|
||||
let line = py * step + parity;
|
||||
if line >= band.rows {
|
||||
break;
|
||||
}
|
||||
let row = &mut a[line * ctx.width..(line + 1) * ctx.width];
|
||||
let xe = (x0 + w).min(ctx.width);
|
||||
if x0 >= xe {
|
||||
continue;
|
||||
}
|
||||
for (o, &v) in row[x0..xe].iter_mut().zip(&buf[py * w..py * w + (xe - x0)]) {
|
||||
*o = conv(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn to_signed(code: u32) -> i64 {
|
||||
let c = code as i64;
|
||||
(c >> 1) ^ -(c & 1)
|
||||
}
|
||||
|
||||
/// Entropy-decode and dequantise one slice component into `blocks` (raster-order coefficients).
|
||||
fn decode_component(data: &[u8], log2_blocks: u32, scan: &[u8; 64], q: &[f32; 64], blocks: &mut [[f32; 64]]) -> std::result::Result<(), ()> {
|
||||
for b in blocks.iter_mut() {
|
||||
*b = [0f32; 64];
|
||||
}
|
||||
let n = blocks.len();
|
||||
let mut r = Reader::new(data);
|
||||
|
||||
// DC coefficients
|
||||
let code = r.read_cw(FIRST_DC_CB).ok_or(())?;
|
||||
let mut dc = to_signed(code);
|
||||
blocks[0][0] = dc as f32 * q[0];
|
||||
let mut code = DC_CTX_INIT;
|
||||
let mut sign = 0i64;
|
||||
for blk in blocks.iter_mut().skip(1) {
|
||||
code = r.read_cw(DC_CB[code.min(6) as usize]).ok_or(())?;
|
||||
if code == 0 {
|
||||
sign = 0;
|
||||
} else {
|
||||
sign ^= -((code & 1) as i64);
|
||||
}
|
||||
let mag = (code as i64 + 1) >> 1;
|
||||
dc = (dc + ((mag ^ sign) - sign)).clamp(-(1 << 24), 1 << 24);
|
||||
blk[0] = dc as f32 * q[0];
|
||||
}
|
||||
|
||||
// AC coefficients, interleaved across blocks: position = scan_index * n + block
|
||||
let mask = n - 1;
|
||||
let total = 64 * n;
|
||||
let mut pos = mask;
|
||||
let mut run_ctx = RUN_CTX_INIT;
|
||||
let mut lev_ctx = LEVEL_CTX_INIT;
|
||||
while !r.only_padding_left() {
|
||||
let run = r.read_cw(RUN_CB[run_ctx.min(15) as usize]).ok_or(())?;
|
||||
run_ctx = run;
|
||||
pos += run as usize + 1;
|
||||
if pos >= total {
|
||||
return Err(());
|
||||
}
|
||||
let lev = r.read_cw(LEVEL_CB[lev_ctx.min(9) as usize]).ok_or(())?.saturating_add(1);
|
||||
lev_ctx = lev;
|
||||
let neg = r.read_bit();
|
||||
let i = scan[pos >> log2_blocks] as usize;
|
||||
let v = lev as f32 * q[i];
|
||||
blocks[pos & mask][i] = if neg { -v } else { v };
|
||||
}
|
||||
if r.overrun() {
|
||||
return Err(());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode a slice's alpha plane (raster order over the slice's 16 lines) into `out`
|
||||
/// (native 8- or 16-bit values).
|
||||
fn decode_alpha(data: &[u8], ty: AlphaType, out: &mut [u16]) -> std::result::Result<(), ()> {
|
||||
let bits = if ty == AlphaType::Bits16 { 16 } else { 8 };
|
||||
let short_bits = if bits == 16 { 7 } else { 4 };
|
||||
let mask: u32 = (1 << bits) - 1;
|
||||
let mut r = Reader::new(data);
|
||||
let mut val: u32 = mask;
|
||||
let n = out.len();
|
||||
let mut idx = 0;
|
||||
'outer: loop {
|
||||
loop {
|
||||
let d = if r.read_bit() {
|
||||
r.read(bits)
|
||||
} else {
|
||||
let c = r.read(short_bits);
|
||||
let m = (c + 2) >> 1;
|
||||
if c & 1 == 1 { m.wrapping_neg() } else { m }
|
||||
};
|
||||
val = val.wrapping_add(d) & mask;
|
||||
out[idx] = val as u16;
|
||||
idx += 1;
|
||||
if idx >= n {
|
||||
break 'outer;
|
||||
}
|
||||
if r.bits_left() <= 0 || !r.read_bit() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut run = r.read(4) as usize;
|
||||
if run == 0 {
|
||||
run = r.read(11) as usize;
|
||||
}
|
||||
let run = run.min(n - idx);
|
||||
out[idx..idx + run].fill(val as u16);
|
||||
idx += run;
|
||||
if idx >= n {
|
||||
break;
|
||||
}
|
||||
if r.bits_left() <= 0 {
|
||||
// Streams may end early; the remainder repeats the last value.
|
||||
out[idx..].fill(val as u16);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if r.bits_left() < -64 {
|
||||
return Err(());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[inline]
|
||||
fn put_block(f: &[f32; 64], plane: &mut [u16], stride: usize, x0: usize, y0: usize, rows: usize, step: usize, parity: usize, conv: OutConv) {
|
||||
if x0 >= stride {
|
||||
return;
|
||||
}
|
||||
let xn = (stride - x0).min(8);
|
||||
if xn == 8 && (y0 + 7) * step + parity < rows {
|
||||
for yy in 0..8 {
|
||||
let o = ((y0 + yy) * step + parity) * stride + x0;
|
||||
let row: &mut [u16; 8] = (&mut plane[o..o + 8]).try_into().unwrap();
|
||||
let src: &[f32; 8] = f[yy * 8..yy * 8 + 8].try_into().unwrap();
|
||||
for x in 0..8 {
|
||||
row[x] = conv.conv(src[x]);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
for yy in 0..8 {
|
||||
let line = (y0 + yy) * step + parity;
|
||||
if line >= rows {
|
||||
break;
|
||||
}
|
||||
let row = &mut plane[line * stride + x0..line * stride + x0 + xn];
|
||||
let src = &f[yy * 8..yy * 8 + xn];
|
||||
for (o, &v) in row.iter_mut().zip(src) {
|
||||
*o = conv.conv(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,495 @@
|
|||
//! ProRes frame encoding (progressive) with per-slice quantiser rate control.
|
||||
|
||||
use crate::bits::{Counter, Sink, Writer};
|
||||
use crate::dct::fdct8x8;
|
||||
use crate::header::{FRAME_ID, FrameHeader, PictureHeader, row_slice_widths};
|
||||
use crate::tables::*;
|
||||
use crate::{AlphaType, ChromaFormat, ColorInfo, Error, Frame, Interlace, Profile, Result};
|
||||
|
||||
/// Encoder configuration.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EncoderConfig {
|
||||
pub profile: Profile,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
/// Coded-bits budget per macroblock; `None` uses [`Profile::nominal_bits_per_mb`].
|
||||
pub bits_per_mb: Option<u32>,
|
||||
/// log2 of the macroblocks per slice (0..=3; 3 = 8 macroblocks, the common choice).
|
||||
pub log2_slice_mbs: u8,
|
||||
pub color: ColorInfo,
|
||||
pub aspect_ratio: u8,
|
||||
pub frame_rate_code: u8,
|
||||
/// Encode the frame's alpha plane (4444 profiles only; 16-bit alpha coding).
|
||||
pub encode_alpha: bool,
|
||||
/// Encode slices in parallel (requires the `threads` feature).
|
||||
pub threads: bool,
|
||||
/// Four-character encoder identifier written into the frame header.
|
||||
pub encoder_id: [u8; 4],
|
||||
}
|
||||
|
||||
impl EncoderConfig {
|
||||
pub fn new(profile: Profile, width: u32, height: u32) -> EncoderConfig {
|
||||
EncoderConfig {
|
||||
profile,
|
||||
width,
|
||||
height,
|
||||
bits_per_mb: None,
|
||||
log2_slice_mbs: 3,
|
||||
color: ColorInfo::default(),
|
||||
aspect_ratio: 0,
|
||||
frame_rate_code: 0,
|
||||
encode_alpha: true,
|
||||
threads: true,
|
||||
encoder_id: *b"fmcr",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ProRes encoder for one stream of same-sized progressive frames.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Encoder {
|
||||
cfg: EncoderConfig,
|
||||
}
|
||||
|
||||
/// Quantiser search range (RDD 36 allows 1..=224).
|
||||
const Q_MIN: u8 = 1;
|
||||
const Q_MAX: u8 = 224;
|
||||
|
||||
impl Encoder {
|
||||
pub fn new(profile: Profile, width: u32, height: u32) -> Encoder {
|
||||
Encoder { cfg: EncoderConfig::new(profile, width, height) }
|
||||
}
|
||||
|
||||
pub fn with_config(cfg: EncoderConfig) -> Encoder {
|
||||
Encoder { cfg }
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &EncoderConfig {
|
||||
&self.cfg
|
||||
}
|
||||
|
||||
pub fn bits_per_mb(&self) -> u32 {
|
||||
self.cfg.bits_per_mb.unwrap_or_else(|| self.cfg.profile.nominal_bits_per_mb())
|
||||
}
|
||||
|
||||
/// The rate-control target for one frame in bytes (excluding alpha).
|
||||
pub fn target_frame_bytes(&self) -> usize {
|
||||
let mbs = (self.cfg.width as usize).div_ceil(16) * (self.cfg.height as usize).div_ceil(16);
|
||||
mbs * self.bits_per_mb() as usize / 8
|
||||
}
|
||||
|
||||
/// Encode one frame. Input planes may be at any bit depth 8..=16 (10 is typical); the frame's
|
||||
/// chroma format must match the profile.
|
||||
pub fn encode(&mut self, frame: &Frame) -> Result<Vec<u8>> {
|
||||
let cfg = &self.cfg;
|
||||
if frame.width != cfg.width || frame.height != cfg.height {
|
||||
return Err(Error::Input(format!("frame is {}x{}, encoder is {}x{}", frame.width, frame.height, cfg.width, cfg.height)));
|
||||
}
|
||||
if cfg.width == 0 || cfg.height == 0 || cfg.width > 65535 || cfg.height > 65535 {
|
||||
return Err(Error::Input("unsupported frame size".into()));
|
||||
}
|
||||
let chroma = cfg.profile.chroma();
|
||||
if frame.chroma != chroma {
|
||||
return Err(Error::Input(format!("{:?} input for a {:?} profile", frame.chroma, chroma)));
|
||||
}
|
||||
if !(8..=16).contains(&frame.bit_depth) {
|
||||
return Err(Error::Input("bit depth must be 8..=16".into()));
|
||||
}
|
||||
let (w, h) = (cfg.width as usize, cfg.height as usize);
|
||||
let cw = chroma.chroma_width(cfg.width) as usize;
|
||||
if frame.y.len() < w * h || frame.cb.len() < cw * h || frame.cr.len() < cw * h {
|
||||
return Err(Error::Input("plane too small".into()));
|
||||
}
|
||||
let with_alpha = cfg.encode_alpha && chroma == ChromaFormat::Yuv444 && frame.alpha.is_some();
|
||||
if let Some(a) = &frame.alpha
|
||||
&& with_alpha
|
||||
&& a.len() < w * h
|
||||
{
|
||||
return Err(Error::Input("alpha plane too small".into()));
|
||||
}
|
||||
if cfg.log2_slice_mbs > 3 {
|
||||
return Err(Error::Input("log2_slice_mbs must be 0..=3".into()));
|
||||
}
|
||||
|
||||
let mb_width = w.div_ceil(16);
|
||||
let mb_height = h.div_ceil(16);
|
||||
let widths = row_slice_widths(mb_width, cfg.log2_slice_mbs);
|
||||
let mut jobs = Vec::with_capacity(widths.len() * mb_height);
|
||||
for row in 0..mb_height {
|
||||
let mut x = 0;
|
||||
for &sw in &widths {
|
||||
jobs.push((x, row, sw));
|
||||
x += sw;
|
||||
}
|
||||
}
|
||||
let src = Source {
|
||||
frame,
|
||||
w,
|
||||
h,
|
||||
cw,
|
||||
chroma444: chroma == ChromaFormat::Yuv444,
|
||||
scale: (2f32).powi(12 - frame.bit_depth as i32),
|
||||
alpha: with_alpha,
|
||||
bits_per_mb: self.bits_per_mb(),
|
||||
};
|
||||
let threads = cfg.threads;
|
||||
let prepared: Vec<SliceData> = par_map(threads, &jobs, |&(x, row, sw)| prepare_slice(&src, x, row, sw));
|
||||
let qs = choose_quantisers(&prepared, &src, mb_width * mb_height, threads);
|
||||
let items: Vec<(&SliceData, u8)> = prepared.iter().zip(qs).collect();
|
||||
let slices: Vec<Vec<u8>> = par_map(threads, &items, |&(sd, q)| write_slice(sd, q));
|
||||
|
||||
let hdr = FrameHeader {
|
||||
header_size: 0,
|
||||
bitstream_version: if chroma == ChromaFormat::Yuv444 { 1 } else { 0 },
|
||||
encoder_id: cfg.encoder_id,
|
||||
width: cfg.width as u16,
|
||||
height: cfg.height as u16,
|
||||
chroma,
|
||||
interlace: Interlace::Progressive,
|
||||
aspect_ratio: cfg.aspect_ratio & 15,
|
||||
frame_rate_code: cfg.frame_rate_code & 15,
|
||||
color: cfg.color,
|
||||
alpha: if with_alpha { AlphaType::Bits16 } else { AlphaType::None },
|
||||
qmat_luma: [4; 64],
|
||||
qmat_chroma: [4; 64],
|
||||
luma_matrix_present: false,
|
||||
chroma_matrix_present: false,
|
||||
};
|
||||
let fh = hdr.write();
|
||||
let slice_bytes: usize = slices.iter().map(Vec::len).sum();
|
||||
let pic_size = 8 + 2 * slices.len() + slice_bytes;
|
||||
let total = 8 + fh.len() + pic_size;
|
||||
let mut out = Vec::with_capacity(total);
|
||||
out.extend_from_slice(&(total as u32).to_be_bytes());
|
||||
out.extend_from_slice(&FRAME_ID);
|
||||
out.extend_from_slice(&fh);
|
||||
PictureHeader { header_size: 8, picture_size: pic_size, num_slices: slices.len(), log2_slice_mbs: cfg.log2_slice_mbs }.write(&mut out);
|
||||
for s in &slices {
|
||||
out.extend_from_slice(&(s.len() as u16).to_be_bytes());
|
||||
}
|
||||
for s in &slices {
|
||||
out.extend_from_slice(s);
|
||||
}
|
||||
debug_assert_eq!(out.len(), total);
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
struct Source<'a> {
|
||||
frame: &'a Frame,
|
||||
w: usize,
|
||||
h: usize,
|
||||
cw: usize,
|
||||
chroma444: bool,
|
||||
/// Multiplier from input sample values to the 12-bit domain.
|
||||
scale: f32,
|
||||
alpha: bool,
|
||||
bits_per_mb: u32,
|
||||
}
|
||||
|
||||
/// Load an 8×8 block (edge-replicated) into the 12-bit signed domain and transform it.
|
||||
fn load_block(plane: &[u16], stride: usize, height: usize, x0: usize, y0: usize, scale: f32) -> [f32; 64] {
|
||||
let mut b = [0f32; 64];
|
||||
for yy in 0..8 {
|
||||
let y = (y0 + yy).min(height - 1);
|
||||
let row = &plane[y * stride..(y + 1) * stride];
|
||||
for xx in 0..8 {
|
||||
let x = (x0 + xx).min(stride - 1);
|
||||
b[yy * 8 + xx] = row[x] as f32 * scale - 2048.0;
|
||||
}
|
||||
}
|
||||
fdct8x8(&mut b);
|
||||
b
|
||||
}
|
||||
|
||||
/// Rounding offsets for quantisation: nearest for DC, a slight dead zone for AC.
|
||||
const AC_ROUND: f32 = 0.45;
|
||||
|
||||
fn quantise(coeffs: &[[f32; 64]], qmat_inv: &[f32; 64], out: &mut Vec<[i32; 64]>) {
|
||||
out.clear();
|
||||
for c in coeffs {
|
||||
let mut l = [0i32; 64];
|
||||
let dc = c[0] * qmat_inv[0];
|
||||
l[0] = dc.round() as i32;
|
||||
for i in 1..64 {
|
||||
let v = c[i] * qmat_inv[i];
|
||||
let a = (v.abs() + AC_ROUND) as i32;
|
||||
l[i] = if v < 0.0 { -a } else { a };
|
||||
}
|
||||
out.push(l);
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_component<S: Sink>(s: &mut S, levels: &[[i32; 64]], scan: &[u8; 64]) {
|
||||
let n = levels.len();
|
||||
// DC
|
||||
let first = levels[0][0];
|
||||
let zz = if first >= 0 { (first as u32) << 1 } else { ((-(first as i64)) as u32) * 2 - 1 };
|
||||
s.put_cw(FIRST_DC_CB, zz);
|
||||
let mut prev = first;
|
||||
let mut ctx = DC_CTX_INIT;
|
||||
let mut sign = false;
|
||||
for blk in &levels[1..] {
|
||||
let dc = blk[0];
|
||||
let delta = dc - prev;
|
||||
prev = dc;
|
||||
let code = if delta == 0 {
|
||||
sign = false;
|
||||
0
|
||||
} else {
|
||||
let neg = delta < 0;
|
||||
let mag = delta.unsigned_abs();
|
||||
let c = if neg != sign { 2 * mag - 1 } else { 2 * mag };
|
||||
sign = neg;
|
||||
c
|
||||
};
|
||||
s.put_cw(DC_CB[ctx.min(6) as usize], code);
|
||||
ctx = code;
|
||||
}
|
||||
// AC, interleaved across blocks
|
||||
let mut run = 0u32;
|
||||
let mut run_ctx = RUN_CTX_INIT;
|
||||
let mut lev_ctx = LEVEL_CTX_INIT;
|
||||
for &nat in &scan[1..] {
|
||||
let nat = nat as usize;
|
||||
for blk in levels.iter().take(n) {
|
||||
let l = blk[nat];
|
||||
if l == 0 {
|
||||
run += 1;
|
||||
} else {
|
||||
s.put_cw(RUN_CB[run_ctx.min(15) as usize], run);
|
||||
run_ctx = run;
|
||||
let a = l.unsigned_abs();
|
||||
s.put_cw(LEVEL_CB[lev_ctx.min(9) as usize], a - 1);
|
||||
lev_ctx = a;
|
||||
s.put((l < 0) as u32, 1);
|
||||
run = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn component_bits(levels: &[[i32; 64]]) -> u64 {
|
||||
let mut c = Counter::default();
|
||||
encode_component(&mut c, levels, &SCAN_PROGRESSIVE);
|
||||
c.0.div_ceil(8) * 8
|
||||
}
|
||||
|
||||
/// A slice's transformed blocks (Y, Cb, Cr) and its coded alpha.
|
||||
struct SliceData {
|
||||
comps: [Vec<[f32; 64]>; 3],
|
||||
alpha: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl SliceData {
|
||||
fn header_size(&self) -> usize {
|
||||
if self.alpha.is_some() { 8 } else { 6 }
|
||||
}
|
||||
/// Largest DCT payload (bits) that keeps every 16-bit size field valid.
|
||||
fn limit_bits(&self) -> u64 {
|
||||
(65535 - self.header_size() - self.alpha.as_ref().map_or(0, Vec::len)) as u64 * 8
|
||||
}
|
||||
}
|
||||
|
||||
fn par_map<T: Sync, R: Send>(threads: bool, items: &[T], f: impl Fn(&T) -> R + Sync + Send) -> Vec<R> {
|
||||
#[cfg(feature = "threads")]
|
||||
if threads {
|
||||
use rayon::prelude::*;
|
||||
return items.par_iter().map(f).collect();
|
||||
}
|
||||
let _ = threads;
|
||||
items.iter().map(f).collect()
|
||||
}
|
||||
|
||||
fn prepare_slice(src: &Source, mb_x: usize, mb_y: usize, mbs: usize) -> SliceData {
|
||||
let f = src.frame;
|
||||
let x0 = mb_x * 16;
|
||||
let y0 = mb_y * 16;
|
||||
let mut luma = Vec::with_capacity(4 * mbs);
|
||||
for mb in 0..mbs {
|
||||
for i in 0..4 {
|
||||
luma.push(load_block(&f.y, src.w, src.h, x0 + mb * 16 + (i & 1) * 8, y0 + (i >> 1) * 8, src.scale));
|
||||
}
|
||||
}
|
||||
let mut chroma = [Vec::with_capacity(4 * mbs), Vec::with_capacity(4 * mbs)];
|
||||
for (k, plane) in [&f.cb, &f.cr].into_iter().enumerate() {
|
||||
for mb in 0..mbs {
|
||||
if src.chroma444 {
|
||||
// 4:4:4 chroma block order within a macroblock: TL, BL, TR, BR.
|
||||
for i in 0..4 {
|
||||
chroma[k].push(load_block(plane, src.cw, src.h, x0 + mb * 16 + (i >> 1) * 8, y0 + (i & 1) * 8, src.scale));
|
||||
}
|
||||
} else {
|
||||
for i in 0..2 {
|
||||
chroma[k].push(load_block(plane, src.cw, src.h, mb_x * 8 + mb * 8, y0 + i * 8, src.scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let [cb, cr] = chroma;
|
||||
let alpha = src.alpha.then(|| encode_alpha_slice(src, x0, y0, mbs));
|
||||
SliceData { comps: [luma, cb, cr], alpha }
|
||||
}
|
||||
|
||||
fn quantise_all(sd: &SliceData, q: u8, lv: &mut [Vec<[i32; 64]>; 3]) {
|
||||
let inv = [1.0 / (4.0 * qscale(q) as f32); 64];
|
||||
for (c, l) in sd.comps.iter().zip(lv.iter_mut()) {
|
||||
quantise(c, &inv, l);
|
||||
}
|
||||
}
|
||||
|
||||
/// Coded DCT payload bits of a slice at quantiser `q` (components padded to bytes).
|
||||
fn slice_bits(sd: &SliceData, q: u8) -> u64 {
|
||||
let mut lv = [Vec::new(), Vec::new(), Vec::new()];
|
||||
quantise_all(sd, q, &mut lv);
|
||||
lv.iter().map(|l| component_bits(l)).sum()
|
||||
}
|
||||
|
||||
/// Frame-level rate control: the smallest uniform quantiser whose frame fits the budget, then
|
||||
/// one step finer on as many slices as the remaining budget allows. Uniform quantisation
|
||||
/// approximately minimises frame MSE for a given size.
|
||||
fn choose_quantisers(slices: &[SliceData], src: &Source, total_mbs: usize, threads: bool) -> Vec<u8> {
|
||||
let n = slices.len();
|
||||
let hsize = slices.first().map_or(6, SliceData::header_size);
|
||||
let fixed = 8 * (8 + 20 + 8 + n * (2 + hsize)) as u64;
|
||||
let budget = (src.bits_per_mb as u64 * total_mbs as u64).saturating_sub(fixed);
|
||||
let bits_at = |q: u8| -> Vec<u64> { par_map(threads, slices, |sd| slice_bits(sd, q)) };
|
||||
let sum = |v: &[u64]| v.iter().sum::<u64>();
|
||||
|
||||
let mut best = bits_at(Q_MIN);
|
||||
let mut qg = Q_MIN;
|
||||
if sum(&best) > budget {
|
||||
let (mut lo, mut hi) = (Q_MIN + 1, Q_MAX);
|
||||
let mut hi_bits = None;
|
||||
while lo < hi {
|
||||
let mid = lo + (hi - lo) / 2;
|
||||
let v = bits_at(mid);
|
||||
if sum(&v) <= budget {
|
||||
hi = mid;
|
||||
hi_bits = Some(v);
|
||||
} else {
|
||||
lo = mid + 1;
|
||||
}
|
||||
}
|
||||
qg = lo;
|
||||
best = hi_bits.unwrap_or_else(|| bits_at(qg));
|
||||
}
|
||||
let mut qs = vec![qg; n];
|
||||
let mut cur = best;
|
||||
let total = sum(&cur);
|
||||
if qg > Q_MIN && total < budget {
|
||||
let finer = bits_at(qg - 1);
|
||||
let mut left = budget - total;
|
||||
for i in 0..n {
|
||||
let d = finer[i].saturating_sub(cur[i]);
|
||||
if d <= left && finer[i] <= slices[i].limit_bits() {
|
||||
left -= d;
|
||||
qs[i] = qg - 1;
|
||||
cur[i] = finer[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keep every slice within its 16-bit size fields.
|
||||
for i in 0..n {
|
||||
while cur[i] > slices[i].limit_bits() && qs[i] < Q_MAX {
|
||||
qs[i] += 1;
|
||||
cur[i] = slice_bits(&slices[i], qs[i]);
|
||||
}
|
||||
}
|
||||
qs
|
||||
}
|
||||
|
||||
fn write_slice(sd: &SliceData, q: u8) -> Vec<u8> {
|
||||
let mut lv = [Vec::new(), Vec::new(), Vec::new()];
|
||||
quantise_all(sd, q, &mut lv);
|
||||
let comps: Vec<Vec<u8>> = lv
|
||||
.iter()
|
||||
.map(|l| {
|
||||
let mut w = Writer::new();
|
||||
encode_component(&mut w, l, &SCAN_PROGRESSIVE);
|
||||
w.finish()
|
||||
})
|
||||
.collect();
|
||||
let hsize = sd.header_size();
|
||||
let alpha = sd.alpha.as_deref().unwrap_or(&[]);
|
||||
let mut out = Vec::with_capacity(hsize + comps.iter().map(Vec::len).sum::<usize>() + alpha.len());
|
||||
out.push((hsize as u8) << 3);
|
||||
out.push(q);
|
||||
out.extend_from_slice(&(comps[0].len() as u16).to_be_bytes());
|
||||
out.extend_from_slice(&(comps[1].len() as u16).to_be_bytes());
|
||||
if sd.alpha.is_some() {
|
||||
out.extend_from_slice(&(comps[2].len() as u16).to_be_bytes());
|
||||
}
|
||||
for c in &comps {
|
||||
out.extend_from_slice(c);
|
||||
}
|
||||
out.extend_from_slice(alpha);
|
||||
out
|
||||
}
|
||||
|
||||
/// Encode the slice's alpha samples (16-bit coding) in raster order over its 16 lines.
|
||||
fn encode_alpha_slice(src: &Source, x0: usize, y0: usize, mbs: usize) -> Vec<u8> {
|
||||
let a = src.frame.alpha.as_ref().expect("alpha checked");
|
||||
let d = src.frame.bit_depth as u32;
|
||||
let to16 = |v: u16| -> u32 {
|
||||
let v = v as u32 & ((1 << d) - 1);
|
||||
if d >= 16 { v } else { ((v << (16 - d)) | (v >> (2 * d).saturating_sub(16).min(d))) & 0xffff }
|
||||
};
|
||||
let sw = mbs * 16;
|
||||
let mut vals = Vec::with_capacity(sw * 16);
|
||||
for yy in 0..16 {
|
||||
let y = (y0 + yy).min(src.h - 1);
|
||||
for xx in 0..sw {
|
||||
let x = (x0 + xx).min(src.w - 1);
|
||||
vals.push(to16(a[y * src.w + x]));
|
||||
}
|
||||
}
|
||||
let mut w = Writer::new();
|
||||
encode_alpha(&mut w, &vals, 16);
|
||||
w.finish()
|
||||
}
|
||||
|
||||
/// Alpha plane coding: a value (difference from the previous value, short or full width), then
|
||||
/// either a one bit (another value follows) or a zero bit and a run of repeats of the value.
|
||||
pub(crate) fn encode_alpha(w: &mut Writer, vals: &[u32], bits: u32) {
|
||||
let mask = (1u32 << bits) - 1;
|
||||
let (short_bits, short_max) = if bits == 16 { (7, 64i32) } else { (4, 8i32) };
|
||||
let n = vals.len();
|
||||
let mut prev = mask;
|
||||
let mut idx = 0;
|
||||
while idx < n {
|
||||
let v = vals[idx];
|
||||
let diff = v.wrapping_sub(prev) & mask;
|
||||
// signed difference in (-2^(bits-1), 2^(bits-1)]
|
||||
let sd = if diff > mask >> 1 { diff as i32 - (mask as i32 + 1) } else { diff as i32 };
|
||||
if sd != 0 && sd.abs() <= short_max {
|
||||
w.put(0, 1);
|
||||
let c = (2 * sd.unsigned_abs() - 2) | (sd < 0) as u32;
|
||||
w.put(c, short_bits);
|
||||
} else {
|
||||
w.put(1, 1);
|
||||
w.put(diff, bits);
|
||||
}
|
||||
prev = v;
|
||||
idx += 1;
|
||||
if idx >= n {
|
||||
break;
|
||||
}
|
||||
let mut run = 0;
|
||||
while idx + run < n && vals[idx + run] == prev && run < 2047 {
|
||||
run += 1;
|
||||
}
|
||||
if run == 0 {
|
||||
w.put(1, 1);
|
||||
continue;
|
||||
}
|
||||
w.put(0, 1);
|
||||
if run < 16 {
|
||||
w.put(run as u32, 4);
|
||||
} else {
|
||||
w.put(0, 4);
|
||||
w.put(run as u32, 11);
|
||||
}
|
||||
idx += run;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
//! Frame and picture headers (RDD 36 §5).
|
||||
|
||||
use crate::{AlphaType, ChromaFormat, ColorInfo, Error, Interlace, Result};
|
||||
use filmcraft_bitstream::BitReader;
|
||||
|
||||
/// `'icpf'`: the frame identifier following the 32-bit frame size.
|
||||
pub const FRAME_ID: [u8; 4] = *b"icpf";
|
||||
|
||||
/// A parsed ProRes frame header.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct FrameHeader {
|
||||
/// Size of the frame header in bytes (from its own first field).
|
||||
pub header_size: u16,
|
||||
pub bitstream_version: u8,
|
||||
/// Four-character encoder identifier (e.g. `apl0`, `Lavc`, `fmcr`).
|
||||
pub encoder_id: [u8; 4],
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
pub chroma: ChromaFormat,
|
||||
pub interlace: Interlace,
|
||||
/// `aspect_ratio_information` (0 = unknown/square, 1 = 1:1, 2 = 4:3, 3 = 16:9).
|
||||
pub aspect_ratio: u8,
|
||||
/// `frame_rate_code` (0 = unknown).
|
||||
pub frame_rate_code: u8,
|
||||
pub color: ColorInfo,
|
||||
pub alpha: AlphaType,
|
||||
/// Luma quantisation matrix in raster order (all 4 when not transmitted).
|
||||
pub qmat_luma: [u8; 64],
|
||||
/// Chroma quantisation matrix in raster order (the luma matrix when not transmitted).
|
||||
pub qmat_chroma: [u8; 64],
|
||||
pub luma_matrix_present: bool,
|
||||
pub chroma_matrix_present: bool,
|
||||
}
|
||||
|
||||
impl FrameHeader {
|
||||
/// Parse the 8-byte frame prefix and frame header. Returns the header, the byte offset of the
|
||||
/// first picture, and the frame size (bytes of `data` that belong to this frame).
|
||||
pub fn parse(data: &[u8]) -> Result<(FrameHeader, usize, usize)> {
|
||||
if data.len() < 8 + 20 {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
let frame_size = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
|
||||
if data[4..8] != FRAME_ID {
|
||||
return Err(Error::Invalid("missing 'icpf' frame identifier"));
|
||||
}
|
||||
if frame_size < 28 || frame_size > data.len() {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
let data = &data[..frame_size];
|
||||
let h = &data[8..];
|
||||
let mut r = BitReader::new(h);
|
||||
let rd = |r: &mut BitReader, n: u32| r.read_bits(n).map_err(|_| Error::Truncated);
|
||||
let header_size = rd(&mut r, 16)? as u16;
|
||||
if (header_size as usize) < 20 || 8 + header_size as usize > data.len() {
|
||||
return Err(Error::Invalid("bad frame header size"));
|
||||
}
|
||||
let _reserved = rd(&mut r, 8)?;
|
||||
let bitstream_version = rd(&mut r, 8)? as u8;
|
||||
let encoder_id = [h[4], h[5], h[6], h[7]];
|
||||
r.skip(32).map_err(|_| Error::Truncated)?;
|
||||
let width = rd(&mut r, 16)? as u16;
|
||||
let height = rd(&mut r, 16)? as u16;
|
||||
let chroma = match rd(&mut r, 2)? {
|
||||
2 => ChromaFormat::Yuv422,
|
||||
3 => ChromaFormat::Yuv444,
|
||||
_ => return Err(Error::Unsupported("chroma format")),
|
||||
};
|
||||
let _ = rd(&mut r, 2)?;
|
||||
let interlace = match rd(&mut r, 2)? {
|
||||
0 => Interlace::Progressive,
|
||||
1 => Interlace::TopFieldFirst,
|
||||
2 => Interlace::BottomFieldFirst,
|
||||
_ => return Err(Error::Invalid("interlace mode")),
|
||||
};
|
||||
let _ = rd(&mut r, 2)?;
|
||||
let aspect_ratio = rd(&mut r, 4)? as u8;
|
||||
let frame_rate_code = rd(&mut r, 4)? as u8;
|
||||
let color = ColorInfo { primaries: rd(&mut r, 8)? as u8, transfer: rd(&mut r, 8)? as u8, matrix: rd(&mut r, 8)? as u8 };
|
||||
let _ = rd(&mut r, 4)?;
|
||||
let alpha = match rd(&mut r, 4)? {
|
||||
0 => AlphaType::None,
|
||||
1 => AlphaType::Bits8,
|
||||
2 => AlphaType::Bits16,
|
||||
_ => return Err(Error::Unsupported("alpha channel type")),
|
||||
};
|
||||
let _ = rd(&mut r, 14)?;
|
||||
let luma_matrix_present = rd(&mut r, 1)? == 1;
|
||||
let chroma_matrix_present = rd(&mut r, 1)? == 1;
|
||||
let mut pos = 20usize;
|
||||
let mut qmat_luma = [4u8; 64];
|
||||
if luma_matrix_present {
|
||||
let m = h.get(pos..pos + 64).ok_or(Error::Truncated)?;
|
||||
qmat_luma.copy_from_slice(m);
|
||||
pos += 64;
|
||||
}
|
||||
let mut qmat_chroma = qmat_luma;
|
||||
if chroma_matrix_present {
|
||||
let m = h.get(pos..pos + 64).ok_or(Error::Truncated)?;
|
||||
qmat_chroma.copy_from_slice(m);
|
||||
pos += 64;
|
||||
}
|
||||
if pos > header_size as usize {
|
||||
return Err(Error::Invalid("quantisation matrices exceed frame header"));
|
||||
}
|
||||
if qmat_luma.contains(&0) || qmat_chroma.contains(&0) {
|
||||
return Err(Error::Invalid("zero quantisation matrix entry"));
|
||||
}
|
||||
if width == 0 || height == 0 {
|
||||
return Err(Error::Invalid("zero frame dimension"));
|
||||
}
|
||||
let hdr = FrameHeader {
|
||||
header_size,
|
||||
bitstream_version,
|
||||
encoder_id,
|
||||
width,
|
||||
height,
|
||||
chroma,
|
||||
interlace,
|
||||
aspect_ratio,
|
||||
frame_rate_code,
|
||||
color,
|
||||
alpha,
|
||||
qmat_luma,
|
||||
qmat_chroma,
|
||||
luma_matrix_present,
|
||||
chroma_matrix_present,
|
||||
};
|
||||
Ok((hdr, 8 + header_size as usize, frame_size))
|
||||
}
|
||||
|
||||
/// Serialise the frame header (without the 8-byte size/identifier prefix).
|
||||
pub fn write(&self) -> Vec<u8> {
|
||||
let mut v = Vec::with_capacity(148);
|
||||
let size = 20 + 64 * (self.luma_matrix_present as usize + self.chroma_matrix_present as usize);
|
||||
v.extend_from_slice(&(size as u16).to_be_bytes());
|
||||
v.push(0);
|
||||
v.push(self.bitstream_version);
|
||||
v.extend_from_slice(&self.encoder_id);
|
||||
v.extend_from_slice(&self.width.to_be_bytes());
|
||||
v.extend_from_slice(&self.height.to_be_bytes());
|
||||
let cf = match self.chroma {
|
||||
ChromaFormat::Yuv422 => 2u8,
|
||||
ChromaFormat::Yuv444 => 3,
|
||||
};
|
||||
let il = match self.interlace {
|
||||
Interlace::Progressive => 0u8,
|
||||
Interlace::TopFieldFirst => 1,
|
||||
Interlace::BottomFieldFirst => 2,
|
||||
};
|
||||
v.push((cf << 6) | (il << 2));
|
||||
v.push((self.aspect_ratio << 4) | (self.frame_rate_code & 15));
|
||||
v.push(self.color.primaries);
|
||||
v.push(self.color.transfer);
|
||||
v.push(self.color.matrix);
|
||||
v.push(match self.alpha {
|
||||
AlphaType::None => 0,
|
||||
AlphaType::Bits8 => 1,
|
||||
AlphaType::Bits16 => 2,
|
||||
});
|
||||
v.push(0);
|
||||
v.push(((self.luma_matrix_present as u8) << 1) | self.chroma_matrix_present as u8);
|
||||
if self.luma_matrix_present {
|
||||
v.extend_from_slice(&self.qmat_luma);
|
||||
}
|
||||
if self.chroma_matrix_present {
|
||||
v.extend_from_slice(&self.qmat_chroma);
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// A parsed picture header (one per progressive frame, two per interlaced frame).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct PictureHeader {
|
||||
pub header_size: usize,
|
||||
/// Total picture size in bytes including this header, the slice table and the slices.
|
||||
pub picture_size: usize,
|
||||
pub num_slices: usize,
|
||||
pub log2_slice_mbs: u8,
|
||||
}
|
||||
|
||||
impl PictureHeader {
|
||||
pub fn parse(data: &[u8]) -> Result<PictureHeader> {
|
||||
if data.len() < 8 {
|
||||
return Err(Error::Truncated);
|
||||
}
|
||||
let header_size = (data[0] >> 3) as usize;
|
||||
if header_size < 8 || header_size > data.len() {
|
||||
return Err(Error::Invalid("bad picture header size"));
|
||||
}
|
||||
let picture_size = u32::from_be_bytes([data[1], data[2], data[3], data[4]]) as usize;
|
||||
let num_slices = u16::from_be_bytes([data[5], data[6]]) as usize;
|
||||
let log2_slice_mbs = (data[7] >> 4) & 3;
|
||||
Ok(PictureHeader { header_size, picture_size, num_slices, log2_slice_mbs })
|
||||
}
|
||||
|
||||
pub fn write(&self, out: &mut Vec<u8>) {
|
||||
out.push(8 << 3);
|
||||
out.extend_from_slice(&(self.picture_size as u32).to_be_bytes());
|
||||
out.extend_from_slice(&(self.num_slices as u16).to_be_bytes());
|
||||
out.push((self.log2_slice_mbs & 3) << 4);
|
||||
}
|
||||
}
|
||||
|
||||
/// Slice widths (in macroblocks) of one macroblock row: as many `1 << log2` slices as fit, then
|
||||
/// the remainder split into decreasing powers of two.
|
||||
pub fn row_slice_widths(mb_width: usize, log2_slice_mbs: u8) -> Vec<usize> {
|
||||
let size = 1usize << log2_slice_mbs;
|
||||
let mut v = vec![size; mb_width / size];
|
||||
let mut rem = mb_width % size;
|
||||
let mut s = size >> 1;
|
||||
while rem > 0 {
|
||||
if rem >= s {
|
||||
v.push(s);
|
||||
rem -= s;
|
||||
}
|
||||
s >>= 1;
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn slice_widths() {
|
||||
assert_eq!(row_slice_widths(120, 3), vec![8; 15]);
|
||||
assert_eq!(row_slice_widths(45, 3), vec![8, 8, 8, 8, 8, 4, 1]);
|
||||
assert_eq!(row_slice_widths(7, 3), vec![4, 2, 1]);
|
||||
assert_eq!(row_slice_widths(2, 3), vec![2]);
|
||||
assert_eq!(row_slice_widths(5, 0), vec![1; 5]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
//! Clean-room Apple ProRes codec (implemented from SMPTE RDD 36).
|
||||
//!
|
||||
//! - **Decoder** ([`decode_frame`], [`decode_frame_with`]): ProRes 422 Proxy/LT/Standard/HQ and
|
||||
//! 4444/4444 XQ including the alpha channel; progressive and interlaced frames; slice-parallel
|
||||
//! with the `threads` feature (rayon). Output is planar `u16` at 10 bits for 4:2:2 and 12 bits
|
||||
//! for 4:4:4 by default (any depth 8..=16 on request).
|
||||
//! - **Encoder** ([`Encoder`]): ProRes 422 (all four flavours) and 4444/4444 XQ (optionally with
|
||||
//! alpha), progressive, with per-slice quantiser rate control targeting Apple's nominal rates.
|
||||
//!
|
||||
//! See the crate README for bitstream details, accuracy and limitations.
|
||||
|
||||
mod bits;
|
||||
mod dct;
|
||||
mod decode;
|
||||
mod encode;
|
||||
pub mod header;
|
||||
mod tables;
|
||||
|
||||
pub use decode::{DecodeOptions, decode_frame, decode_frame_into, decode_frame_with, probe};
|
||||
pub use encode::{Encoder, EncoderConfig};
|
||||
pub use header::{FrameHeader, PictureHeader};
|
||||
|
||||
/// Codec errors.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("truncated ProRes data")]
|
||||
Truncated,
|
||||
#[error("invalid ProRes data: {0}")]
|
||||
Invalid(&'static str),
|
||||
#[error("unsupported ProRes feature: {0}")]
|
||||
Unsupported(&'static str),
|
||||
#[error("corrupt slice {slice} in picture {picture}")]
|
||||
Slice { picture: usize, slice: usize },
|
||||
#[error("invalid encoder input: {0}")]
|
||||
Input(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Chroma subsampling of a frame.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ChromaFormat {
|
||||
/// Chroma planes are half width, full height.
|
||||
Yuv422,
|
||||
/// Chroma planes are full size.
|
||||
Yuv444,
|
||||
}
|
||||
|
||||
impl ChromaFormat {
|
||||
/// Width of a chroma plane for a luma width.
|
||||
pub fn chroma_width(self, width: u32) -> u32 {
|
||||
match self {
|
||||
ChromaFormat::Yuv422 => width.div_ceil(2),
|
||||
ChromaFormat::Yuv444 => width,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Frame scan structure.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum Interlace {
|
||||
#[default]
|
||||
Progressive,
|
||||
/// Two fields; the first coded picture is the top field (even lines).
|
||||
TopFieldFirst,
|
||||
/// Two fields; the first coded picture is the bottom field (odd lines).
|
||||
BottomFieldFirst,
|
||||
}
|
||||
|
||||
/// Alpha channel coding in the bitstream.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum AlphaType {
|
||||
#[default]
|
||||
None,
|
||||
Bits8,
|
||||
Bits16,
|
||||
}
|
||||
|
||||
/// Colour description codes from the frame header (ITU-T H.273 / ISO 23091-2 code points;
|
||||
/// 0 = unspecified in ProRes, 2 = unspecified in H.273).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ColorInfo {
|
||||
pub primaries: u8,
|
||||
pub transfer: u8,
|
||||
pub matrix: u8,
|
||||
}
|
||||
|
||||
impl Default for ColorInfo {
|
||||
/// BT.709 primaries, transfer and matrix.
|
||||
fn default() -> Self {
|
||||
ColorInfo { primaries: 1, transfer: 1, matrix: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
/// ProRes profiles (flavours).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Profile {
|
||||
/// `apco`
|
||||
Proxy,
|
||||
/// `apcs`
|
||||
Lt,
|
||||
/// `apcn`
|
||||
Standard,
|
||||
/// `apch`
|
||||
Hq,
|
||||
/// `ap4h`
|
||||
P4444,
|
||||
/// `ap4x`
|
||||
P4444Xq,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub const ALL: [Profile; 6] = [Profile::Proxy, Profile::Lt, Profile::Standard, Profile::Hq, Profile::P4444, Profile::P4444Xq];
|
||||
|
||||
/// The QuickTime sample-entry four-character code.
|
||||
pub fn fourcc(self) -> [u8; 4] {
|
||||
*match self {
|
||||
Profile::Proxy => b"apco",
|
||||
Profile::Lt => b"apcs",
|
||||
Profile::Standard => b"apcn",
|
||||
Profile::Hq => b"apch",
|
||||
Profile::P4444 => b"ap4h",
|
||||
Profile::P4444Xq => b"ap4x",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_fourcc(f: &[u8; 4]) -> Option<Profile> {
|
||||
Profile::ALL.into_iter().find(|p| &p.fourcc() == f)
|
||||
}
|
||||
|
||||
pub fn chroma(self) -> ChromaFormat {
|
||||
match self {
|
||||
Profile::P4444 | Profile::P4444Xq => ChromaFormat::Yuv444,
|
||||
_ => ChromaFormat::Yuv422,
|
||||
}
|
||||
}
|
||||
|
||||
/// Apple's published nominal data rate at 1920×1080, 29.97 fps, in Mbit/s.
|
||||
pub fn nominal_mbps_1080p30(self) -> f64 {
|
||||
match self {
|
||||
Profile::Proxy => 45.0,
|
||||
Profile::Lt => 102.0,
|
||||
Profile::Standard => 147.0,
|
||||
Profile::Hq => 220.0,
|
||||
Profile::P4444 => 330.0,
|
||||
Profile::P4444Xq => 500.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Nominal coded bits per 16×16 macroblock (the nominal rate spread over the 8160
|
||||
/// macroblocks of a 1920×1080 frame at 30000/1001 fps). Frame targets scale with area.
|
||||
pub fn nominal_bits_per_mb(self) -> u32 {
|
||||
(self.nominal_mbps_1080p30() * 1e6 * 1001.0 / 30000.0 / 8160.0).round() as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// A decoded (or to-be-encoded) picture: planar `u16` samples, one sample per element,
|
||||
/// rows packed without padding.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Frame {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub chroma: ChromaFormat,
|
||||
/// Significant bits per sample of every plane (including alpha).
|
||||
pub bit_depth: u8,
|
||||
/// `width × height`
|
||||
pub y: Vec<u16>,
|
||||
/// `chroma_width × height`
|
||||
pub cb: Vec<u16>,
|
||||
/// `chroma_width × height`
|
||||
pub cr: Vec<u16>,
|
||||
/// `width × height` (full range: 0 = transparent, max = opaque)
|
||||
pub alpha: Option<Vec<u16>>,
|
||||
pub interlace: Interlace,
|
||||
pub color: ColorInfo,
|
||||
/// `aspect_ratio_information` from the frame header.
|
||||
pub aspect_ratio: u8,
|
||||
/// `frame_rate_code` from the frame header.
|
||||
pub frame_rate_code: u8,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
/// A black (video-range) frame with the given geometry.
|
||||
pub fn new(width: u32, height: u32, chroma: ChromaFormat, bit_depth: u8, with_alpha: bool) -> Frame {
|
||||
let n = width as usize * height as usize;
|
||||
let cn = chroma.chroma_width(width) as usize * height as usize;
|
||||
let s = bit_depth.saturating_sub(8) as u32;
|
||||
Frame {
|
||||
width,
|
||||
height,
|
||||
chroma,
|
||||
bit_depth,
|
||||
y: vec![(16u32 << s) as u16; n],
|
||||
cb: vec![(128u32 << s) as u16; cn],
|
||||
cr: vec![(128u32 << s) as u16; cn],
|
||||
alpha: with_alpha.then(|| vec![((1u32 << bit_depth) - 1) as u16; n]),
|
||||
interlace: Interlace::Progressive,
|
||||
color: ColorInfo::default(),
|
||||
aspect_ratio: 0,
|
||||
frame_rate_code: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chroma_width(&self) -> u32 {
|
||||
self.chroma.chroma_width(self.width)
|
||||
}
|
||||
|
||||
pub fn is_interlaced(&self) -> bool {
|
||||
self.interlace != Interlace::Progressive
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
//! Constant tables defined by the ProRes bitstream format (SMPTE RDD 36).
|
||||
|
||||
/// A ProRes variable-length codebook: a Golomb-Rice code for small values that switches to an
|
||||
/// Exp-Golomb code for large ones.
|
||||
///
|
||||
/// A codeword starts with `q` zero bits and a one bit.
|
||||
/// - If `q <= rice_q_max` it is a Rice codeword: `value = (q << rice) | next(rice bits)`.
|
||||
/// - Otherwise it is an Exp-Golomb codeword of order `exp` whose prefix is `q - rice_q_max - 1`
|
||||
/// zeros, offset by the number of values the Rice part covers: `(rice_q_max + 1) << rice`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) struct Codebook {
|
||||
pub rice: u8,
|
||||
pub exp: u8,
|
||||
pub rice_q_max: u8,
|
||||
}
|
||||
|
||||
const fn cb(rice: u8, exp: u8, rice_q_max: u8) -> Codebook {
|
||||
Codebook { rice, exp, rice_q_max }
|
||||
}
|
||||
|
||||
/// Codebook of the first DC coefficient in a slice component (equivalent to Exp-Golomb order 5).
|
||||
pub(crate) const FIRST_DC_CB: Codebook = cb(5, 6, 0);
|
||||
|
||||
/// DC difference codebooks, selected by the previous DC codeword value (clamped to 6).
|
||||
pub(crate) const DC_CB: [Codebook; 7] = [cb(0, 1, 0), cb(1, 2, 0), cb(1, 2, 0), cb(2, 3, 1), cb(2, 3, 1), cb(3, 4, 0), cb(3, 4, 0)];
|
||||
|
||||
/// AC run codebooks, selected by the previous run (clamped to 15).
|
||||
pub(crate) const RUN_CB: [Codebook; 16] = [
|
||||
cb(0, 1, 2),
|
||||
cb(0, 1, 2),
|
||||
cb(0, 1, 1),
|
||||
cb(0, 1, 1),
|
||||
cb(0, 1, 0),
|
||||
cb(1, 2, 1),
|
||||
cb(1, 2, 1),
|
||||
cb(1, 2, 1),
|
||||
cb(1, 2, 1),
|
||||
cb(1, 2, 0),
|
||||
cb(1, 2, 0),
|
||||
cb(1, 2, 0),
|
||||
cb(1, 2, 0),
|
||||
cb(1, 2, 0),
|
||||
cb(1, 2, 0),
|
||||
cb(2, 3, 0),
|
||||
];
|
||||
|
||||
/// AC level codebooks (level magnitude minus one), selected by the previous level magnitude (clamped to 9).
|
||||
pub(crate) const LEVEL_CB: [Codebook; 10] =
|
||||
[cb(0, 1, 0), cb(0, 2, 2), cb(0, 1, 1), cb(0, 1, 2), cb(0, 1, 0), cb(1, 2, 0), cb(1, 2, 0), cb(1, 2, 0), cb(1, 2, 0), cb(2, 3, 0)];
|
||||
|
||||
/// Initial DC codebook context for the second DC of a component.
|
||||
pub(crate) const DC_CTX_INIT: u32 = 5;
|
||||
/// Initial AC run / level codebook contexts.
|
||||
pub(crate) const RUN_CTX_INIT: u32 = 4;
|
||||
pub(crate) const LEVEL_CTX_INIT: u32 = 2;
|
||||
|
||||
/// Progressive-frame coefficient scan: scan position → raster index within the 8×8 block.
|
||||
pub(crate) const SCAN_PROGRESSIVE: [u8; 64] = [
|
||||
0, 1, 8, 9, 2, 3, 10, 11, //
|
||||
16, 17, 24, 25, 18, 19, 26, 27, //
|
||||
4, 5, 12, 20, 13, 6, 7, 14, //
|
||||
21, 28, 29, 22, 15, 23, 30, 31, //
|
||||
32, 33, 40, 48, 41, 34, 35, 42, //
|
||||
49, 56, 57, 50, 43, 36, 37, 44, //
|
||||
51, 58, 59, 52, 45, 38, 39, 46, //
|
||||
53, 60, 61, 54, 47, 55, 62, 63,
|
||||
];
|
||||
|
||||
/// Interlaced-field coefficient scan: scan position → raster index within the 8×8 block.
|
||||
pub(crate) const SCAN_INTERLACED: [u8; 64] = [
|
||||
0, 8, 1, 9, 16, 24, 17, 25, //
|
||||
2, 10, 3, 11, 18, 26, 19, 27, //
|
||||
32, 40, 33, 34, 41, 48, 56, 49, //
|
||||
42, 35, 43, 50, 57, 58, 51, 59, //
|
||||
4, 12, 5, 6, 13, 20, 28, 21, //
|
||||
14, 7, 15, 22, 29, 36, 44, 37, //
|
||||
30, 23, 31, 38, 45, 52, 60, 53, //
|
||||
46, 39, 47, 54, 61, 62, 55, 63,
|
||||
];
|
||||
|
||||
/// Quantisation scale for a slice `quantization_index` (1..=224).
|
||||
#[inline]
|
||||
pub(crate) fn qscale(qidx: u8) -> u32 {
|
||||
let q = qidx.max(1) as u32;
|
||||
if q <= 128 { q } else { 128 + ((q - 128) << 2) }
|
||||
}
|
||||
|
||||
/// Orthonormal 1-D DCT-II basis: `BASIS[u][x] = C(u)/2 · cos((2x+1)uπ/16)`, `C(0) = 1/√2`.
|
||||
pub(crate) const BASIS: [[f32; 8]; 8] = [
|
||||
[0.353_553_39, 0.353_553_39, 0.353_553_39, 0.353_553_39, 0.353_553_39, 0.353_553_39, 0.353_553_39, 0.353_553_39],
|
||||
[0.490_392_64, 0.415_734_8, 0.277_785_12, 0.097_545_16, -0.097_545_16, -0.277_785_12, -0.415_734_8, -0.490_392_64],
|
||||
[0.461_939_77, 0.191_341_72, -0.191_341_72, -0.461_939_77, -0.461_939_77, -0.191_341_72, 0.191_341_72, 0.461_939_77],
|
||||
[0.415_734_8, -0.097_545_16, -0.490_392_64, -0.277_785_12, 0.277_785_12, 0.490_392_64, 0.097_545_16, -0.415_734_8],
|
||||
[0.353_553_39, -0.353_553_39, -0.353_553_39, 0.353_553_39, 0.353_553_39, -0.353_553_39, -0.353_553_39, 0.353_553_39],
|
||||
[0.277_785_12, -0.490_392_64, 0.097_545_16, 0.415_734_8, -0.415_734_8, -0.097_545_16, 0.490_392_64, -0.277_785_12],
|
||||
[0.191_341_72, -0.461_939_77, 0.461_939_77, -0.191_341_72, -0.191_341_72, 0.461_939_77, -0.461_939_77, 0.191_341_72],
|
||||
[0.097_545_16, -0.277_785_12, 0.415_734_8, -0.490_392_64, 0.490_392_64, -0.415_734_8, 0.277_785_12, -0.097_545_16],
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn scans_are_permutations() {
|
||||
for scan in [&SCAN_PROGRESSIVE, &SCAN_INTERLACED] {
|
||||
let mut seen = [false; 64];
|
||||
for &i in scan.iter() {
|
||||
assert!(!seen[i as usize]);
|
||||
seen[i as usize] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basis_matches_cosines() {
|
||||
for u in 0..8 {
|
||||
for x in 0..8 {
|
||||
let c = if u == 0 { (0.125f64).sqrt() } else { 0.5 };
|
||||
let v = c * (((2 * x + 1) * u) as f64 * std::f64::consts::PI / 16.0).cos();
|
||||
assert!((v - BASIS[u][x] as f64).abs() < 1e-7);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qscale_mapping() {
|
||||
assert_eq!(qscale(1), 1);
|
||||
assert_eq!(qscale(128), 128);
|
||||
assert_eq!(qscale(129), 132);
|
||||
assert_eq!(qscale(224), 512);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
//! Shared helpers for oracle tests. ffmpeg is used only as an external fixture generator and
|
||||
//! reference decoder (never linked).
|
||||
#![allow(dead_code)]
|
||||
|
||||
use filmcraft_prores::{ChromaFormat, Frame};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
pub fn ffmpeg() -> Option<PathBuf> {
|
||||
for dir in ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin"] {
|
||||
let p = Path::new(dir).join("ffmpeg");
|
||||
if p.exists() {
|
||||
return Some(p);
|
||||
}
|
||||
}
|
||||
let ok = Command::new("ffmpeg").arg("-version").output().map(|o| o.status.success()).unwrap_or(false);
|
||||
if !ok {
|
||||
eprintln!("ffmpeg not found; skipping oracle test");
|
||||
}
|
||||
ok.then(|| PathBuf::from("ffmpeg"))
|
||||
}
|
||||
|
||||
pub fn fixture_dir() -> PathBuf {
|
||||
let d = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/fixtures/prores");
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn run(ff: &Path, args: &[&str]) {
|
||||
let out = Command::new(ff).args(["-hide_banner", "-loglevel", "error", "-y"]).args(args).output().expect("run ffmpeg");
|
||||
assert!(out.status.success(), "ffmpeg {:?} failed: {}", args, String::from_utf8_lossy(&out.stderr));
|
||||
}
|
||||
|
||||
/// A ProRes fixture: a lavfi source encoded by an ffmpeg ProRes encoder.
|
||||
pub struct Spec {
|
||||
pub name: &'static str,
|
||||
/// lavfi graph (source plus filters, including the input pixel format).
|
||||
pub lavfi: String,
|
||||
pub frames: u32,
|
||||
/// Encoder arguments (codec, profile, flags...).
|
||||
pub enc: Vec<&'static str>,
|
||||
}
|
||||
|
||||
impl Spec {
|
||||
pub fn new(name: &'static str, lavfi: impl Into<String>, frames: u32, enc: &[&'static str]) -> Spec {
|
||||
Spec { name, lavfi: lavfi.into(), frames, enc: enc.to_vec() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate (or reuse) the fixture MOV.
|
||||
pub fn make(ff: &Path, spec: &Spec) -> PathBuf {
|
||||
let path = fixture_dir().join(format!("{}.mov", spec.name));
|
||||
if !path.exists() {
|
||||
let tmp = fixture_dir().join(format!("{}.tmp.mov", spec.name));
|
||||
let frames = spec.frames.to_string();
|
||||
let mut args = vec!["-f", "lavfi", "-i", spec.lavfi.as_str(), "-frames:v", frames.as_str()];
|
||||
args.extend(spec.enc.iter().copied());
|
||||
let t = tmp.to_str().unwrap().to_string();
|
||||
args.push(&t);
|
||||
run(ff, &args);
|
||||
std::fs::rename(&tmp, &path).unwrap();
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// Extract the coded ProRes frames of a MOV (ffmpeg's `data` muxer concatenates packets; ProRes
|
||||
/// frames are self-delimiting through their leading 32-bit size).
|
||||
pub fn packets(ff: &Path, mov: &Path) -> Vec<Vec<u8>> {
|
||||
let out = Command::new(ff)
|
||||
.args(["-hide_banner", "-loglevel", "error", "-i"])
|
||||
.arg(mov)
|
||||
.args(["-map", "0:v", "-c", "copy", "-f", "data", "-"])
|
||||
.output()
|
||||
.expect("run ffmpeg");
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
split_frames(&out.stdout)
|
||||
}
|
||||
|
||||
pub fn split_frames(mut d: &[u8]) -> Vec<Vec<u8>> {
|
||||
let mut v = Vec::new();
|
||||
while d.len() >= 8 {
|
||||
let n = u32::from_be_bytes([d[0], d[1], d[2], d[3]]) as usize;
|
||||
assert!(n >= 8 && n <= d.len(), "bad frame size");
|
||||
v.push(d[..n].to_vec());
|
||||
d = &d[n..];
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
/// Reference-decode a MOV with ffmpeg to raw planar little-endian 16-bit samples.
|
||||
pub fn reference(ff: &Path, mov: &Path, pix_fmt: &str) -> Vec<u16> {
|
||||
let out = Command::new(ff)
|
||||
.args(["-hide_banner", "-loglevel", "error", "-i"])
|
||||
.arg(mov)
|
||||
.args(["-f", "rawvideo", "-pix_fmt", pix_fmt, "-"])
|
||||
.output()
|
||||
.expect("run ffmpeg");
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
out.stdout.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect()
|
||||
}
|
||||
|
||||
/// Number of u16 samples per frame of a raw planar image.
|
||||
pub fn frame_samples(w: usize, h: usize, chroma: ChromaFormat, alpha: bool) -> usize {
|
||||
let cw = if chroma == ChromaFormat::Yuv422 { w.div_ceil(2) } else { w };
|
||||
w * h * (1 + alpha as usize) + 2 * cw * h
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
pub struct Stats {
|
||||
pub max: u32,
|
||||
pub sum: u64,
|
||||
pub n: u64,
|
||||
pub exact: u64,
|
||||
}
|
||||
|
||||
impl Stats {
|
||||
pub fn add(&mut self, a: &[u16], b: &[u16]) {
|
||||
assert_eq!(a.len(), b.len());
|
||||
for (&x, &y) in a.iter().zip(b) {
|
||||
let d = (x as i32 - y as i32).unsigned_abs();
|
||||
self.max = self.max.max(d);
|
||||
self.sum += d as u64;
|
||||
self.n += 1;
|
||||
self.exact += (d == 0) as u64;
|
||||
}
|
||||
}
|
||||
pub fn merge(&mut self, o: &Stats) {
|
||||
self.max = self.max.max(o.max);
|
||||
self.sum += o.sum;
|
||||
self.n += o.n;
|
||||
self.exact += o.exact;
|
||||
}
|
||||
pub fn mean(&self) -> f64 {
|
||||
self.sum as f64 / self.n.max(1) as f64
|
||||
}
|
||||
pub fn exact_pct(&self) -> f64 {
|
||||
100.0 * self.exact as f64 / self.n.max(1) as f64
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare a decoded frame against a raw reference frame (planes in Y, Cb, Cr, A order).
|
||||
pub fn compare(f: &Frame, raw: &[u16]) -> (Stats, Stats) {
|
||||
let n = f.y.len();
|
||||
let c = f.cb.len();
|
||||
let mut luma = Stats::default();
|
||||
let mut rest = Stats::default();
|
||||
luma.add(&f.y, &raw[..n]);
|
||||
rest.add(&f.cb, &raw[n..n + c]);
|
||||
rest.add(&f.cr, &raw[n + c..n + 2 * c]);
|
||||
if let Some(a) = &f.alpha {
|
||||
rest.add(a, &raw[n + 2 * c..n + 2 * c + n]);
|
||||
}
|
||||
(luma, rest)
|
||||
}
|
||||
|
||||
/// PSNR over given peak.
|
||||
pub fn psnr(a: &[u16], b: &[u16], peak: f64) -> f64 {
|
||||
let mse: f64 = a.iter().zip(b).map(|(&x, &y)| (x as f64 - y as f64).powi(2)).sum::<f64>() / a.len() as f64;
|
||||
if mse == 0.0 { 99.0 } else { 10.0 * (peak * peak / mse).log10() }
|
||||
}
|
||||
|
||||
fn bx(kind: &[u8; 4], payload: &[u8]) -> Vec<u8> {
|
||||
let mut v = Vec::with_capacity(payload.len() + 8);
|
||||
v.extend_from_slice(&((payload.len() + 8) as u32).to_be_bytes());
|
||||
v.extend_from_slice(kind);
|
||||
v.extend_from_slice(payload);
|
||||
v
|
||||
}
|
||||
|
||||
const MATRIX: [u32; 9] = [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x4000_0000];
|
||||
|
||||
/// Minimal QuickTime writer (test-only): one ProRes video track, all samples in one chunk.
|
||||
pub fn write_mov(path: &Path, fourcc: &[u8; 4], width: u16, height: u16, depth: u16, frames: &[Vec<u8>]) {
|
||||
let n = frames.len() as u32;
|
||||
let be32 = |v: u32| v.to_be_bytes();
|
||||
let ftyp = bx(b"ftyp", &[b"qt ".as_slice(), &be32(0x200), b"qt "].concat());
|
||||
let mdat_payload: Vec<u8> = frames.concat();
|
||||
let data_off = ftyp.len() as u32 + 8;
|
||||
let mdat = bx(b"mdat", &mdat_payload);
|
||||
let matrix: Vec<u8> = MATRIX.iter().flat_map(|v| v.to_be_bytes()).collect();
|
||||
|
||||
let mut mvhd = vec![0u8; 4 + 8];
|
||||
mvhd.extend_from_slice(&be32(25));
|
||||
mvhd.extend_from_slice(&be32(n));
|
||||
mvhd.extend_from_slice(&be32(0x10000));
|
||||
mvhd.extend_from_slice(&[1, 0]);
|
||||
mvhd.extend_from_slice(&[0; 10]);
|
||||
mvhd.extend_from_slice(&matrix);
|
||||
mvhd.extend_from_slice(&[0; 24]);
|
||||
mvhd.extend_from_slice(&be32(2));
|
||||
|
||||
let mut tkhd = vec![0, 0, 0, 0xf];
|
||||
tkhd.extend_from_slice(&[0; 8]);
|
||||
tkhd.extend_from_slice(&be32(1));
|
||||
tkhd.extend_from_slice(&[0; 4]);
|
||||
tkhd.extend_from_slice(&be32(n));
|
||||
tkhd.extend_from_slice(&[0; 8]);
|
||||
tkhd.extend_from_slice(&[0; 8]); // layer, alternate group, volume, reserved
|
||||
tkhd.extend_from_slice(&matrix);
|
||||
tkhd.extend_from_slice(&be32((width as u32) << 16));
|
||||
tkhd.extend_from_slice(&be32((height as u32) << 16));
|
||||
|
||||
let mut mdhd = vec![0u8; 4 + 8];
|
||||
mdhd.extend_from_slice(&be32(25));
|
||||
mdhd.extend_from_slice(&be32(n));
|
||||
mdhd.extend_from_slice(&[0; 4]);
|
||||
let hdlr = [&[0u8; 4][..], b"mhlr", b"vide", &[0; 12], &[0]].concat();
|
||||
let vmhd = [0u8, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
let dref = bx(b"dref", &[&[0u8; 4][..], &be32(1), &bx(b"alis", &[0, 0, 0, 1])].concat());
|
||||
let dinf = bx(b"dinf", &dref);
|
||||
|
||||
let mut entry = vec![0u8; 6];
|
||||
entry.extend_from_slice(&[0, 1]);
|
||||
entry.extend_from_slice(&[0; 4]); // version, revision
|
||||
entry.extend_from_slice(b"fmcr");
|
||||
entry.extend_from_slice(&be32(0));
|
||||
entry.extend_from_slice(&be32(0x400));
|
||||
entry.extend_from_slice(&width.to_be_bytes());
|
||||
entry.extend_from_slice(&height.to_be_bytes());
|
||||
entry.extend_from_slice(&be32(72 << 16));
|
||||
entry.extend_from_slice(&be32(72 << 16));
|
||||
entry.extend_from_slice(&be32(0));
|
||||
entry.extend_from_slice(&[0, 1]);
|
||||
entry.extend_from_slice(&[0; 32]);
|
||||
entry.extend_from_slice(&depth.to_be_bytes());
|
||||
entry.extend_from_slice(&[0xff, 0xff]);
|
||||
let stsd = bx(b"stsd", &[&[0u8; 4][..], &be32(1), &bx(fourcc, &entry)].concat());
|
||||
let stts = bx(b"stts", &[&[0u8; 4][..], &be32(1), &be32(n), &be32(1)].concat());
|
||||
let stsc = bx(b"stsc", &[&[0u8; 4][..], &be32(1), &be32(1), &be32(n), &be32(1)].concat());
|
||||
let mut stsz = vec![0u8; 4];
|
||||
stsz.extend_from_slice(&be32(0));
|
||||
stsz.extend_from_slice(&be32(n));
|
||||
for f in frames {
|
||||
stsz.extend_from_slice(&be32(f.len() as u32));
|
||||
}
|
||||
let stsz = bx(b"stsz", &stsz);
|
||||
let stco = bx(b"stco", &[&[0u8; 4][..], &be32(1), &be32(data_off)].concat());
|
||||
let stbl = bx(b"stbl", &[stsd, stts, stsc, stsz, stco].concat());
|
||||
let minf = bx(b"minf", &[bx(b"vmhd", &vmhd), dinf, stbl].concat());
|
||||
let mdia = bx(b"mdia", &[bx(b"mdhd", &mdhd), bx(b"hdlr", &hdlr), minf].concat());
|
||||
let trak = bx(b"trak", &[bx(b"tkhd", &tkhd), mdia].concat());
|
||||
let moov = bx(b"moov", &[bx(b"mvhd", &mvhd), trak].concat());
|
||||
std::fs::write(path, [ftyp, mdat, moov].concat()).unwrap();
|
||||
}
|
||||
|
||||
/// Decode a MOV with ffmpeg (failing on any decode error); returns (raw samples, stderr text).
|
||||
pub fn ffmpeg_decode(ff: &Path, mov: &Path, pix_fmt: &str) -> (Vec<u16>, String) {
|
||||
let out = Command::new(ff)
|
||||
.args(["-hide_banner", "-loglevel", "error", "-xerror", "-i"])
|
||||
.arg(mov)
|
||||
.args(["-f", "rawvideo", "-pix_fmt", pix_fmt, "-"])
|
||||
.output()
|
||||
.expect("run ffmpeg");
|
||||
let err = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert!(out.status.success(), "ffmpeg failed: {err}");
|
||||
(out.stdout.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect(), err)
|
||||
}
|
||||
|
||||
/// Render a lavfi source to raw 10-bit planar frames with ffmpeg and load them as [`Frame`]s.
|
||||
pub fn source_frames(ff: &Path, name: &str, lavfi: &str, frames: u32, w: u32, h: u32, chroma: ChromaFormat, alpha: bool) -> Vec<Frame> {
|
||||
let path = fixture_dir().join(format!("{name}.raw"));
|
||||
if !path.exists() {
|
||||
let pix = match (chroma, alpha) {
|
||||
(ChromaFormat::Yuv422, _) => "yuv422p10le",
|
||||
(ChromaFormat::Yuv444, false) => "yuv444p10le",
|
||||
(ChromaFormat::Yuv444, true) => "yuva444p10le",
|
||||
};
|
||||
let tmp = fixture_dir().join(format!("{name}.tmp.raw"));
|
||||
let f = frames.to_string();
|
||||
run(ff, &["-f", "lavfi", "-i", lavfi, "-frames:v", &f, "-f", "rawvideo", "-pix_fmt", pix, tmp.to_str().unwrap()]);
|
||||
std::fs::rename(&tmp, &path).unwrap();
|
||||
}
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
let s: Vec<u16> = bytes.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect();
|
||||
let (w, h) = (w as usize, h as usize);
|
||||
let per = frame_samples(w, h, chroma, alpha);
|
||||
assert_eq!(s.len(), per * frames as usize);
|
||||
s.chunks_exact(per).map(|p| frame_from_raw(p, w as u32, h as u32, chroma, 10, alpha)).collect()
|
||||
}
|
||||
|
||||
pub fn frame_from_raw(p: &[u16], w: u32, h: u32, chroma: ChromaFormat, depth: u8, alpha: bool) -> Frame {
|
||||
let mut f = Frame::new(w, h, chroma, depth, alpha);
|
||||
let n = f.y.len();
|
||||
let c = f.cb.len();
|
||||
f.y.copy_from_slice(&p[..n]);
|
||||
f.cb.copy_from_slice(&p[n..n + c]);
|
||||
f.cr.copy_from_slice(&p[n + c..n + 2 * c]);
|
||||
if let Some(a) = f.alpha.as_mut() {
|
||||
a.copy_from_slice(&p[n + 2 * c..n + 2 * c + n]);
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
/// Frame planes concatenated (Y, Cb, Cr, A).
|
||||
pub fn planes(f: &Frame) -> Vec<u16> {
|
||||
[&f.y, &f.cb, &f.cr].into_iter().chain(f.alpha.as_ref()).flat_map(|p| p.iter().copied()).collect()
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
//! Robustness: mutated and random inputs must never panic (errors are fine).
|
||||
|
||||
mod common;
|
||||
use filmcraft_prores::*;
|
||||
|
||||
struct Rng(u64);
|
||||
impl Rng {
|
||||
fn next(&mut self) -> u64 {
|
||||
self.0 ^= self.0 << 13;
|
||||
self.0 ^= self.0 >> 7;
|
||||
self.0 ^= self.0 << 17;
|
||||
self.0
|
||||
}
|
||||
fn below(&mut self, n: usize) -> usize {
|
||||
(self.next() % n.max(1) as u64) as usize
|
||||
}
|
||||
}
|
||||
|
||||
fn seeds() -> Vec<Vec<u8>> {
|
||||
let mut v = Vec::new();
|
||||
for (p, w, h) in [(Profile::Proxy, 96u32, 48u32), (Profile::Hq, 70, 38), (Profile::P4444, 64, 32), (Profile::P4444Xq, 48, 20)] {
|
||||
let chroma = p.chroma();
|
||||
let mut f = Frame::new(w, h, chroma, 10, chroma == ChromaFormat::Yuv444);
|
||||
for (i, s) in f.y.iter_mut().enumerate() {
|
||||
*s = (64 + (i * 37) % 800) as u16;
|
||||
}
|
||||
if let Some(a) = f.alpha.as_mut() {
|
||||
for (i, s) in a.iter_mut().enumerate() {
|
||||
*s = if i % 5 == 0 { 1023 } else { (i % 1024) as u16 };
|
||||
}
|
||||
}
|
||||
let mut cfg = EncoderConfig::new(p, w, h);
|
||||
cfg.log2_slice_mbs = (w % 4) as u8;
|
||||
v.push(Encoder::with_config(cfg).encode(&f).unwrap());
|
||||
}
|
||||
// An interlaced ffmpeg stream, when fixtures from the oracle tests are present.
|
||||
if let Some(ff) = common::ffmpeg() {
|
||||
let spec = common::Spec::new(
|
||||
"fuzz_seed_interlaced",
|
||||
"testsrc2=s=64x48:r=25,setfield=tff,format=yuv422p10le",
|
||||
1,
|
||||
&["-c:v", "prores_ks", "-profile:v", "2", "-flags", "+ildct+ilme"],
|
||||
);
|
||||
let mov = common::make(&ff, &spec);
|
||||
v.extend(common::packets(&ff, &mov));
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutated_streams_never_panic() {
|
||||
let seeds = seeds();
|
||||
for s in &seeds {
|
||||
decode_frame(s).expect("seed decodes");
|
||||
}
|
||||
let mut rng = Rng(0x9e3779b97f4a7c15);
|
||||
let mut decoded_ok = 0;
|
||||
let iters = 3000;
|
||||
for i in 0..iters {
|
||||
let mut d = seeds[i % seeds.len()].clone();
|
||||
match rng.below(6) {
|
||||
0 => {
|
||||
for _ in 0..1 + rng.below(8) {
|
||||
let p = rng.below(d.len());
|
||||
d[p] ^= 1 << rng.below(8);
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
for _ in 0..1 + rng.below(4) {
|
||||
let p = rng.below(d.len());
|
||||
d[p] = rng.next() as u8;
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
let n = rng.below(d.len());
|
||||
d.truncate(n);
|
||||
if d.len() >= 4 && rng.below(2) == 0 {
|
||||
// keep the frame size field consistent with the truncation
|
||||
let n = d.len() as u32;
|
||||
d[..4].copy_from_slice(&n.to_be_bytes());
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
// header fields
|
||||
let p = 8 + rng.below(20.min(d.len() - 8));
|
||||
d[p] = rng.next() as u8;
|
||||
}
|
||||
4 => {
|
||||
// slice region garbage
|
||||
let start = 150.min(d.len() - 1) + rng.below(d.len().saturating_sub(150).max(1));
|
||||
for b in d.iter_mut().skip(start).take(1 + rng.below(64)) {
|
||||
*b = rng.next() as u8;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let p = rng.below(d.len());
|
||||
let extra: Vec<u8> = (0..1 + rng.below(16)).map(|_| rng.next() as u8).collect();
|
||||
d.splice(p..p, extra);
|
||||
}
|
||||
}
|
||||
for depth in [None, Some(8), Some(16)] {
|
||||
let opts = DecodeOptions { bit_depth: depth, threads: i % 2 == 0 };
|
||||
if let Ok(f) = decode_frame_with(&d, &opts) {
|
||||
decoded_ok += 1;
|
||||
assert_eq!(f.y.len(), f.width as usize * f.height as usize);
|
||||
}
|
||||
}
|
||||
let _ = probe(&d);
|
||||
}
|
||||
println!("{decoded_ok} of {} mutated decodes succeeded", iters * 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn random_bytes_never_panic() {
|
||||
let mut rng = Rng(12345);
|
||||
for _ in 0..2000 {
|
||||
let n = rng.below(400);
|
||||
let mut d: Vec<u8> = (0..n).map(|_| rng.next() as u8).collect();
|
||||
if d.len() >= 8 && rng.below(2) == 0 {
|
||||
d[..4].copy_from_slice(&(n as u32).to_be_bytes());
|
||||
d[4..8].copy_from_slice(b"icpf");
|
||||
}
|
||||
let _ = decode_frame(&d);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
//! Decoder accuracy against ffmpeg's ProRes decoder (oracle only; skipped without ffmpeg).
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use filmcraft_prores::{AlphaType, ChromaFormat, DecodeOptions, Interlace, decode_frame, decode_frame_with, probe};
|
||||
|
||||
fn specs() -> Vec<Spec> {
|
||||
let p422 = "format=yuv422p10le";
|
||||
let p444 = "format=yuv444p10le";
|
||||
let alpha = "format=yuva444p10le,geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='mod(X*7+Y*3,1024)'";
|
||||
let ks = |p: &'static str| -> Vec<&'static str> { vec!["-c:v", "prores_ks", "-profile:v", p] };
|
||||
let aw = |p: &'static str| -> Vec<&'static str> { vec!["-c:v", "prores_aw", "-profile:v", p] };
|
||||
let mut v = Vec::new();
|
||||
for (i, p) in ["0", "1", "2", "3"].into_iter().enumerate() {
|
||||
let names = [
|
||||
["ks_proxy_testsrc2_1080", "ks_lt_testsrc2_1080", "ks_std_testsrc2_1080", "ks_hq_testsrc2_1080"],
|
||||
["ks_proxy_mandel_1918", "ks_lt_mandel_1918", "ks_std_mandel_1918", "ks_hq_mandel_1918"],
|
||||
["ks_proxy_bars_720", "ks_lt_bars_720", "ks_std_bars_720", "ks_hq_bars_720"],
|
||||
["aw_proxy_testsrc2_640", "aw_lt_testsrc2_640", "aw_std_testsrc2_640", "aw_hq_testsrc2_640"],
|
||||
];
|
||||
v.push(Spec::new(names[0][i], format!("testsrc2=s=1920x1080:r=25,{p422}"), 2, &ks(p)));
|
||||
v.push(Spec::new(names[1][i], format!("mandelbrot=s=1918x1080:r=25,{p422}"), 2, &ks(p)));
|
||||
v.push(Spec::new(names[2][i], format!("smptehdbars=s=1280x720:r=25,{p422}"), 1, &ks(p)));
|
||||
v.push(Spec::new(names[3][i], format!("testsrc2=s=640x360:r=25,{p422}"), 2, &aw(p)));
|
||||
}
|
||||
v.push(Spec::new("ks_hq_odd_70x38", format!("testsrc2=s=70x38:r=25,{p422}"), 2, &ks("3")));
|
||||
v.push(Spec::new("ks_hq_small_slices", format!("mandelbrot=s=352x288:r=25,{p422}"), 1, &[&ks("3")[..], &["-mbs_per_slice", "2"]].concat()));
|
||||
let il = ["-flags", "+ildct+ilme"];
|
||||
v.push(Spec::new("ks_hq_ntsc_tff", format!("testsrc2=s=720x486:r=30000/1001,setfield=tff,{p422}"), 2, &[&ks("3")[..], &il].concat()));
|
||||
v.push(Spec::new("ks_std_ntsc_bff", format!("mandelbrot=s=720x486:r=30000/1001,setfield=bff,{p422}"), 2, &[&ks("2")[..], &il].concat()));
|
||||
v.push(Spec::new("aw_hq_1080i", format!("testsrc2=s=1920x1080:r=30000/1001,setfield=tff,{p422}"), 1, &[&aw("3")[..], &il].concat()));
|
||||
v.push(Spec::new("ks_4444_testsrc2_1080", format!("testsrc2=s=1920x1080:r=25,{p444}"), 2, &ks("4")));
|
||||
v.push(Spec::new("ks_4444xq_mandel_1918", format!("mandelbrot=s=1918x1080:r=25,{p444}"), 1, &ks("5")));
|
||||
v.push(Spec::new("ks_4444_alpha16_640", format!("testsrc2=s=640x360:r=25,{alpha}"), 2, &[&ks("4")[..], &["-alpha_bits", "16"]].concat()));
|
||||
v.push(Spec::new("ks_4444_alpha8_640", format!("testsrc2=s=640x360:r=25,{alpha}"), 1, &[&ks("4")[..], &["-alpha_bits", "8"]].concat()));
|
||||
v.push(Spec::new("ks_4444xq_alpha_odd", format!("mandelbrot=s=333x201:r=25,{alpha}"), 1, &ks("5")));
|
||||
v.push(Spec::new("aw_4444_640", format!("testsrc2=s=640x360:r=25,{p444}"), 1, &aw("4")));
|
||||
v.push(Spec::new("aw_4444_alpha_640", format!("testsrc2=s=640x360:r=25,{alpha}"), 1, &aw("4")));
|
||||
v.push(Spec::new("ks_4444_ntsc_tff", format!("testsrc2=s=720x486:r=30000/1001,setfield=tff,{p444}"), 1, &[&ks("4")[..], &il].concat()));
|
||||
v
|
||||
}
|
||||
|
||||
fn native_pix_fmt(chroma: ChromaFormat, alpha: bool) -> &'static str {
|
||||
match (chroma, alpha) {
|
||||
(ChromaFormat::Yuv422, _) => "yuv422p10le",
|
||||
(ChromaFormat::Yuv444, false) => "yuv444p12le",
|
||||
(ChromaFormat::Yuv444, true) => "yuva444p12le",
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_matches_ffmpeg() {
|
||||
let Some(ff) = ffmpeg() else { return };
|
||||
let filter = std::env::var("PRORES_FIXTURE").ok();
|
||||
let mut report = vec![format!("{:<26} {:>9} {:>5} {:>8} {:>8} {:>8} {:>8}", "fixture", "format", "maxY", "meanY", "maxC/A", "meanC/A", "exact%")];
|
||||
let mut worst = 0;
|
||||
for spec in specs() {
|
||||
if let Some(f) = &filter
|
||||
&& !spec.name.contains(f.as_str())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mov = make(&ff, &spec);
|
||||
let pkts = packets(&ff, &mov);
|
||||
assert_eq!(pkts.len(), spec.frames as usize, "{}", spec.name);
|
||||
let hdr = probe(&pkts[0]).unwrap();
|
||||
let has_alpha = hdr.alpha != AlphaType::None;
|
||||
let fmt = native_pix_fmt(hdr.chroma, has_alpha);
|
||||
let raw = reference(&ff, &mov, fmt);
|
||||
let per = frame_samples(hdr.width as usize, hdr.height as usize, hdr.chroma, has_alpha);
|
||||
assert_eq!(raw.len(), per * pkts.len());
|
||||
let (mut sl, mut sc) = (Stats::default(), Stats::default());
|
||||
for (i, p) in pkts.iter().enumerate() {
|
||||
let f = decode_frame(p).unwrap_or_else(|e| panic!("{}: frame {i}: {e}", spec.name));
|
||||
if spec.enc.contains(&"+ildct+ilme") {
|
||||
assert_ne!(f.interlace, Interlace::Progressive);
|
||||
}
|
||||
let (l, c) = compare(&f, &raw[i * per..(i + 1) * per]);
|
||||
sl.merge(&l);
|
||||
sc.merge(&c);
|
||||
}
|
||||
let mut all = sl;
|
||||
all.merge(&sc);
|
||||
worst = worst.max(all.max);
|
||||
report.push(format!(
|
||||
"{:<26} {:>9} {:>5} {:>8.5} {:>8} {:>8.5} {:>8.3}",
|
||||
spec.name,
|
||||
&fmt[..fmt.len() - 2],
|
||||
sl.max,
|
||||
sl.mean(),
|
||||
sc.max,
|
||||
sc.mean(),
|
||||
all.exact_pct()
|
||||
));
|
||||
}
|
||||
let text = report.join("\n");
|
||||
println!("{text}");
|
||||
std::fs::write(fixture_dir().join("decode_report.txt"), &text).unwrap();
|
||||
assert!(worst <= 1, "decoder deviates from reference by up to {worst} LSB\n{text}");
|
||||
}
|
||||
|
||||
/// 10-bit output of 4444 streams stays within ±1 of ffmpeg's 12-bit output rounded to 10 bits.
|
||||
#[test]
|
||||
fn decode_4444_at_10_bits() {
|
||||
let Some(ff) = ffmpeg() else { return };
|
||||
let spec = Spec::new("ks_4444_alpha16_640", "", 2, &[]);
|
||||
let spec = specs().into_iter().find(|s| s.name == spec.name).unwrap();
|
||||
let mov = make(&ff, &spec);
|
||||
let pkts = packets(&ff, &mov);
|
||||
let raw12 = reference(&ff, &mov, "yuva444p12le");
|
||||
let per = frame_samples(640, 360, ChromaFormat::Yuv444, true);
|
||||
let f = decode_frame_with(&pkts[0], &DecodeOptions { bit_depth: Some(10), threads: true }).unwrap();
|
||||
assert_eq!(f.bit_depth, 10);
|
||||
let r10: Vec<u16> = raw12[..per].iter().map(|&v| ((v as u32 + 2) >> 2).min(1023) as u16).collect();
|
||||
let (l, c) = compare(&f, &r10);
|
||||
assert!(l.max <= 1 && c.max <= 1, "{l:?} {c:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_threaded_matches_threaded() {
|
||||
let Some(ff) = ffmpeg() else { return };
|
||||
let spec = specs().into_iter().find(|s| s.name == "ks_hq_ntsc_tff").unwrap();
|
||||
let mov = make(&ff, &spec);
|
||||
let pkts = packets(&ff, &mov);
|
||||
let a = decode_frame_with(&pkts[0], &DecodeOptions { bit_depth: None, threads: false }).unwrap();
|
||||
let b = decode_frame(&pkts[0]).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_metadata_is_reported() {
|
||||
let Some(ff) = ffmpeg() else { return };
|
||||
let spec = Spec::new(
|
||||
"ks_hq_bt2020_pq",
|
||||
"testsrc2=s=128x64:r=25,format=yuv422p10le,setparams=color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc",
|
||||
1,
|
||||
&["-c:v", "prores_ks", "-profile:v", "3"],
|
||||
);
|
||||
let mov = make(&ff, &spec);
|
||||
let f = decode_frame(&packets(&ff, &mov)[0]).unwrap();
|
||||
assert_eq!((f.color.primaries, f.color.transfer, f.color.matrix), (9, 16, 9));
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
//! Encoder conformance and quality against ffmpeg's ProRes decoder (oracle only).
|
||||
|
||||
mod common;
|
||||
use common::*;
|
||||
use filmcraft_prores::{ChromaFormat, DecodeOptions, Encoder, Frame, Profile, decode_frame, decode_frame_with};
|
||||
|
||||
struct Content {
|
||||
name: &'static str,
|
||||
lavfi: &'static str,
|
||||
/// Detailed enough that every profile's rate control is budget-bound.
|
||||
busy: bool,
|
||||
}
|
||||
|
||||
const W: u32 = 1920;
|
||||
const H: u32 = 1080;
|
||||
|
||||
fn contents(chroma: ChromaFormat) -> Vec<Content> {
|
||||
match chroma {
|
||||
ChromaFormat::Yuv422 => vec![
|
||||
Content { name: "src422_testsrc2", lavfi: "testsrc2=s=1920x1080:r=25", busy: false },
|
||||
Content { name: "src422_mandel", lavfi: "mandelbrot=s=1920x1080:r=25", busy: false },
|
||||
Content { name: "src422_noise", lavfi: "testsrc2=s=1920x1080:r=25,noise=alls=40:allf=t", busy: true },
|
||||
],
|
||||
ChromaFormat::Yuv444 => vec![
|
||||
Content {
|
||||
name: "src444a_testsrc2_disc",
|
||||
lavfi: "testsrc2=s=1920x1080:r=25,format=yuva444p10le,geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='1023*lt(hypot(X-W/2,Y-H/2),400)'",
|
||||
busy: false,
|
||||
},
|
||||
Content { name: "src444a_noise", lavfi: "testsrc2=s=1920x1080:r=25,noise=alls=40:allf=t,format=yuva444p10le", busy: true },
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
fn native_fmt(chroma: ChromaFormat, alpha: bool) -> &'static str {
|
||||
match (chroma, alpha) {
|
||||
(ChromaFormat::Yuv422, _) => "yuv422p10le",
|
||||
(ChromaFormat::Yuv444, false) => "yuv444p12le",
|
||||
(ChromaFormat::Yuv444, true) => "yuva444p12le",
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoder_output_decodes_in_ffmpeg() {
|
||||
let Some(ff) = ffmpeg() else { return };
|
||||
let mut report = vec![format!(
|
||||
"{:<9} {:<18} {:>9} {:>9} {:>6} {:>8} {:>8} {:>7} {:>7}",
|
||||
"profile", "content", "target", "avg", "ratio", "psnrY", "psnrC", "ffΔmax", "ffΔ=0%"
|
||||
)];
|
||||
let mut failures = Vec::new();
|
||||
for profile in Profile::ALL {
|
||||
let chroma = profile.chroma();
|
||||
for c in contents(chroma) {
|
||||
let alpha = chroma == ChromaFormat::Yuv444;
|
||||
let src = source_frames(&ff, c.name, c.lavfi, 2, W, H, chroma, alpha);
|
||||
let mut enc = Encoder::new(profile, W, H);
|
||||
let coded: Vec<Vec<u8>> = src.iter().map(|f| enc.encode(f).unwrap()).collect();
|
||||
let target = enc.target_frame_bytes() as f64;
|
||||
let avg = coded.iter().map(|c| c.len()).sum::<usize>() as f64 / coded.len() as f64;
|
||||
let mov = fixture_dir().join(format!("enc_{}_{}.mov", String::from_utf8_lossy(&profile.fourcc()), c.name));
|
||||
write_mov(&mov, &profile.fourcc(), W as u16, H as u16, if alpha { 32 } else { 24 }, &coded);
|
||||
let fmt = native_fmt(chroma, alpha);
|
||||
let (ffraw, err) = ffmpeg_decode(&ff, &mov, fmt);
|
||||
if !err.trim().is_empty() {
|
||||
failures.push(format!("{profile:?}/{}: ffmpeg reported: {err}", c.name));
|
||||
}
|
||||
let per = frame_samples(W as usize, H as usize, chroma, alpha);
|
||||
assert_eq!(ffraw.len(), per * coded.len(), "{profile:?}/{}", c.name);
|
||||
let (mut ffd, mut py, mut pc) = (Stats::default(), 0.0, 0.0);
|
||||
for (i, (cf, s)) in coded.iter().zip(&src).enumerate() {
|
||||
// Our decode of our stream agrees with ffmpeg's decode of it.
|
||||
let ours = decode_frame(cf).unwrap();
|
||||
let (l, cc) = compare(&ours, &ffraw[i * per..(i + 1) * per]);
|
||||
ffd.merge(&l);
|
||||
ffd.merge(&cc);
|
||||
// Quality against the 10-bit source.
|
||||
let d10 = decode_frame_with(cf, &DecodeOptions { bit_depth: Some(10), threads: true }).unwrap();
|
||||
py += psnr(&d10.y, &s.y, 1023.0) / coded.len() as f64;
|
||||
pc += psnr(&[d10.cb.clone(), d10.cr.clone()].concat(), &[s.cb.clone(), s.cr.clone()].concat(), 1023.0) / coded.len() as f64;
|
||||
if let (Some(a), Some(b)) = (&d10.alpha, &s.alpha) {
|
||||
assert_eq!(a, b, "alpha is lossless");
|
||||
}
|
||||
}
|
||||
let ratio = avg / target;
|
||||
report.push(format!(
|
||||
"{:<9} {:<18} {:>9.0} {:>9.0} {:>6.3} {:>8.2} {:>8.2} {:>7} {:>7.2}",
|
||||
format!("{profile:?}"),
|
||||
c.name,
|
||||
target,
|
||||
avg,
|
||||
ratio,
|
||||
py,
|
||||
pc,
|
||||
ffd.max,
|
||||
ffd.exact_pct()
|
||||
));
|
||||
if ffd.max > 1 {
|
||||
failures.push(format!("{profile:?}/{}: ffmpeg decode differs from ours by {}", c.name, ffd.max));
|
||||
}
|
||||
if ratio > 1.15 || (c.busy && ratio < 0.85) {
|
||||
failures.push(format!("{profile:?}/{}: frame size {avg:.0} vs target {target:.0}", c.name));
|
||||
}
|
||||
if profile == Profile::Hq && !c.busy && py.min(pc) < 45.0 {
|
||||
failures.push(format!("HQ/{}: PSNR {py:.2}/{pc:.2} dB below 45", c.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
let text = report.join("\n");
|
||||
println!("{text}");
|
||||
std::fs::write(fixture_dir().join("encode_report.txt"), &text).unwrap();
|
||||
assert!(failures.is_empty(), "{}\n{text}", failures.join("\n"));
|
||||
}
|
||||
|
||||
/// Odd sizes and small slices round-trip through ffmpeg too.
|
||||
#[test]
|
||||
fn odd_geometry_encodes() {
|
||||
let Some(ff) = ffmpeg() else { return };
|
||||
for (profile, w, h) in [(Profile::Standard, 1918u32, 1080u32), (Profile::Proxy, 70, 38), (Profile::P4444, 333, 201)] {
|
||||
let chroma = profile.chroma();
|
||||
let alpha = chroma == ChromaFormat::Yuv444;
|
||||
let lav = if alpha {
|
||||
format!("mandelbrot=s={w}x{h}:r=25,format=yuva444p10le,geq=lum='lum(X,Y)':cb='cb(X,Y)':cr='cr(X,Y)':a='mod(3*X+Y,1024)'")
|
||||
} else {
|
||||
format!("mandelbrot=s={w}x{h}:r=25")
|
||||
};
|
||||
let name = format!("src_odd_{w}x{h}");
|
||||
let src = source_frames(&ff, Box::leak(name.into_boxed_str()), &lav, 1, w, h, chroma, alpha);
|
||||
let mut enc = Encoder::new(profile, w, h);
|
||||
let coded = vec![enc.encode(&src[0]).unwrap()];
|
||||
let mov = fixture_dir().join(format!("enc_odd_{w}x{h}.mov"));
|
||||
write_mov(&mov, &profile.fourcc(), w as u16, h as u16, 24, &coded);
|
||||
let (ffraw, err) = ffmpeg_decode(&ff, &mov, native_fmt(chroma, alpha));
|
||||
assert!(err.trim().is_empty(), "{err}");
|
||||
let ours: Frame = decode_frame(&coded[0]).unwrap();
|
||||
let (l, c) = compare(&ours, &ffraw);
|
||||
assert!(l.max <= 1 && c.max <= 1, "{profile:?} {w}x{h}: {l:?} {c:?}");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
//! Decode/encode throughput. Run with
|
||||
//! `cargo test --release -p filmcraft-prores --test perf -- --ignored --nocapture`.
|
||||
|
||||
mod common;
|
||||
use filmcraft_prores::*;
|
||||
use std::time::Instant;
|
||||
|
||||
/// A 1080p frame whose HQ encoding uses the full nominal rate (detailed, noisy content).
|
||||
fn busy_1080p(chroma: ChromaFormat) -> Frame {
|
||||
let mut f = Frame::new(1920, 1080, chroma, 10, false);
|
||||
let mut s = 0x1234_5678u32;
|
||||
let mut rnd = move || {
|
||||
s ^= s << 13;
|
||||
s ^= s >> 17;
|
||||
s ^= s << 5;
|
||||
s
|
||||
};
|
||||
let cw = f.chroma_width() as usize;
|
||||
for y in 0..1080usize {
|
||||
for x in 0..1920usize {
|
||||
let base = 200 + ((x * 3 + y * 2) % 600) as u32;
|
||||
f.y[y * 1920 + x] = (base + rnd() % 48) as u16;
|
||||
}
|
||||
for x in 0..cw {
|
||||
f.cb[y * cw + x] = (480 + (x + y) as u32 % 64 + rnd() % 16) as u16;
|
||||
f.cr[y * cw + x] = (500 + (x * 7) as u32 % 50 + rnd() % 16) as u16;
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
fn fps(frame: &[u8], threads: bool, secs: f64) -> f64 {
|
||||
let opts = DecodeOptions { bit_depth: None, threads };
|
||||
// Reuse the output planes across frames, as a player would.
|
||||
let mut out = decode_frame_with(frame, &opts).unwrap();
|
||||
let t = Instant::now();
|
||||
let mut n = 0;
|
||||
while t.elapsed().as_secs_f64() < secs {
|
||||
decode_frame_into(frame, &opts, &mut out).unwrap();
|
||||
std::hint::black_box(&out);
|
||||
n += 1;
|
||||
}
|
||||
n as f64 / t.elapsed().as_secs_f64()
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn decode_1080p_fps() {
|
||||
let mut streams: Vec<(String, Vec<u8>)> = Vec::new();
|
||||
for p in [Profile::Hq, Profile::P4444] {
|
||||
let src = busy_1080p(p.chroma());
|
||||
let mut enc = Encoder::new(p, 1920, 1080);
|
||||
let t = Instant::now();
|
||||
let coded = enc.encode(&src).unwrap();
|
||||
println!("encode {p:?} 1080p: {:.1} ms, {} bytes", t.elapsed().as_secs_f64() * 1e3, coded.len());
|
||||
streams.push((format!("ours {p:?} (nominal rate)"), coded));
|
||||
}
|
||||
if let Some(ff) = common::ffmpeg() {
|
||||
let spec = common::Spec::new("perf_ks_hq_mandel_1080", "mandelbrot=s=1920x1080:r=25,format=yuv422p10le", 1, &["-c:v", "prores_ks", "-profile:v", "3"]);
|
||||
let mov = common::make(&ff, &spec);
|
||||
streams.push(("prores_ks HQ mandelbrot".into(), common::packets(&ff, &mov).remove(0)));
|
||||
}
|
||||
for (name, s) in &streams {
|
||||
let mt = fps(s, true, 2.0);
|
||||
let st = fps(s, false, 2.0);
|
||||
println!("{name:<32} {:>8} bytes: {mt:7.1} fps threaded, {st:6.1} fps single-threaded", s.len());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
//! Encoder → decoder round trips on synthetic content (no external tools needed).
|
||||
|
||||
mod common;
|
||||
use common::psnr;
|
||||
use filmcraft_prores::*;
|
||||
|
||||
/// Deterministic test picture: gradients, a sharp-edged grid, a disc and mild noise.
|
||||
pub fn synth(w: u32, h: u32, chroma: ChromaFormat, depth: u8, alpha: bool, seed: u32) -> Frame {
|
||||
let mut f = Frame::new(w, h, chroma, depth, alpha);
|
||||
let max = ((1u32 << depth) - 1) as f32;
|
||||
let mut s = seed.wrapping_mul(2654435761).wrapping_add(1);
|
||||
let mut rnd = move || {
|
||||
s ^= s << 13;
|
||||
s ^= s >> 17;
|
||||
s ^= s << 5;
|
||||
(s % 1000) as f32 / 1000.0 - 0.5
|
||||
};
|
||||
let cw = f.chroma_width() as usize;
|
||||
let (wu, hu) = (w as usize, h as usize);
|
||||
for y in 0..hu {
|
||||
for x in 0..wu {
|
||||
let fx = x as f32 / w as f32;
|
||||
let fy = y as f32 / h as f32;
|
||||
let grid = if (x / 24 + y / 24) % 2 == 0 { 0.15 } else { 0.0 };
|
||||
let disc = if (fx - 0.6).powi(2) + (fy - 0.4).powi(2) < 0.04 { 0.2 } else { 0.0 };
|
||||
let v = 0.1 + 0.5 * fx * (1.0 - fy) + grid + disc + 0.01 * rnd();
|
||||
f.y[y * wu + x] = (v.clamp(0.0, 1.0) * max) as u16;
|
||||
if let Some(a) = f.alpha.as_mut() {
|
||||
a[y * wu + x] = if disc > 0.0 { max as u16 } else { ((fx * max) as u16) & !7 };
|
||||
}
|
||||
}
|
||||
for x in 0..cw {
|
||||
let fx = x as f32 / cw as f32;
|
||||
f.cb[y * cw + x] = ((0.5 + 0.2 * (fx * 6.0).sin()) * max) as u16;
|
||||
f.cr[y * cw + x] = ((0.5 - 0.15 * (y as f32 / 40.0).cos()) * max) as u16;
|
||||
}
|
||||
}
|
||||
f
|
||||
}
|
||||
|
||||
fn quality(profile: Profile, w: u32, h: u32) -> (f64, f64, usize, usize) {
|
||||
let chroma = profile.chroma();
|
||||
let src = synth(w, h, chroma, 10, chroma == ChromaFormat::Yuv444, 7);
|
||||
let mut enc = Encoder::new(profile, w, h);
|
||||
let coded = enc.encode(&src).unwrap();
|
||||
let hdr = probe(&coded).unwrap();
|
||||
assert_eq!((hdr.width as u32, hdr.height as u32, hdr.chroma), (w, h, chroma));
|
||||
let dec = decode_frame_with(&coded, &DecodeOptions { bit_depth: Some(10), threads: true }).unwrap();
|
||||
assert_eq!(dec.alpha, src.alpha, "alpha is lossless");
|
||||
let py = psnr(&dec.y, &src.y, 1023.0);
|
||||
let pc = psnr(&[dec.cb, dec.cr].concat(), &[src.cb, src.cr].concat(), 1023.0);
|
||||
(py, pc, coded.len(), enc.target_frame_bytes())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_profile_round_trips() {
|
||||
let mut last = 0.0;
|
||||
for p in [Profile::Proxy, Profile::Lt, Profile::Standard, Profile::Hq] {
|
||||
let (py, pc, size, target) = quality(p, 640, 360);
|
||||
println!("{p:?}: Y {py:.2} dB, C {pc:.2} dB, {size} / {target} bytes");
|
||||
assert!(size as f64 <= target as f64 * 1.15);
|
||||
assert!(py >= last - 0.5, "quality should not drop with a higher profile");
|
||||
assert!(py > 38.0 && pc > 38.0);
|
||||
last = py;
|
||||
}
|
||||
let (py, pc, _, _) = quality(Profile::Hq, 640, 360);
|
||||
assert!(py >= 45.0 && pc >= 45.0);
|
||||
for p in [Profile::P4444, Profile::P4444Xq] {
|
||||
let (py, pc, size, target) = quality(p, 320, 240);
|
||||
println!("{p:?}: Y {py:.2} dB, C {pc:.2} dB, {size} / {target} bytes");
|
||||
assert!(py >= 45.0 && pc >= 45.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_sizes_and_slice_widths() {
|
||||
for (w, h) in [(1u32, 1u32), (17, 9), (70, 38), (1918, 34), (720, 486)] {
|
||||
for log2 in [0u8, 1, 3] {
|
||||
let src = synth(w, h, ChromaFormat::Yuv422, 10, false, w);
|
||||
let mut cfg = EncoderConfig::new(Profile::Hq, w, h);
|
||||
cfg.log2_slice_mbs = log2;
|
||||
let coded = Encoder::with_config(cfg).encode(&src).unwrap();
|
||||
let dec = decode_frame(&coded).unwrap();
|
||||
assert_eq!((dec.width, dec.height, dec.bit_depth), (w, h, 10));
|
||||
assert_eq!(dec.cb.len(), w.div_ceil(2) as usize * h as usize);
|
||||
assert!(psnr(&dec.y, &src.y, 1023.0) > 40.0, "{w}x{h} log2 {log2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twelve_bit_input_and_output() {
|
||||
let src = synth(256, 128, ChromaFormat::Yuv444, 12, true, 3);
|
||||
let coded = Encoder::new(Profile::P4444Xq, 256, 128).encode(&src).unwrap();
|
||||
let dec = decode_frame(&coded).unwrap();
|
||||
assert_eq!(dec.bit_depth, 12);
|
||||
assert!(psnr(&dec.y, &src.y, 4095.0) > 50.0);
|
||||
assert_eq!(dec.alpha, src.alpha);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_round_trips() {
|
||||
let src = synth(64, 32, ChromaFormat::Yuv422, 10, false, 1);
|
||||
let mut cfg = EncoderConfig::new(Profile::Standard, 64, 32);
|
||||
cfg.color = ColorInfo { primaries: 9, transfer: 16, matrix: 9 };
|
||||
cfg.aspect_ratio = 1;
|
||||
cfg.frame_rate_code = 3;
|
||||
let coded = Encoder::with_config(cfg).encode(&src).unwrap();
|
||||
let hdr = probe(&coded).unwrap();
|
||||
assert_eq!(&hdr.encoder_id, b"fmcr");
|
||||
let dec = decode_frame(&coded).unwrap();
|
||||
assert_eq!(dec.color, ColorInfo { primaries: 9, transfer: 16, matrix: 9 });
|
||||
assert_eq!((dec.aspect_ratio, dec.frame_rate_code, dec.interlace), (1, 3, Interlace::Progressive));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bad_input() {
|
||||
let mut enc = Encoder::new(Profile::Hq, 64, 32);
|
||||
assert!(enc.encode(&Frame::new(32, 32, ChromaFormat::Yuv422, 10, false)).is_err());
|
||||
assert!(enc.encode(&Frame::new(64, 32, ChromaFormat::Yuv444, 10, false)).is_err());
|
||||
let mut short = Frame::new(64, 32, ChromaFormat::Yuv422, 10, false);
|
||||
short.cb.truncate(10);
|
||||
assert!(enc.encode(&short).is_err());
|
||||
assert!(decode_frame(&[]).is_err());
|
||||
assert!(decode_frame(b"\0\0\0\x20icpfxxxxxxxxxxxxxxxxxxxxxxxx").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_identifiers() {
|
||||
for p in Profile::ALL {
|
||||
assert_eq!(Profile::from_fourcc(&p.fourcc()), Some(p));
|
||||
}
|
||||
assert_eq!(Profile::Hq.nominal_bits_per_mb(), 900);
|
||||
assert_eq!(Profile::Proxy.chroma(), ChromaFormat::Yuv422);
|
||||
assert_eq!(Profile::P4444Xq.chroma(), ChromaFormat::Yuv444);
|
||||
}
|
||||
Loading…
Reference in New Issue