Compare commits
43
Commits
No files matched your search
Generated
+45
-45
@@ -1655,7 +1655,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"cpal",
|
||||
@@ -1676,7 +1676,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-aac"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"filmcraft-testkit",
|
||||
@@ -1686,7 +1686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-ac3"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
"thiserror 2.0.21",
|
||||
@@ -1694,14 +1694,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-audio-dsp"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-automation"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"filmcraft-engine",
|
||||
@@ -1717,7 +1717,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-av1"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"filmcraft-testkit",
|
||||
@@ -1728,14 +1728,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-bitstream"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-captions"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-color",
|
||||
"filmcraft-project",
|
||||
@@ -1748,11 +1748,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-cfb"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-cli"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-automation",
|
||||
"filmcraft-codecs",
|
||||
@@ -1768,7 +1768,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-codecs"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-aac",
|
||||
"filmcraft-ac3",
|
||||
@@ -1800,7 +1800,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-color"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -1808,7 +1808,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-dnx"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"filmcraft-testkit",
|
||||
@@ -1818,7 +1818,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-edit"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-media",
|
||||
"filmcraft-project",
|
||||
@@ -1830,7 +1830,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-engine"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-audio-dsp",
|
||||
"filmcraft-captions",
|
||||
@@ -1861,7 +1861,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-export"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-aac",
|
||||
"filmcraft-audio-dsp",
|
||||
@@ -1890,7 +1890,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-format"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-media",
|
||||
"filmcraft-project",
|
||||
@@ -1902,7 +1902,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-frame"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-color",
|
||||
"filmcraft-time",
|
||||
@@ -1912,7 +1912,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-geom"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -1920,7 +1920,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-golden"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-color",
|
||||
"filmcraft-frame",
|
||||
@@ -1937,7 +1937,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-gpu"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-color",
|
||||
"filmcraft-frame",
|
||||
@@ -1954,7 +1954,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-h264"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"filmcraft-testkit",
|
||||
@@ -1964,7 +1964,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-h264enc"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"filmcraft-testkit",
|
||||
@@ -1974,7 +1974,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-hevc"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"filmcraft-testkit",
|
||||
@@ -1984,7 +1984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-interchange"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-cfb",
|
||||
"filmcraft-color",
|
||||
@@ -2001,7 +2001,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-isobmff"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"filmcraft-testkit",
|
||||
@@ -2012,7 +2012,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-matroska"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
"serde_json",
|
||||
@@ -2020,7 +2020,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-media"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-color",
|
||||
"filmcraft-frame",
|
||||
@@ -2033,7 +2033,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-mpeg2v"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
"rayon",
|
||||
@@ -2042,7 +2042,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-mpegts"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
"thiserror 2.0.21",
|
||||
@@ -2050,28 +2050,28 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-mxf"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-ogg"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-opus"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-project"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-color",
|
||||
"filmcraft-geom",
|
||||
@@ -2085,7 +2085,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-prores"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"filmcraft-testkit",
|
||||
@@ -2095,7 +2095,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-render"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-audio-dsp",
|
||||
"filmcraft-captions",
|
||||
@@ -2115,7 +2115,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-scopes"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-color",
|
||||
"serde",
|
||||
@@ -2124,7 +2124,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-speech"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"candle-core",
|
||||
"candle-nn",
|
||||
@@ -2142,14 +2142,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-testkit"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-text"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-color",
|
||||
"harfrust",
|
||||
@@ -2160,7 +2160,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-time"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"proptest",
|
||||
"serde",
|
||||
@@ -2168,7 +2168,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-ui-egui"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -2200,7 +2200,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-vp9"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-bitstream",
|
||||
"rayon",
|
||||
@@ -2209,7 +2209,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filmcraft-web"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -7140,7 +7140,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"filmcraft-testkit",
|
||||
"serde_json",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/*", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -88,7 +88,9 @@ fn insert_dotted(m: &mut Map<String, Value>, key: &str, v: Value) {
|
||||
if !e.is_object() {
|
||||
*e = Value::Object(Map::new());
|
||||
}
|
||||
insert_dotted(e.as_object_mut().expect("object"), tail, v);
|
||||
if let Value::Object(child) = e {
|
||||
insert_dotted(child, tail, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//!
|
||||
//! Run `filmcraft-cli help` for the full reference (also in docs/agents.md).
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod args;
|
||||
mod probe;
|
||||
|
||||
@@ -339,7 +341,7 @@ async fn main() {
|
||||
if a.opt("--bridge").is_some() {
|
||||
usage("render is headless; with --bridge use the MCP `render_frame` tool or `exec` ui commands");
|
||||
}
|
||||
let Backend::Local(mut s) = Backend::open(&a) else { unreachable!() };
|
||||
let Backend::Local(mut s) = Backend::open(&a) else { usage("render is headless") };
|
||||
let secs: f64 = a.opt("--seconds").and_then(|v| v.parse().ok()).unwrap_or(0.0);
|
||||
let scale: f32 = a.opt("--scale").and_then(|v| v.parse().ok()).unwrap_or(1.0);
|
||||
let out = a.opt("--out").unwrap_or("frame.png");
|
||||
@@ -356,7 +358,7 @@ async fn main() {
|
||||
Some(addr) => filmcraft_automation::FilmcraftMcp::bridge(addr).unwrap_or_else(|e| fail(e)),
|
||||
None => match Backend::open(&a) {
|
||||
Backend::Local(s) => filmcraft_automation::FilmcraftMcp::headless(*s),
|
||||
Backend::Bridge(_) => unreachable!(),
|
||||
Backend::Bridge(_) => usage("mcp: use --bridge ADDR to drive a running app"),
|
||||
},
|
||||
};
|
||||
if let Err(e) = server.serve_stdio().await {
|
||||
|
||||
@@ -289,20 +289,25 @@ impl ByteReader for BlobReader {
|
||||
}
|
||||
return Err(filmcraft_media::pending::would_block());
|
||||
}
|
||||
FS.with(|fs| {
|
||||
let complete = FS.with(|fs| {
|
||||
let mut fs = fs.borrow_mut();
|
||||
fs.clock += 1;
|
||||
let t = fs.clock;
|
||||
let mut pos = 0usize;
|
||||
for c in c0..=c1 {
|
||||
let (d, used) = fs.chunks.get_mut(&(self.id, c)).expect("checked above");
|
||||
// Present (checked above); evicted in between would be a bug, reported as an error.
|
||||
let Some((d, used)) = fs.chunks.get_mut(&(self.id, c)) else { return false };
|
||||
*used = t;
|
||||
let a = if c == c0 { (offset - c * CHUNK) as usize } else { 0 };
|
||||
let n = (d.len() - a).min(buf.len() - pos);
|
||||
buf[pos..pos + n].copy_from_slice(&d[a..a + n]);
|
||||
pos += n;
|
||||
}
|
||||
true
|
||||
});
|
||||
if !complete {
|
||||
return Err(filmcraft_media::pending::would_block());
|
||||
}
|
||||
// read-ahead for sequential access
|
||||
for c in c1 + 1..=(c1 + READ_AHEAD).min(last) {
|
||||
request(self.id, c);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
//!
|
||||
//! Everything here is `wasm32`-only; on other targets the crate is empty.
|
||||
#![cfg(target_arch = "wasm32")]
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub mod api;
|
||||
pub mod audio;
|
||||
|
||||
@@ -463,7 +463,7 @@ impl WcSource {
|
||||
c.frames.insert(pts, Arc::new(f));
|
||||
}
|
||||
while c.frames.len() > CACHE_FRAMES {
|
||||
let far = c.frames.keys().copied().max_by_key(|p| (p - want).abs()).expect("non-empty");
|
||||
let Some(far) = c.frames.keys().copied().max_by_key(|p| (p - want).abs()) else { break };
|
||||
c.frames.remove(&far);
|
||||
}
|
||||
// the session's own frame came out: other requests may restart it now
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
//! asking, `--no-recover` starts without asking (the changes stay available via File ▸ Recover
|
||||
//! Unsaved Changes…).
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod app_nap;
|
||||
mod audio;
|
||||
mod audio_in;
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
//! # Ok::<(), filmcraft_aac::Error>(())
|
||||
//! ```
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod config;
|
||||
mod decoder;
|
||||
mod encoder;
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
//!
|
||||
//! E-AC-3 (bsid 11-16) is recognised and refused.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod tables;
|
||||
|
||||
use std::sync::OnceLock;
|
||||
@@ -359,8 +361,7 @@ fn bit_allocation(exp: &[u8; 256], p: &AllocParams, g: &Globals, bap: &mut [u8;
|
||||
let bndend = tables::MASKTAB[end - 1] as usize + 1;
|
||||
let mut excite = [0i32; 50];
|
||||
let (mut fastleak, mut slowleak) = p.leak.unwrap_or((0, 0));
|
||||
let begin;
|
||||
if bndstrt == 0 {
|
||||
let begin = if bndstrt == 0 {
|
||||
// the last band of the LFE channel (bndend 7) has no band above it
|
||||
let lfe_last = |bin: usize| !(bndend == 7 && bin == 6);
|
||||
let mut lowcomp = 0;
|
||||
@@ -391,10 +392,10 @@ fn bit_allocation(exp: &[u8; 256], p: &AllocParams, g: &Globals, bap: &mut [u8;
|
||||
slowleak = slowleak.max(bndpsd[bin] - sgain);
|
||||
excite[bin] = (fastleak - lowcomp).max(slowleak);
|
||||
}
|
||||
begin = 22;
|
||||
22
|
||||
} else {
|
||||
begin = bndstrt;
|
||||
}
|
||||
bndstrt
|
||||
};
|
||||
for bin in begin..bndend {
|
||||
fastleak -= fdecay;
|
||||
fastleak = fastleak.max(bndpsd[bin] - p.fgain);
|
||||
@@ -1030,7 +1031,7 @@ impl Decoder {
|
||||
/// Coded channel order (Table 5.8) → WAV / SMPTE order: fronts L R C, LFE, surrounds.
|
||||
fn wav_order(mut pcm: Vec<Vec<f32>>, acmod: usize, lfeon: bool) -> Vec<Vec<f32>> {
|
||||
let nf = NFCHANS[acmod];
|
||||
let lfe = lfeon.then(|| pcm.pop().expect("lfe"));
|
||||
let lfe = if lfeon { pcm.pop() } else { None };
|
||||
let mut fbw = pcm;
|
||||
// acmod 3, 5, 7 code L C R …: move C after R
|
||||
if acmod & 1 == 1 && acmod != 1 {
|
||||
|
||||
@@ -99,7 +99,7 @@ fn ffmpeg_encoded_streams_decode() {
|
||||
}
|
||||
}
|
||||
let o = Command::new(&ff).args(["-v", "error", "-i"]).arg(&f).args(["-f", "f32le", "-"]).output().unwrap();
|
||||
let want: Vec<f32> = o.stdout.chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
let want: Vec<f32> = o.stdout.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
let ch = s.channels as usize;
|
||||
let n = want.len() / ch;
|
||||
assert_eq!(n, ours[0].len(), "{}: sample count", s.name);
|
||||
|
||||
@@ -123,7 +123,7 @@ pub fn matrix(from: Layout, to: Layout, mixdown: Mixdown) -> Vec<Vec<f32>> {
|
||||
let (ni, no) = (from.channels(), to.channels());
|
||||
let mut m = vec![vec![0.0f32; ni]; no];
|
||||
match (from, to) {
|
||||
(a, b) if a == b => {
|
||||
(Layout::Mono, Layout::Mono) | (Layout::Stereo, Layout::Stereo) | (Layout::Surround51, Layout::Surround51) => {
|
||||
for (i, row) in m.iter_mut().enumerate() {
|
||||
row[i] = 1.0;
|
||||
}
|
||||
@@ -159,7 +159,6 @@ pub fn matrix(from: Layout, to: Layout, mixdown: Mixdown) -> Vec<Vec<f32>> {
|
||||
m[1][LFE] = K;
|
||||
}
|
||||
}
|
||||
_ => unreachable!("all layout pairs are covered"),
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
//! denormals never appear (no FTZ/DAZ CPU flags needed, which keeps the crate `unsafe`-free and
|
||||
//! portable to wasm).
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub mod biquad;
|
||||
pub mod channels;
|
||||
pub mod design;
|
||||
@@ -169,12 +171,11 @@ impl ParamValues {
|
||||
pub(crate) fn get(&self, id: &str) -> Option<f32> {
|
||||
self.index_of(id).map(|i| self.values[i])
|
||||
}
|
||||
/// Value of a parameter that is known to exist (panics otherwise — programming error).
|
||||
/// Value of a parameter that is known to exist (0 otherwise — a programming error caught by
|
||||
/// debug builds).
|
||||
pub(crate) fn v(&self, id: &str) -> f32 {
|
||||
match self.get(id) {
|
||||
Some(v) => v,
|
||||
None => panic!("unknown parameter {id}"),
|
||||
}
|
||||
debug_assert!(self.get(id).is_some(), "unknown parameter {id}");
|
||||
self.get(id).unwrap_or(0.0)
|
||||
}
|
||||
pub(crate) fn on(&self, id: &str) -> bool {
|
||||
self.v(id) >= 0.5
|
||||
|
||||
@@ -42,7 +42,9 @@ impl BridgeClient {
|
||||
*guard = Some((BufReader::new(r), w));
|
||||
}
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
let conn = guard.as_mut().expect("connected");
|
||||
let Some(conn) = guard.as_mut() else {
|
||||
return Err(AutomationError::Bridge(format!("not connected to {}", self.addr)));
|
||||
};
|
||||
let line = format!("{}\n", json!({"id": id, "method": method, "params": params}));
|
||||
let res: Result<Value, AutomationError> = async {
|
||||
conn.1.write_all(line.as_bytes()).await.map_err(|e| AutomationError::Bridge(e.to_string()))?;
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
//! (`filmcraft --control <port>`), so agents can also inspect, screenshot, click, drag, scroll
|
||||
//! and type in the live UI — every menu, panel, timeline gesture and keystroke.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub mod bridge;
|
||||
pub mod server;
|
||||
|
||||
|
||||
@@ -237,12 +237,16 @@ fn filter_rows<const W: usize>(buf: &[i32; PAD_STRIDE * PAD_STRIDE], taps: &[(is
|
||||
}
|
||||
for i in 0..h {
|
||||
let ci = ((i + 2) * PAD_STRIDE + 2) as isize;
|
||||
let x: [i16; W] = b16[ci as usize..ci as usize + W].try_into().expect("centre row");
|
||||
let Some(&x) = b16.get(ci as usize..).and_then(|r| r.first_chunk::<W>()) else {
|
||||
return;
|
||||
};
|
||||
let (mut sum, mut max, mut min) = ([0i16; W], x, x);
|
||||
for &(off, weight, thr, adj) in taps {
|
||||
let (weight, thr) = (weight as i16, thr as i16);
|
||||
let start = (ci + off) as usize;
|
||||
let p: &[i16; W] = b16[start..start + W].try_into().expect("tap row");
|
||||
let Some(p) = b16.get(start..).and_then(|r| r.first_chunk::<W>()) else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
// constrain(): .max / .min instead of clamp (whose bound check panics), and
|
||||
// the sign applied branch-free, so the loop vectorises.
|
||||
|
||||
@@ -72,10 +72,12 @@ impl<T: Clone> Slot<T> {
|
||||
|
||||
fn wait(&self) -> Result<T> {
|
||||
let mut g = self.value.lock().unwrap_or_else(|e| e.into_inner());
|
||||
while g.is_none() {
|
||||
loop {
|
||||
if let Some(v) = g.as_ref() {
|
||||
return v.clone();
|
||||
}
|
||||
g = self.cv.wait(g).unwrap_or_else(|e| e.into_inner());
|
||||
}
|
||||
g.as_ref().expect("slot value").clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +310,7 @@ impl Decoder {
|
||||
if !o.is_ready() && self.out.len() <= max_queue {
|
||||
break;
|
||||
}
|
||||
let o = self.out.pop_front().expect("front");
|
||||
let Some(o) = self.out.pop_front() else { break };
|
||||
pics.push(o.take()?);
|
||||
}
|
||||
parsed?;
|
||||
@@ -358,7 +360,9 @@ impl Decoder {
|
||||
match last {
|
||||
Some((lt, lsid)) if lt == t => {
|
||||
if sid >= lsid {
|
||||
*sel.last_mut().expect("picture") = o;
|
||||
if let Some(l) = sel.last_mut() {
|
||||
*l = o;
|
||||
}
|
||||
last = Some((t, sid));
|
||||
}
|
||||
}
|
||||
@@ -612,7 +616,7 @@ impl Decoder {
|
||||
self.in_flight.push_back(slot);
|
||||
// Bound the frames in flight (memory): wait for the oldest.
|
||||
while self.in_flight.len() > self.max_in_flight {
|
||||
let s = self.in_flight.pop_front().expect("in flight");
|
||||
let Some(s) = self.in_flight.pop_front() else { break };
|
||||
let _ = s.wait();
|
||||
}
|
||||
self.in_flight.retain(|s| !s.is_set());
|
||||
|
||||
+17
-5
@@ -180,7 +180,11 @@ impl TileDecoder<'_, '_> {
|
||||
}
|
||||
|
||||
fn ref_plane(&self, ref_idx: i32, plane: usize) -> &Plane {
|
||||
if ref_idx < 0 { &self.t.cur.planes[plane] } else { &self.fs.refs[ref_idx as usize].as_ref().expect("reference frame").buf.planes[plane] }
|
||||
// A missing reference (rejected earlier for valid streams) predicts from the current frame.
|
||||
match usize::try_from(ref_idx).ok().and_then(|i| self.fs.refs.get(i)).and_then(|r| r.as_ref()) {
|
||||
Some(r) => &r.buf.planes[plane],
|
||||
None => &self.t.cur.planes[plane],
|
||||
}
|
||||
}
|
||||
|
||||
/// Block inter prediction process (7.11.3.4) into `pred` (stride PRED_STRIDE); `tmp` holds
|
||||
@@ -672,12 +676,16 @@ fn hpass<const W: usize>(src: &[u16], hf: &[i16; 8], out: &mut [i32], half: i32,
|
||||
let mut acc = [half; W];
|
||||
for t in 0..8 {
|
||||
let f = hf[t] as i32;
|
||||
let p: &[u16; W] = src[t..t + W].try_into().expect("row");
|
||||
let Some(p) = src.get(t..).and_then(|s| s.first_chunk::<W>()) else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
acc[j] += f * p[j] as i32;
|
||||
}
|
||||
}
|
||||
let o: &mut [i32; W] = (&mut out[..W]).try_into().expect("row");
|
||||
let Some(o) = out.first_chunk_mut::<W>() else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
o[j] = acc[j] >> sh;
|
||||
}
|
||||
@@ -689,12 +697,16 @@ fn vpass<const W: usize>(rows: &[i32], vf: &[i16; 8], out: &mut [i32], half: i32
|
||||
let mut acc = [half; W];
|
||||
for t in 0..8 {
|
||||
let f = vf[t] as i32;
|
||||
let p: &[i32; W] = rows[t * W..t * W + W].try_into().expect("row");
|
||||
let Some(p) = rows.get(t * W..).and_then(|s| s.first_chunk::<W>()) else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
acc[j] += f * p[j];
|
||||
}
|
||||
}
|
||||
let o: &mut [i32; W] = (&mut out[..W]).try_into().expect("row");
|
||||
let Some(o) = out.first_chunk_mut::<W>() else {
|
||||
return;
|
||||
};
|
||||
for j in 0..W {
|
||||
o[j] = acc[j] >> sh;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
// Items used only by stages still being written (inter prediction, post filters).
|
||||
#![allow(dead_code)]
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod bits;
|
||||
mod cdef;
|
||||
|
||||
@@ -76,7 +76,7 @@ pub fn reference(ff: &Path, path: &Path, pix_fmt: &str) -> Vec<u16> {
|
||||
.expect("run ffmpeg");
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
if pix_fmt.ends_with("le") {
|
||||
out.stdout.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect()
|
||||
out.stdout.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes([c[0], c[1]])).collect()
|
||||
} else {
|
||||
out.stdout.iter().map(|&b| b as u16).collect()
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//! - [`unescape_rbsp`] / [`escape_rbsp`]: remove/insert H.264/HEVC emulation-prevention bytes.
|
||||
//! - [`annexb_nals`] / [`length_prefixed_nals`]: split Annex-B byte streams and AVCC/HVCC samples into NAL units.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
//! - **Burn-in:** [`burn`] lays a caption out with the track style and draws it with the
|
||||
//! `filmcraft-text` engine (Inter SemiBold, shaped and kerned).
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub mod burn;
|
||||
pub mod cea608;
|
||||
pub mod mcc;
|
||||
@@ -212,7 +214,7 @@ pub fn parse(bytes: &[u8], format: Format) -> Result<Document> {
|
||||
Format::Scc => scc::parse(&text),
|
||||
Format::Mcc => mcc::parse(&text),
|
||||
Format::Ttml | Format::Dfxp => ttml::parse(&text),
|
||||
Format::Stl => unreachable!("handled above"),
|
||||
Format::Stl => stl::parse(bytes),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +255,7 @@ pub fn decode_text(bytes: &[u8]) -> String {
|
||||
} else if bytes.starts_with(&[0xff, 0xfe]) || bytes.starts_with(&[0xfe, 0xff]) {
|
||||
let le = bytes[0] == 0xff;
|
||||
let units: Vec<u16> =
|
||||
bytes[2..].chunks_exact(2).map(|c| if le { u16::from_le_bytes([c[0], c[1]]) } else { u16::from_be_bytes([c[0], c[1]]) }).collect();
|
||||
bytes[2..].as_chunks::<2>().0.iter().map(|c| if le { u16::from_le_bytes([c[0], c[1]]) } else { u16::from_be_bytes([c[0], c[1]]) }).collect();
|
||||
String::from_utf16_lossy(&units)
|
||||
} else {
|
||||
match std::str::from_utf8(bytes) {
|
||||
|
||||
@@ -411,10 +411,10 @@ impl Dec708 {
|
||||
}
|
||||
fn put(&mut self, c: char) {
|
||||
let w = self.w();
|
||||
if w.rows.is_empty() {
|
||||
w.rows.push(String::new());
|
||||
match w.rows.last_mut() {
|
||||
Some(row) => row.push(c),
|
||||
None => w.rows.push(c.to_string()),
|
||||
}
|
||||
w.rows.last_mut().expect("row").push(c);
|
||||
}
|
||||
fn bitmap(&mut self, m: u8, f: impl Fn(&mut Win)) {
|
||||
for (i, w) in self.win.iter_mut().enumerate() {
|
||||
|
||||
@@ -251,7 +251,9 @@ pub fn parse(bytes: &[u8]) -> Result<Document> {
|
||||
if !sniff(bytes) {
|
||||
return Err(Error::NotFormat("EBU STL"));
|
||||
}
|
||||
let rate = rate_of(&bytes[3..11]).expect("sniffed");
|
||||
let Some(rate) = bytes.get(3..11).and_then(rate_of) else {
|
||||
return Err(Error::NotFormat("EBU STL"));
|
||||
};
|
||||
let gsi = &bytes[..GSI];
|
||||
// start of programme (HHMMSSFF, ASCII)
|
||||
let tcp = ascii_field(&gsi[256..264]);
|
||||
@@ -277,7 +279,7 @@ pub fn parse(bytes: &[u8]) -> Result<Document> {
|
||||
}
|
||||
}
|
||||
};
|
||||
for blk in bytes[GSI..].chunks_exact(TTI) {
|
||||
for blk in bytes[GSI..].as_chunks::<TTI>().0 {
|
||||
let sn = u16::from_le_bytes([blk[1], blk[2]]);
|
||||
let ebn = blk[3];
|
||||
let cf = blk[15];
|
||||
|
||||
@@ -103,7 +103,7 @@ fn inline(text: &str) -> String {
|
||||
rest = &after[end + 1..];
|
||||
continue;
|
||||
}
|
||||
let ch = rest.chars().next().expect("non-empty");
|
||||
let Some(ch) = rest.chars().next() else { break };
|
||||
if ch == '\n' {
|
||||
out.push_str("<br/>");
|
||||
} else {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
//!
|
||||
//! Layer L0: no dependencies beyond `std`, no `unsafe`, builds for `wasm32-unknown-unknown`.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod read;
|
||||
mod write;
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ impl<'a> CompoundFile<'a> {
|
||||
// Mini FAT and mini stream.
|
||||
if first_minifat <= MAXREGSECT {
|
||||
let mf = cf.read_chain(first_minifat, None)?;
|
||||
cf.minifat = mf.chunks_exact(4).map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect();
|
||||
cf.minifat = mf.as_chunks::<4>().0.iter().map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect();
|
||||
}
|
||||
let root = &cf.entries[0];
|
||||
if root.start <= MAXREGSECT && root.size > 0 {
|
||||
|
||||
@@ -36,7 +36,7 @@ log = { workspace = true }
|
||||
web-time = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["jpeg"] }
|
||||
# Bootstrap audio decoding (MPL-2.0, used unmodified); replaced by own decoders over time.
|
||||
symphonia = { version = "0.5.5", default-features = false, features = ["aac", "mp1", "mp2", "mp3", "flac", "alac", "pcm", "wav", "ogg", "vorbis", "isomp4"] }
|
||||
symphonia = { version = "0.5.5", default-features = false, features = ["aac", "mp1", "mp2", "mp3", "flac", "alac", "pcm", "wav", "aiff", "ogg", "vorbis", "isomp4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
filmcraft-testkit = { workspace = true }
|
||||
@@ -240,8 +240,7 @@ impl GopCache {
|
||||
}
|
||||
// evict frames far from the most recent (keep a window around the working position)
|
||||
while st.bytes > budget && st.frames.len() > 2 {
|
||||
let first = *st.frames.keys().next().expect("non-empty");
|
||||
let last = *st.frames.keys().next_back().expect("non-empty");
|
||||
let (Some(&first), Some(&last)) = (st.frames.keys().next(), st.frames.keys().next_back()) else { break };
|
||||
let victim = if pts - first > last - pts { first } else { last };
|
||||
if let Some(v) = st.frames.remove(&victim) {
|
||||
st.drafts.remove(&victim);
|
||||
@@ -323,7 +322,7 @@ impl GopCache {
|
||||
// every sample): step back to a sample the decoder can start from.
|
||||
while key > 0 {
|
||||
let data = s.read(key)?;
|
||||
if st.decoder.as_ref().expect("decoder").is_random_access(&data) != Some(false) {
|
||||
if st.decoder.as_ref().and_then(|d| d.is_random_access(&data)) != Some(false) {
|
||||
break;
|
||||
}
|
||||
key = s.sync_before(key - 1);
|
||||
@@ -337,7 +336,10 @@ impl GopCache {
|
||||
}
|
||||
let limit = (i.max(st.next) + 64).min(n);
|
||||
let late = late_before.unwrap_or(i64::MIN).min(want_pts);
|
||||
st.decoder.as_mut().expect("decoder").set_draft(draft);
|
||||
let Some(decoder) = st.decoder.as_mut() else {
|
||||
return Err(CodecError::Decode("no video decoder".into()));
|
||||
};
|
||||
decoder.set_draft(draft);
|
||||
while st.next < limit {
|
||||
if filmcraft_media::cancel::cancelled() {
|
||||
// The decoder state stays consistent (`next`, `out_max`): a later request continues.
|
||||
@@ -345,12 +347,15 @@ impl GopCache {
|
||||
}
|
||||
let k = st.next;
|
||||
let data = s.read(k)?;
|
||||
if k != i && s.pts(k) < late && st.decoder.as_ref().expect("decoder").is_disposable(&data) {
|
||||
if k != i && s.pts(k) < late && st.decoder.as_ref().is_some_and(|d| d.is_disposable(&data)) {
|
||||
st.next += 1;
|
||||
SKIPPED.fetch_add(1, Ordering::Relaxed);
|
||||
continue;
|
||||
}
|
||||
let out = timed(|| st.decoder.as_mut().expect("decoder").decode(&data, s.pts(k)))?;
|
||||
let Some(decoder) = st.decoder.as_mut() else {
|
||||
return Err(CodecError::Decode("no video decoder".into()));
|
||||
};
|
||||
let out = timed(|| decoder.decode(&data, s.pts(k)))?;
|
||||
st.next += 1;
|
||||
DECODED.fetch_add(1, Ordering::Relaxed);
|
||||
self.store_output(&mut st, out);
|
||||
@@ -359,7 +364,7 @@ impl GopCache {
|
||||
}
|
||||
}
|
||||
if st.cached(want_pts, draft).is_none() {
|
||||
let out = timed(|| st.decoder.as_mut().expect("decoder").flush());
|
||||
let out = st.decoder.as_mut().map(|d| timed(|| d.flush())).unwrap_or_default();
|
||||
self.store_output(&mut st, out);
|
||||
st.next = usize::MAX;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
//! - [`AudioFileSource`]: standalone compressed audio files (MP3, FLAC, AIFF, …).
|
||||
//! - [`openers`]: the openers to register with the engine's media pool.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub mod audio;
|
||||
pub mod gop;
|
||||
pub mod mkv;
|
||||
|
||||
@@ -369,9 +369,9 @@ impl MkvSource {
|
||||
if let Some(p) = st.packets.get(&i) {
|
||||
return Ok(p.clone());
|
||||
}
|
||||
let ti = self.atrack.expect("audio");
|
||||
let ti = self.atrack.ok_or_else(|| CodecError::Unsupported("no audio track".into()))?;
|
||||
let track = &self.file.tracks[ti];
|
||||
let ainfo = self.info.audio.as_ref().expect("audio info");
|
||||
let ainfo = self.info.audio.as_ref().ok_or_else(|| CodecError::Unsupported("no audio track".into()))?;
|
||||
let data = self.read(ti, i)?;
|
||||
let decoded = match &track.codec {
|
||||
Codec::Pcm { float, big_endian, bits } => {
|
||||
@@ -392,7 +392,7 @@ impl MkvSource {
|
||||
// Non-sequential access: reset and prime with the preceding packets (codec pre-roll:
|
||||
// one packet, or `SeekPreRoll` worth for Opus).
|
||||
if st.last_decoded.is_none_or(|l| l + 1 != i) {
|
||||
let d = st.decoder.as_mut().expect("decoder");
|
||||
let d = st.decoder.as_mut().ok_or_else(|| CodecError::Decode("no audio decoder".into()))?;
|
||||
d.reset();
|
||||
let from = if self.audio_preroll > 0 {
|
||||
self.audio_starts.partition_point(|&x| x <= self.audio_starts[i] - self.audio_preroll).saturating_sub(1)
|
||||
@@ -405,7 +405,7 @@ impl MkvSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
let r = st.decoder.as_mut().expect("decoder").decode(&data, self.audio_starts[i].max(0) as u64);
|
||||
let r = st.decoder.as_mut().ok_or_else(|| CodecError::Decode("no audio decoder".into()))?.decode(&data, self.audio_starts[i].max(0) as u64);
|
||||
st.last_decoded = Some(i);
|
||||
r.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -107,8 +107,11 @@ impl Mp4Source {
|
||||
pub fn open_reader(name: &str, reader: filmcraft_media::SharedReader) -> crate::Result<Self> {
|
||||
let bytes = crate::Src(reader);
|
||||
let file = filmcraft_isobmff::open(&bytes).map_err(|e| CodecError::Container(e.to_string()))?;
|
||||
let vtrack = file.tracks.iter().position(|t| t.kind == TrackKind::Video && !t.samples.is_empty());
|
||||
let atrack = file.tracks.iter().position(|t| t.kind == TrackKind::Audio && !t.samples.is_empty());
|
||||
// A track needs samples, a sample description and a timescale to be playable (damaged
|
||||
// files can lack them: indexing `entries[0]` or dividing by the timescale used to panic).
|
||||
let playable = |t: &&filmcraft_isobmff::Track, kind| t.kind == kind && !t.samples.is_empty() && !t.entries.is_empty() && t.timescale > 0;
|
||||
let vtrack = file.tracks.iter().position(|t| playable(&t, TrackKind::Video));
|
||||
let atrack = file.tracks.iter().position(|t| playable(&t, TrackKind::Audio));
|
||||
if vtrack.is_none() && atrack.is_none() {
|
||||
return Err(CodecError::Unsupported("no playable tracks".into()));
|
||||
}
|
||||
@@ -126,6 +129,8 @@ impl Mp4Source {
|
||||
durs.sort_unstable();
|
||||
let d = durs.get(durs.len() / 2).copied().unwrap_or(1).max(1);
|
||||
let rate = FrameRate::from_f64(t.timescale as f64 / d as f64);
|
||||
// A rate that rounds to zero (one sample per huge duration) can't be divided by.
|
||||
let rate = if rate.num > 0 && rate.den > 0 { rate } else { FrameRate::default() };
|
||||
color = color_from(entry, w, h);
|
||||
if entry
|
||||
.video
|
||||
@@ -264,9 +269,9 @@ impl Mp4Source {
|
||||
if let Some(p) = st.packets.get(&i) {
|
||||
return Ok(p.clone());
|
||||
}
|
||||
let ti = self.atrack.expect("audio");
|
||||
let ti = self.atrack.ok_or_else(|| CodecError::Unsupported("no audio track".into()))?;
|
||||
let track = &self.file.tracks[ti];
|
||||
let entry = &track.entries[0];
|
||||
let entry = track.entries.first().ok_or_else(|| CodecError::Container("audio track without a sample description".into()))?;
|
||||
let data = self.read(ti, i)?;
|
||||
let decoded = match &entry.codec {
|
||||
CodecConfig::Pcm(p) => decode_pcm(&data, p),
|
||||
@@ -277,7 +282,7 @@ impl Mp4Source {
|
||||
// Non-sequential access: reset and prime with the preceding packets (codec pre-roll:
|
||||
// one packet, or `OPUS_PRE_ROLL` worth for Opus).
|
||||
if st.last_decoded.is_none_or(|l| l + 1 != i) {
|
||||
let d = st.decoder.as_mut().expect("decoder");
|
||||
let d = st.decoder.as_mut().ok_or_else(|| CodecError::Decode("no audio decoder".into()))?;
|
||||
d.reset();
|
||||
let from = if self.audio_preroll > 0 {
|
||||
self.audio_starts.partition_point(|&x| x <= self.audio_starts[i] - self.audio_preroll).saturating_sub(1)
|
||||
@@ -290,7 +295,7 @@ impl Mp4Source {
|
||||
}
|
||||
}
|
||||
}
|
||||
let r = st.decoder.as_mut().expect("decoder").decode(&data, self.audio_starts[i].max(0) as u64);
|
||||
let r = st.decoder.as_mut().ok_or_else(|| CodecError::Decode("no audio decoder".into()))?.decode(&data, self.audio_starts[i].max(0) as u64);
|
||||
st.last_decoded = Some(i);
|
||||
match r {
|
||||
Ok(v) => v,
|
||||
@@ -513,6 +518,33 @@ mod tests {
|
||||
b.into()
|
||||
}
|
||||
|
||||
/// Overwrite the big-endian u32 `skip` bytes after the first `fourcc` box type.
|
||||
fn patch_u32(b: &mut [u8], fourcc: &[u8; 4], skip: usize, v: u32) {
|
||||
let k = b.windows(4).position(|x| x == fourcc).expect("box") + 4 + skip;
|
||||
b[k..k + 4].copy_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
|
||||
/// Damaged MOVs found by mutation fuzzing panicked on import: a track with timescale 0
|
||||
/// ("attempt to divide by zero" in the duration) and one whose sample description list is
|
||||
/// empty ("index out of bounds" on `entries[0]`). Both must open as an error, or as media
|
||||
/// that answers requests without panicking.
|
||||
#[test]
|
||||
fn damaged_track_headers_never_panic() {
|
||||
let identity = [0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x4000_0000];
|
||||
// mdhd v0: version/flags, creation, modification, then timescale
|
||||
let mut zero_timescale = rotated_mov(identity).to_vec();
|
||||
patch_u32(&mut zero_timescale, b"mdhd", 12, 0);
|
||||
// stsd: version/flags, then entry_count
|
||||
let mut no_entries = rotated_mov(identity).to_vec();
|
||||
patch_u32(&mut no_entries, b"stsd", 4, 0);
|
||||
for (name, b) in [("ts0.mov", zero_timescale), ("stsd0.mov", no_entries)] {
|
||||
if let Ok(s) = Mp4Source::open(name, b.into()) {
|
||||
let _ = s.video_frame(FrameRequest::full(Tick::ZERO));
|
||||
let _ = s.audio(0, 1024, 48_000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mean luma of the top and bottom halves of the decoded frame, and its size.
|
||||
fn halves(src: &Mp4Source) -> (u32, u32, u8, u8) {
|
||||
let f = src.video_frame(FrameRequest::full(Tick::ZERO)).expect("frame");
|
||||
|
||||
+41
-15
@@ -123,6 +123,19 @@ fn audio_supported(c: &Codec) -> bool {
|
||||
matches!(c, Codec::MpegAudio | Codec::AacAdts | Codec::AacLatm | Codec::LpcmBluray | Codec::LpcmDvd | Codec::Ac3)
|
||||
}
|
||||
|
||||
/// The coded rate unless the timestamps say otherwise (3:2 pulldown, field-rate coding). Never
|
||||
/// zero: damaged timestamps spread over a huge span give a rate that rounds to 0 fps, which the
|
||||
/// duration computation divided by.
|
||||
fn stream_rate(coded: Option<FrameRate>, from_pts: Option<f64>) -> FrameRate {
|
||||
let rate = match (coded, from_pts) {
|
||||
(Some(r), Some(p)) if (r.as_f64() - p).abs() / p > 0.01 => FrameRate::from_f64(p),
|
||||
(Some(r), _) => r,
|
||||
(None, Some(p)) => FrameRate::from_f64(p),
|
||||
(None, None) => FrameRate::FPS_25,
|
||||
};
|
||||
if rate.num > 0 && rate.den > 0 { rate } else { FrameRate::FPS_25 }
|
||||
}
|
||||
|
||||
/// Display-order keys of samples (decode order): the PTS when every unit has one, else (MPEG
|
||||
/// video) GOP start + temporal_reference, else the decode order.
|
||||
fn display_keys(units: &[&Unit], mpeg: bool) -> Vec<i64> {
|
||||
@@ -242,8 +255,9 @@ impl MpegSource {
|
||||
src.file = Some(file);
|
||||
src.video = Some(track);
|
||||
src.describe_video(rate_from_pts)?;
|
||||
let file = src.file.take().expect("file");
|
||||
src.setup_audio(file, astream, origin);
|
||||
if let Some(file) = src.file.take() {
|
||||
src.setup_audio(file, astream, origin);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
src.unsupported_video = Some(format!("{} video in {} (FilmCraft has no decoder for it)", st.codec.name(), file.format.name()));
|
||||
@@ -369,14 +383,14 @@ impl MpegSource {
|
||||
if let Some((data, aus)) = &self.es {
|
||||
return Ok(data[aus[u].clone()].to_vec());
|
||||
}
|
||||
let file = self.file.as_ref().expect("file");
|
||||
let file = self.file.as_ref().ok_or_else(|| CodecError::Container("no stream file".into()))?;
|
||||
file.read_unit(&self.bytes, v.stream, u).map_err(|e| CodecError::Container(e.to_string()))
|
||||
}
|
||||
|
||||
/// Fill in the video stream info from the first picture.
|
||||
fn describe_video(&mut self, rate_from_pts: Option<f64>) -> crate::Result<()> {
|
||||
let mut seq_header = None;
|
||||
let v = self.video.as_ref().expect("video");
|
||||
let v = self.video.as_ref().ok_or_else(|| CodecError::Unsupported("no video stream".into()))?;
|
||||
let first = self.read_unit(v, 0)?;
|
||||
let n = v.order.len();
|
||||
let (width, height, par, codec, pixel_format, mut color);
|
||||
@@ -432,12 +446,7 @@ impl MpegSource {
|
||||
}
|
||||
}
|
||||
// the coded rate unless the timestamps say otherwise (3:2 pulldown, field-rate coding)
|
||||
let rate = match (rate, rate_from_pts) {
|
||||
(Some(r), Some(p)) if (r.as_f64() - p).abs() / p > 0.01 => FrameRate::from_f64(p),
|
||||
(Some(r), _) => r,
|
||||
(None, Some(p)) => FrameRate::from_f64(p),
|
||||
(None, None) => FrameRate::FPS_25,
|
||||
};
|
||||
let rate = stream_rate(rate, rate_from_pts);
|
||||
let duration = rate.tick_of(n as i64);
|
||||
let total_bytes: u64 = match (&self.es, &self.file) {
|
||||
(Some((_, aus)), _) => v.units.iter().map(|&u| aus[u].len() as u64).sum(),
|
||||
@@ -445,8 +454,10 @@ impl MpegSource {
|
||||
_ => 0,
|
||||
};
|
||||
let secs = duration.seconds();
|
||||
if let Some(h) = seq_header {
|
||||
self.video.as_mut().expect("video").header = h;
|
||||
if let Some(h) = seq_header
|
||||
&& let Some(v) = self.video.as_mut()
|
||||
{
|
||||
v.header = h;
|
||||
}
|
||||
self.info.duration = duration;
|
||||
self.info.video = Some(VideoStreamInfo {
|
||||
@@ -525,7 +536,7 @@ impl MpegSource {
|
||||
return self.audio_unsupported(&codec_name, "DVD LPCM: no format header".into());
|
||||
}
|
||||
},
|
||||
_ => unreachable!("audio_supported"),
|
||||
_ => return self.audio_unsupported(&codec_name, format!("{codec_name} audio")),
|
||||
};
|
||||
if codec_is_ac3_unsupported(codec) {
|
||||
return self.audio_unsupported(&codec_name, format!("{codec_name} audio (FilmCraft has no AC-3 decoder yet)"));
|
||||
@@ -608,12 +619,12 @@ impl MpegSource {
|
||||
if let Some(p) = st.cache.get(&i) {
|
||||
return Ok(p.clone());
|
||||
}
|
||||
let file = self.file.as_ref().expect("file");
|
||||
let file = self.file.as_ref().ok_or_else(|| CodecError::Container("no stream file".into()))?;
|
||||
let read = |k: usize| file.read_unit(&self.bytes, a.stream, k).map_err(|e| CodecError::Container(e.to_string()));
|
||||
if st.dec.is_none() {
|
||||
st.dec = Some(AudioDecoder::new(a.codec, a.rate, a.channels, a.latm.clone())?);
|
||||
}
|
||||
let dec = st.dec.as_mut().expect("decoder");
|
||||
let dec = st.dec.as_mut().ok_or_else(|| CodecError::Decode("no audio decoder".into()))?;
|
||||
if st.last.is_none_or(|l| l + 1 != i) {
|
||||
dec.reset();
|
||||
if i > 0
|
||||
@@ -840,6 +851,21 @@ pub fn reader_opener(name: &str, head: &[u8], reader: &filmcraft_media::SharedRe
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A mutated transport stream whose PTS span gave ~0 fps panicked on open with "attempt to
|
||||
/// divide by zero" (`FrameRate::tick_of`).
|
||||
#[test]
|
||||
fn damaged_timestamps_never_give_a_zero_rate() {
|
||||
for (coded, p) in
|
||||
[(None, Some(1e-9)), (Some(FrameRate::FPS_25), Some(1e-9)), (Some(FrameRate::new(0, 0)), None), (None, Some(0.0)), (None, Some(f64::NAN))]
|
||||
{
|
||||
let r = stream_rate(coded, p);
|
||||
assert!(r.num > 0 && r.den > 0, "{coded:?} {p:?} -> {r:?}");
|
||||
let _ = r.tick_of(1000);
|
||||
}
|
||||
assert_eq!(stream_rate(Some(FrameRate::FPS_25), Some(25.0)), FrameRate::FPS_25);
|
||||
assert_eq!(stream_rate(None, Some(50.0)), FrameRate::from_f64(50.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presentation_drops_leading_pictures_and_orders_by_key() {
|
||||
// decode order I(2) B(0) B(1) P(5) B(3) B(4): the leading B pictures are not presented
|
||||
|
||||
@@ -333,7 +333,7 @@ impl MxfSource {
|
||||
let mut channels = 0;
|
||||
for &ai in &atracks {
|
||||
let t = &file.tracks[ai];
|
||||
let si = t.sound.as_ref().expect("sound info");
|
||||
let Some(si) = t.sound.as_ref() else { continue };
|
||||
if si.sample_rate != rate {
|
||||
continue;
|
||||
}
|
||||
@@ -424,7 +424,7 @@ impl VideoSamples for MxfVideo<'_> {
|
||||
return Err(CodecError::Unsupported(why.clone()));
|
||||
}
|
||||
let t = &self.src.file.tracks[self.track];
|
||||
let v = self.src.info.video.as_ref().expect("video info");
|
||||
let v = self.src.info.video.as_ref().ok_or_else(|| CodecError::Unsupported("no video track".into()))?;
|
||||
let (w, h) = (v.width.min(u16::MAX as u32) as u16, v.height.min(u16::MAX as u32) as u16);
|
||||
match t.codec {
|
||||
Codec::Avc { .. } => Ok(Box::new(crate::video::H264Decoder::annexb())),
|
||||
|
||||
@@ -133,7 +133,7 @@ impl OggSource {
|
||||
}
|
||||
// Non-sequential access: reset and decode the pre-roll (RFC 7845 §4.6; see OPUS_PRE_ROLL).
|
||||
if st.last_decoded.is_none_or(|l| l + 1 != i) {
|
||||
let d = st.decoder.as_mut().expect("decoder");
|
||||
let d = st.decoder.as_mut().ok_or_else(|| CodecError::Decode("no audio decoder".into()))?;
|
||||
d.reset();
|
||||
let from = timing.starts.partition_point(|&x| x <= timing.starts[i] - OPUS_PRE_ROLL as i64).saturating_sub(1);
|
||||
for j in from..i {
|
||||
@@ -143,7 +143,7 @@ impl OggSource {
|
||||
}
|
||||
}
|
||||
let data = read(i)?;
|
||||
let r = st.decoder.as_mut().expect("decoder").decode(&data, 0);
|
||||
let r = st.decoder.as_mut().ok_or_else(|| CodecError::Decode("no audio decoder".into()))?.decode(&data, 0);
|
||||
st.last_decoded = Some(i);
|
||||
let p = Arc::new(r.unwrap_or_default());
|
||||
st.packets.insert(i, p.clone());
|
||||
|
||||
@@ -88,6 +88,33 @@ fn mp3_file_decodes() {
|
||||
assert!(a.peaks()[0] > 0.1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aiff_file_decodes() {
|
||||
// 0.1 s of 48 kHz 16-bit stereo PCM: left a ramp, right silent (AIFF, big-endian samples)
|
||||
let frames = 4800u32;
|
||||
let mut b = b"FORM".to_vec();
|
||||
b.extend((4 + 26 + 16 + frames * 4).to_be_bytes());
|
||||
b.extend(b"AIFFCOMM");
|
||||
b.extend(18u32.to_be_bytes());
|
||||
b.extend(2u16.to_be_bytes());
|
||||
b.extend(frames.to_be_bytes());
|
||||
b.extend(16u16.to_be_bytes());
|
||||
b.extend([0x40, 0x0E, 0xBB, 0x80, 0, 0, 0, 0, 0, 0]); // 48000 as an 80-bit extended float
|
||||
b.extend(b"SSND");
|
||||
b.extend((8 + frames * 4).to_be_bytes());
|
||||
b.extend([0; 8]);
|
||||
for i in 0..frames {
|
||||
b.extend(((i as i16) * 4).to_be_bytes());
|
||||
b.extend(0i16.to_be_bytes());
|
||||
}
|
||||
let src = crate::open_bytes("tone.aif", b.into()).unwrap();
|
||||
assert!((src.info().duration.seconds() - 0.1).abs() < 1e-3, "{}", src.info().duration.seconds());
|
||||
let a = src.audio(0, 4800, 48_000).unwrap();
|
||||
assert_eq!(a.channels.len(), 2);
|
||||
assert!((a.channels[0][1000] - 4000.0 / 32768.0).abs() < 1e-4, "{}", a.channels[0][1000]);
|
||||
assert_eq!(a.channels[1][1000], 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seek_backwards_and_forwards_mjpeg() {
|
||||
let Some(b) = fixture("counter_mjpeg.mov", &["-f", "lavfi", "-i", "testsrc2=s=160x120:r=25:d=3", "-c:v", "mjpeg"]) else { return };
|
||||
@@ -445,7 +472,7 @@ fn opus_reference(name: &str) -> Option<Vec<f32>> {
|
||||
if !st.success() {
|
||||
return None;
|
||||
}
|
||||
Some(std::fs::read(out).ok()?.chunks_exact(4).map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect())
|
||||
Some(std::fs::read(out).ok()?.as_chunks::<4>().0.iter().map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect())
|
||||
}
|
||||
|
||||
fn snr_db(reference: &[f32], test: &[f32]) -> f64 {
|
||||
|
||||
@@ -386,7 +386,8 @@ impl Vp9Decoder {
|
||||
}
|
||||
}
|
||||
|
||||
fn convert(&self, p: filmcraft_vp9::Picture) -> DecodedFrame {
|
||||
/// `None` for a picture whose planes mix sample types (never produced by the decoder).
|
||||
fn convert(&self, p: filmcraft_vp9::Picture) -> Option<DecodedFrame> {
|
||||
use filmcraft_color::{Matrix, Primaries, Range};
|
||||
use filmcraft_frame::{Chroma, PixelData};
|
||||
use filmcraft_vp9::Plane;
|
||||
@@ -423,7 +424,7 @@ impl Vp9Decoder {
|
||||
}
|
||||
let mut frame = VideoFrame::rgba8(p.width, p.height, rgba);
|
||||
frame.color = filmcraft_color::ColorInfo { range: Range::Full, transfer: filmcraft_color::Transfer::Srgb, ..color };
|
||||
return DecodedFrame { pts, frame, draft };
|
||||
return Some(DecodedFrame { pts, frame, draft });
|
||||
}
|
||||
// 4:4:0 has no frame format of its own: chroma rows are repeated to 4:4:4.
|
||||
let (chroma, rows_440) = match (p.subsampling_x, p.subsampling_y) {
|
||||
@@ -455,22 +456,22 @@ impl Vp9Decoder {
|
||||
bits: p.bit_depth,
|
||||
alpha: None,
|
||||
},
|
||||
_ => unreachable!("VP9 planes share one sample type"),
|
||||
_ => return None,
|
||||
};
|
||||
// render_size (the intended display size) is not applied: the container's display
|
||||
// dimensions / pixel aspect describe the presentation.
|
||||
let par = (1, 1);
|
||||
DecodedFrame { pts, frame: VideoFrame { width: p.width, height: p.height, data, color, par, pts: filmcraft_time::Tick::ZERO }, draft }
|
||||
Some(DecodedFrame { pts, frame: VideoFrame { width: p.width, height: p.height, data, color, par, pts: filmcraft_time::Tick::ZERO }, draft })
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoDecoder for Vp9Decoder {
|
||||
fn decode(&mut self, sample: &[u8], pts: i64) -> Result<Vec<DecodedFrame>> {
|
||||
let pics = self.dec.decode(sample, pts).map_err(|e| CodecError::Decode(e.to_string()))?;
|
||||
Ok(pics.into_iter().map(|p| self.convert(p)).collect())
|
||||
Ok(pics.into_iter().filter_map(|p| self.convert(p)).collect())
|
||||
}
|
||||
fn flush(&mut self) -> Vec<DecodedFrame> {
|
||||
self.dec.flush().into_iter().map(|p| self.convert(p)).collect()
|
||||
self.dec.flush().into_iter().filter_map(|p| self.convert(p)).collect()
|
||||
}
|
||||
fn reset(&mut self) {
|
||||
self.dec.reset();
|
||||
|
||||
@@ -48,7 +48,7 @@ pub fn ffmpeg_audio_f32(ff: &Path, file: &Path, extra: &[&str]) -> Vec<f32> {
|
||||
let mut args = vec!["-i", file.to_str().unwrap()];
|
||||
args.extend_from_slice(extra);
|
||||
args.extend_from_slice(&["-map", "0:a:0", "-f", "f32le", "-"]);
|
||||
ffmpeg_out(ff, &args).chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect()
|
||||
ffmpeg_out(ff, &args).as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect()
|
||||
}
|
||||
|
||||
/// Our frame's three planes widened to u16 (and their sizes).
|
||||
@@ -63,7 +63,7 @@ pub fn planes(f: &VideoFrame) -> Vec<Vec<u16>> {
|
||||
/// Split one raw frame of `ffmpeg_frames` into planes (`bps` bytes per sample, chroma `cw`×`ch`).
|
||||
pub fn raw_planes(raw: &[u8], w: usize, h: usize, cw: usize, ch: usize, bps: usize) -> Vec<Vec<u16>> {
|
||||
let get = |b: &[u8]| -> Vec<u16> {
|
||||
if bps == 1 { b.iter().map(|&v| v as u16).collect() } else { b.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect() }
|
||||
if bps == 1 { b.iter().map(|&v| v as u16).collect() } else { b.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes([c[0], c[1]])).collect() }
|
||||
};
|
||||
let y = w * h * bps;
|
||||
let c = cw * ch * bps;
|
||||
|
||||
@@ -92,7 +92,7 @@ fn decode_both(ff: &Path, file: &Path, decoder: &[&str]) -> (OggSource, Vec<f32>
|
||||
let ch = a.channels as usize;
|
||||
let mut args: Vec<&str> = decoder.to_vec();
|
||||
args.extend_from_slice(&["-i", file.to_str().unwrap(), "-map", "0:a:0", "-f", "f32le", "-"]);
|
||||
let want: Vec<f32> = ffmpeg_out(ff, &args).chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
let want: Vec<f32> = ffmpeg_out(ff, &args).as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
let n = want.len() / ch;
|
||||
let frames = src.info().duration.to_units_floor(a.sample_rate as i64) as usize;
|
||||
assert_eq!(frames, n, "{}: our length = ffmpeg's ({} samples)", file.display(), n);
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//! The compositor works in **linear-light, premultiplied RGBA f32** in the sequence working space
|
||||
//! (Rec.709 primaries by default). Decoded frames carry [`ColorInfo`] so conversions are explicit.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ let f = decode_frame_with(&sample, &DecodeOptions { threads: false, ..Default::d
|
||||
let hdr: FrameHeader = probe(&sample)?; // cid, raster, depth, chroma…
|
||||
let name = cid_name(hdr.cid); // "DNxHR HQX", "DNxHD 1080i 8-bit (CID 1242)"
|
||||
|
||||
let mut enc = Encoder::new(Profile::Hq, 1920, 1080); // or Encoder::with_config(EncoderConfig { .. })
|
||||
let mut enc = Encoder::new(Profile::Hq, 1920, 1080)?; // or Encoder::with_config(EncoderConfig { .. })
|
||||
let sample: Vec<u8> = enc.encode(&frame)?; // exactly enc.frame_size() bytes (CBR)
|
||||
```
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ pub(crate) fn idct8x8(block: &mut [f32; 64]) {
|
||||
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();
|
||||
for (r, c) in block.as_chunks::<8>().0.iter().enumerate() {
|
||||
if c[1..].iter().all(|&v| v == 0.0) {
|
||||
if c[0] != 0.0 {
|
||||
t[r] = [c[0] * BASIS[0][0]; 8];
|
||||
|
||||
@@ -61,8 +61,9 @@ struct Prepared {
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
pub fn new(profile: Profile, width: u32, height: u32) -> Encoder {
|
||||
Encoder::with_config(EncoderConfig::new(profile, width, height)).expect("valid default configuration")
|
||||
/// An encoder with the default configuration; fails on rasters the profile can't code.
|
||||
pub fn new(profile: Profile, width: u32, height: u32) -> Result<Encoder> {
|
||||
Encoder::with_config(EncoderConfig::new(profile, width, height))
|
||||
}
|
||||
|
||||
pub fn with_config(cfg: EncoderConfig) -> Result<Encoder> {
|
||||
@@ -79,7 +80,7 @@ impl Encoder {
|
||||
if !ok_depth {
|
||||
return Err(Error::Input(format!("{} does not support {}-bit", cfg.profile.name(), cfg.bit_depth)));
|
||||
}
|
||||
let info = cid_info(cfg.profile.cid()).expect("RI CID");
|
||||
let info = cid_info(cfg.profile.cid()).ok_or_else(|| Error::Input(format!("no compression table for {}", cfg.profile.name())))?;
|
||||
let frame_size = cfg.profile.frame_size(cfg.width, cfg.height);
|
||||
let header_size = header::header_size_for(cfg.height);
|
||||
Ok(Encoder { cfg, info, frame_size, header_size, last_q: 8 })
|
||||
|
||||
@@ -127,7 +127,7 @@ fn probe_weights() {
|
||||
let px: Vec<f64> = if depth == 8 {
|
||||
out.stdout.iter().map(|&b| b as f64).collect()
|
||||
} else {
|
||||
out.stdout.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]]) as f64).collect()
|
||||
out.stdout.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes([c[0], c[1]]) as f64).collect()
|
||||
};
|
||||
let (off, stride) = if blk == 0 { (0, w as usize) } else { ((w * h) as usize, w as usize / 2) };
|
||||
let mid = (1 << (depth - 1)) as f64;
|
||||
@@ -224,7 +224,7 @@ fn probe_reconstruction() {
|
||||
let px: Vec<f64> = if depth == 8 {
|
||||
out.stdout.iter().map(|&b| b as f64).collect()
|
||||
} else {
|
||||
out.stdout.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]]) as f64).collect()
|
||||
out.stdout.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes([c[0], c[1]]) as f64).collect()
|
||||
};
|
||||
let mid = (1 << (depth - 1)) as f64;
|
||||
let mut sum = 0f64;
|
||||
|
||||
@@ -152,7 +152,7 @@ pub fn parse(d: &[u8]) -> Result<FrameHeader> {
|
||||
2 => ColorVolume::Bt2020Cl,
|
||||
_ => ColorVolume::OutOfBand,
|
||||
};
|
||||
let timecode = (d[0x30] & 0x80 != 0).then(|| d[0x31..0x39].try_into().unwrap());
|
||||
let timecode = if d[0x30] & 0x80 != 0 { d.get(0x31..).and_then(|t| t.first_chunk::<8>()).copied() } else { None };
|
||||
let mb_rows = be16(d, 0x16C);
|
||||
if width == 0 || lines == 0 || width > 16384 || lines > 16384 {
|
||||
return Err(Error::Invalid("raster size"));
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
//!
|
||||
//! See the crate README for accuracy, performance and limitations.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod bits;
|
||||
mod dct;
|
||||
mod decode;
|
||||
|
||||
@@ -91,7 +91,7 @@ pub fn reference(ff: &Path, mov: &Path, pix_fmt: &str) -> Vec<u16> {
|
||||
.expect("run ffmpeg");
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
if pix_fmt.ends_with("le") {
|
||||
out.stdout.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect()
|
||||
out.stdout.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes([c[0], c[1]])).collect()
|
||||
} else {
|
||||
out.stdout.iter().map(|&b| b as u16).collect()
|
||||
}
|
||||
@@ -247,7 +247,7 @@ pub fn ffmpeg_decode(ff: &Path, mov: &Path, pix_fmt: &str) -> Vec<u16> {
|
||||
let err = String::from_utf8_lossy(&out.stderr).to_string();
|
||||
assert!(out.status.success() && err.trim().is_empty(), "ffmpeg failed: {err}");
|
||||
if pix_fmt.ends_with("le") {
|
||||
out.stdout.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect()
|
||||
out.stdout.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes([c[0], c[1]])).collect()
|
||||
} else {
|
||||
out.stdout.iter().map(|&b| b as u16).collect()
|
||||
}
|
||||
@@ -270,8 +270,11 @@ pub fn source_frames(ff: &Path, name: &str, lavfi: &str, frames: u32, w: u32, h:
|
||||
std::fs::rename(&tmp, &path).unwrap();
|
||||
}
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
let s: Vec<u16> =
|
||||
if depth == 8 { bytes.iter().map(|&b| b as u16).collect() } else { bytes.chunks_exact(2).map(|c| u16::from_le_bytes([c[0], c[1]])).collect() };
|
||||
let s: Vec<u16> = if depth == 8 {
|
||||
bytes.iter().map(|&b| b as u16).collect()
|
||||
} else {
|
||||
bytes.as_chunks::<2>().0.iter().map(|c| u16::from_le_bytes([c[0], c[1]])).collect()
|
||||
};
|
||||
let proto = Frame::new(w, h, chroma, depth, false);
|
||||
let per = frame_samples(&proto);
|
||||
assert_eq!(s.len(), per * frames as usize);
|
||||
|
||||
@@ -68,17 +68,20 @@ fn roundtrip_all_profiles() {
|
||||
|
||||
#[test]
|
||||
fn rejects_bad_input() {
|
||||
let mut enc = Encoder::new(Profile::Hq, 64, 64);
|
||||
let mut enc = Encoder::new(Profile::Hq, 64, 64).unwrap();
|
||||
assert!(enc.encode(&Frame::new(32, 64, ChromaFormat::Yuv422, 8, false)).is_err());
|
||||
assert!(enc.encode(&Frame::new(64, 64, ChromaFormat::Yuv444, 8, false)).is_err());
|
||||
assert!(Encoder::with_config(EncoderConfig { bit_depth: 10, ..EncoderConfig::new(Profile::Hq, 64, 64) }).is_err());
|
||||
assert!(decode_frame(&[0u8; 100]).is_err());
|
||||
assert!(decode_frame(&[0u8; 1000]).is_err());
|
||||
// An unsupported raster is an error, not a panic.
|
||||
assert!(Encoder::new(Profile::Hq, 0, 0).is_err());
|
||||
assert!(Encoder::new(Profile::Hq, 20_000, 64).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_streams_never_panic() {
|
||||
let mut enc = Encoder::new(Profile::Hq, 256, 128);
|
||||
let mut enc = Encoder::new(Profile::Hq, 256, 128).unwrap();
|
||||
let good = enc.encode(&picture(256, 128, ChromaFormat::Yuv422, 8, 1)).unwrap();
|
||||
let mut seed = 0x1234_5678u32;
|
||||
let mut rnd = || {
|
||||
|
||||
@@ -105,8 +105,9 @@ pub fn merge_captions(seq: &mut Sequence, ids: &[ClipId]) -> Result<ClipId> {
|
||||
if idx.windows(2).any(|w| w[1] != w[0] + 1) {
|
||||
return Err(EditError::Other("captions to merge must be next to each other".into()));
|
||||
}
|
||||
let first = idx[0];
|
||||
let last = *idx.last().expect("non-empty");
|
||||
let (Some(&first), Some(&last)) = (idx.first(), idx.last()) else {
|
||||
return Err(EditError::Nothing);
|
||||
};
|
||||
let end = tr.captions[last].end();
|
||||
let text: Vec<String> = idx.iter().map(|&i| tr.captions[i].text.trim().to_string()).filter(|t| !t.is_empty()).collect();
|
||||
let keep = tr.captions[first].id;
|
||||
@@ -205,7 +206,7 @@ pub fn move_captions(seq: &mut Sequence, ids: &[ClipId], delta: Tick) -> Result<
|
||||
return Err(EditError::Nothing);
|
||||
}
|
||||
for tid in tracks {
|
||||
let tr = seq.caption_track_mut(tid).expect("checked");
|
||||
let Some(tr) = seq.caption_track_mut(tid) else { continue };
|
||||
for c in tr.captions.iter_mut().filter(|c| ids.contains(&c.id)) {
|
||||
c.start += d;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
//!
|
||||
//! Keyframes are stored in media time, so trims and splits never need to move them.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub mod captions;
|
||||
pub mod multicam;
|
||||
pub mod through;
|
||||
@@ -181,6 +183,34 @@ pub fn remove_orphan_transitions(track: &mut Track) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Where a transition sits: the incoming clip's start, or the outgoing clip's end for a fade to nothing.
|
||||
fn transition_anchor(track: &Track, tr: &Transition) -> Option<Tick> {
|
||||
match (tr.from, tr.to) {
|
||||
(_, Some(to)) => track.item(to).map(|i| i.start),
|
||||
(Some(from), None) => track.item(from).map(|i| i.end()),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Keep transitions on their edit points after an edit moved cuts (ripple trim, roll, slide, ripple
|
||||
/// speed change): each transition shifts by as much as its anchor clip edge moved.
|
||||
fn transitions_follow_cuts(before: &Sequence, after: &mut Sequence) {
|
||||
for track in after.all_tracks_mut() {
|
||||
let Some(old) = before.track(track.id) else { continue };
|
||||
let shifts: Vec<Tick> = track
|
||||
.transitions
|
||||
.iter()
|
||||
.map(|tr| match (transition_anchor(old, tr), transition_anchor(track, tr)) {
|
||||
(Some(a), Some(b)) => b - a,
|
||||
_ => Tick::ZERO,
|
||||
})
|
||||
.collect();
|
||||
for (tr, d) in track.transitions.iter_mut().zip(shifts) {
|
||||
tr.start += d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn place(track: &mut Track, item: TrackItem) {
|
||||
let idx = track.items.partition_point(|i| i.start <= item.start);
|
||||
track.items.insert(idx, item);
|
||||
@@ -520,6 +550,7 @@ pub fn trim(seq: &mut Sequence, clip: ClipId, edge: Edge, mode: TrimMode, delta:
|
||||
tr.sort();
|
||||
}
|
||||
}
|
||||
transitions_follow_cuts(seq, &mut work);
|
||||
}
|
||||
work.check().map_err(EditError::Other)?;
|
||||
*seq = work;
|
||||
@@ -597,6 +628,7 @@ pub fn ripple_trim_group(seq: &mut Sequence, clips: &[ClipId], edge: Edge, delta
|
||||
}
|
||||
tr.sort();
|
||||
}
|
||||
transitions_follow_cuts(seq, &mut work);
|
||||
work.check().map_err(EditError::Other)?;
|
||||
*seq = work;
|
||||
Ok(d)
|
||||
@@ -620,6 +652,7 @@ pub fn roll(seq: &mut Sequence, left: ClipId, right: ClipId, delta: Tick, ctx: &
|
||||
return Ok(d);
|
||||
}
|
||||
let (ls, rs) = (l.speed.abs(), r.speed.abs());
|
||||
let before = seq.clone();
|
||||
{
|
||||
let (_, l) = seq.find_item_mut(left).ok_or(EditError::NoItem(left))?;
|
||||
l.duration += d;
|
||||
@@ -631,6 +664,7 @@ pub fn roll(seq: &mut Sequence, left: ClipId, right: ClipId, delta: Tick, ctx: &
|
||||
r.source_in += src_of(d, rs);
|
||||
}
|
||||
let _ = ls;
|
||||
transitions_follow_cuts(&before, seq);
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
@@ -673,6 +707,7 @@ pub fn slide(seq: &mut Sequence, clip: ClipId, delta: Tick, ctx: &mut EditCtx) -
|
||||
if d == Tick::ZERO {
|
||||
return Ok(d);
|
||||
}
|
||||
let before = seq.clone();
|
||||
let t = seq.track_mut(tid).ok_or(EditError::NoTrack(tid))?;
|
||||
if let Some(p) = prev {
|
||||
let pi = t.item_mut(p.id).ok_or(EditError::NoItem(p.id))?;
|
||||
@@ -686,6 +721,7 @@ pub fn slide(seq: &mut Sequence, clip: ClipId, delta: Tick, ctx: &mut EditCtx) -
|
||||
}
|
||||
let me = t.item_mut(clip).ok_or(EditError::NoItem(clip))?;
|
||||
me.start += d;
|
||||
transitions_follow_cuts(&before, seq);
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
@@ -728,6 +764,7 @@ pub fn set_speed(seq: &mut Sequence, clip: ClipId, speed: f64, reverse: bool, ri
|
||||
new_dur = new_dur.min(next_start - it.start);
|
||||
}
|
||||
let delta = new_dur - it.duration;
|
||||
let before = ripple.then(|| seq.clone());
|
||||
{
|
||||
let (_, it) = seq.find_item_mut(clip).ok_or(EditError::NoItem(clip))?;
|
||||
it.speed = speed;
|
||||
@@ -746,6 +783,9 @@ pub fn set_speed(seq: &mut Sequence, clip: ClipId, speed: f64, reverse: bool, ri
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(before) = before {
|
||||
transitions_follow_cuts(&before, seq);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -210,6 +210,53 @@ fn roll_slip_slide() {
|
||||
fx.seq.check().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transitions_stay_on_their_cuts() {
|
||||
let mut fx = Fx::new();
|
||||
let v1 = fx.v(0);
|
||||
let a = fx.put(v1, 0, 10, 10);
|
||||
let b = fx.put(v1, 10, 10, 10);
|
||||
let c = fx.put(v1, 20, 10, 10);
|
||||
let cross = |id, start, from, to| Transition {
|
||||
id: TransitionId(id),
|
||||
effect: filmcraft_project::find_effect("cross_dissolve").unwrap().instance(),
|
||||
start: f(start),
|
||||
duration: f(4),
|
||||
from,
|
||||
to,
|
||||
align: Default::default(),
|
||||
reverse: false,
|
||||
};
|
||||
let t = fx.seq.track_mut(v1).unwrap();
|
||||
t.transitions = vec![cross(1, 8, Some(a), Some(b)), cross(2, 18, Some(b), Some(c)), cross(3, 26, Some(c), None)];
|
||||
// every transition is centred on its cut (or ends with the clip it fades out)
|
||||
let check = |fx: &Fx, what: &str| {
|
||||
let t = fx.seq.track(v1).unwrap();
|
||||
for tr in &t.transitions {
|
||||
let cut = match tr.to {
|
||||
Some(to) => t.item(to).unwrap().start,
|
||||
None => t.item(tr.from.unwrap()).unwrap().end() - f(2),
|
||||
};
|
||||
assert_eq!(tr.start + f(2), cut, "{what}: transition {:?} left its cut", tr.id);
|
||||
}
|
||||
assert_eq!(t.transitions.len(), 3, "{what}");
|
||||
};
|
||||
let mut n = fx.next;
|
||||
trim(&mut fx.seq, a, Edge::Out, TrimMode::Ripple, f(4), &mut Fx::ctx(&mut n)).unwrap();
|
||||
check(&fx, "ripple trim out");
|
||||
trim(&mut fx.seq, b, Edge::In, TrimMode::Ripple, f(3), &mut Fx::ctx(&mut n)).unwrap();
|
||||
check(&fx, "ripple trim in");
|
||||
ripple_trim_group(&mut fx.seq, &[b], Edge::Out, -f(2), &mut Fx::ctx(&mut n)).unwrap();
|
||||
check(&fx, "ripple trim group");
|
||||
roll(&mut fx.seq, b, c, f(2), &mut Fx::ctx(&mut n)).unwrap();
|
||||
check(&fx, "roll");
|
||||
slide(&mut fx.seq, b, -f(3), &mut Fx::ctx(&mut n)).unwrap();
|
||||
check(&fx, "slide");
|
||||
set_speed(&mut fx.seq, b, 0.5, false, true, &mut Fx::ctx(&mut n)).unwrap();
|
||||
check(&fx, "ripple speed change");
|
||||
fx.seq.check().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_stretch_and_speed() {
|
||||
let mut fx = Fx::new();
|
||||
|
||||
@@ -344,7 +344,9 @@ pub fn caption_blocks(words: &[SeqWord], rules: &CaptionRules, rate: FrameRate)
|
||||
continue;
|
||||
}
|
||||
if fits_line {
|
||||
*lines.last_mut().expect("non-empty") += 1 + len;
|
||||
if let Some(l) = lines.last_mut() {
|
||||
*l += 1 + len;
|
||||
}
|
||||
} else {
|
||||
lines.push(len);
|
||||
}
|
||||
@@ -362,7 +364,7 @@ pub fn caption_blocks(words: &[SeqWord], rules: &CaptionRules, rate: FrameRate)
|
||||
for g in groups {
|
||||
let mut text_lines: Vec<String> = vec![String::new()];
|
||||
for w in &words[g.clone()] {
|
||||
let l = text_lines.last_mut().expect("non-empty");
|
||||
let Some(l) = text_lines.last_mut() else { break };
|
||||
if l.is_empty() {
|
||||
l.push_str(&w.text);
|
||||
} else if l.chars().count() + 1 + w.text.chars().count() <= max_chars {
|
||||
|
||||
@@ -1097,7 +1097,7 @@ fn subclip_plan(s: &Session, p: &Value) -> Result<(ItemId, TimeRange, String, La
|
||||
} else {
|
||||
let item = item_p(p, "item").or_else(|| subclip_source(s)).ok_or_else(|| bad("clip.makeSubclip", "open a clip in the Source monitor"))?;
|
||||
let (root, m, sub) = media_root(&s.project, item).ok_or_else(|| bad("clip.makeSubclip", "only media clips can be subclipped"))?;
|
||||
let pi = s.project.item(item).expect("resolved");
|
||||
let pi = s.project.item(item).ok_or_else(|| EngineError::Other("no such item".into()))?;
|
||||
let rate = m.frame_rate();
|
||||
let still = s.prefs.timeline.still_duration(rate);
|
||||
let is_still = matches!(m.info.kind, filmcraft_media::MediaKind::Still);
|
||||
@@ -1246,7 +1246,7 @@ fn audio_channels(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut out = Vec::new();
|
||||
let mut maps = Vec::new();
|
||||
for i in &items {
|
||||
let m = s.project.item(*i).and_then(|it| it.as_media()).expect("filtered");
|
||||
let Some(m) = s.project.item(*i).and_then(|it| it.as_media()) else { continue };
|
||||
let n = m.info.audio.as_ref().map_or(2, |a| a.channels) as u16;
|
||||
let fmt = format.unwrap_or_else(|| m.interpret.audio_channels.as_ref().map_or(AudioChannels::Stereo, |a| a.format));
|
||||
let map = match &custom {
|
||||
@@ -1603,7 +1603,7 @@ fn extract_audio(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
.ok_or_else(|| EngineError::Other("save the project first: extracted audio is written next to it".into()))?,
|
||||
};
|
||||
let pi = s.project.item(item).expect("resolved");
|
||||
let pi = s.project.item(item).ok_or_else(|| EngineError::Other("no such item".into()))?;
|
||||
let name = pi.name.clone();
|
||||
let stem = std::path::Path::new(&name).file_stem().map(|x| x.to_string_lossy().into_owned()).unwrap_or_else(|| name.clone());
|
||||
let src = s.media.full_res_source(&s.project, root, &*s.services).ok_or_else(|| EngineError::Other(format!("{name}: media is offline")))?;
|
||||
|
||||
@@ -33,7 +33,7 @@ fn lumetri_text(s: &Session, clip: ClipId, p: &str) -> String {
|
||||
fn mean(img: &filmcraft_render::Image) -> [f32; 3] {
|
||||
assert!(img.px.iter().any(|v| *v > 0.01), "blank render");
|
||||
let mut m = [0f64; 3];
|
||||
for p in img.px.chunks_exact(4) {
|
||||
for p in img.px.as_chunks::<4>().0 {
|
||||
for k in 0..3 {
|
||||
m[k] += p[k] as f64;
|
||||
}
|
||||
@@ -213,7 +213,7 @@ fn lumetri_presets_list_apply_and_thumbnails() {
|
||||
assert!(it.effects[k + 1..].iter().all(|e| e.def().is_some_and(|d| d.intrinsic)) || k + 1 == it.effects.len());
|
||||
assert_eq!(it.effects[k].param("saturation").unwrap().value, ParamValue::Float(0.0));
|
||||
let img = s.render_program(0.25).unwrap();
|
||||
let grey = img.px.chunks_exact(4).filter(|p| p[3] > 0.5).all(|p| (p[0] - p[1]).abs() < 0.01 && (p[1] - p[2]).abs() < 0.01);
|
||||
let grey = img.px.as_chunks::<4>().0.iter().filter(|p| p[3] > 0.5).all(|p| (p[0] - p[1]).abs() < 0.01 && (p[1] - p[2]).abs() < 0.01);
|
||||
assert!(grey, "the program is black and white");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(s.active_sequence().unwrap().find_item(clip).unwrap().1.effects.len(), before);
|
||||
@@ -242,7 +242,7 @@ fn hdr_lumetri_and_mastering_metadata_through_commands() {
|
||||
for k in 0..3 {
|
||||
assert!((m1[k] / m0[k] - 2.0).abs() < 0.08, "channel {k}: {m0:?} → {m1:?}");
|
||||
}
|
||||
assert!(up.px.chunks_exact(4).any(|p| p[1] > 1.2), "values above reference white survive");
|
||||
assert!(up.px.as_chunks::<4>().0.iter().any(|p| p[1] > 1.2), "values above reference white survive");
|
||||
// HDR White / HDR Specular / HDR Range are ordinary Lumetri parameters
|
||||
s.execute("effects.setParam", json!({"clip": clip.0, "effect": idx, "param": "hdr_white", "value": 2000.0})).unwrap();
|
||||
s.execute("effects.setParam", json!({"clip": clip.0, "effect": idx, "param": "hdr_specular", "value": -50.0})).unwrap();
|
||||
|
||||
@@ -362,7 +362,7 @@ pub(crate) fn place_item(
|
||||
let frame = (seq.settings.width, seq.settings.height);
|
||||
let mut placements = Vec::new();
|
||||
let link = if has_v && has_a { Some(p.alloc_id()) } else { None };
|
||||
if has_v {
|
||||
if let Some(vdest) = vdest.filter(|_| has_v) {
|
||||
let mut v = p.make_track_item(item, TrackKind::Video, at, range, rate).ok_or_else(|| bad(label, "bad item"))?;
|
||||
v.link = link;
|
||||
for e in &mut v.effects {
|
||||
@@ -371,10 +371,9 @@ pub(crate) fn place_item(
|
||||
if is_media && src_size != frame {
|
||||
crate::settings::apply_media_scaling(&mut v, &scaling, frame, src_size);
|
||||
}
|
||||
placements.push((vdest.expect("checked"), v));
|
||||
placements.push((vdest, v));
|
||||
}
|
||||
if has_a {
|
||||
let adest = adest.expect("checked");
|
||||
if let Some(adest) = adest.filter(|_| has_a) {
|
||||
let (arange, aat) = audio_split.unwrap_or((range, at));
|
||||
let mut a = p.make_track_item(item, TrackKind::Audio, aat, arange, rate).ok_or_else(|| bad(label, "bad item"))?;
|
||||
// Modify ▸ Audio Channels with several audio clips: one per clip, on the tracks below
|
||||
@@ -675,27 +674,27 @@ fn build() -> Vec<CommandSpec> {
|
||||
Ok(id) => ids.push(id.0),
|
||||
Err(e) => errors.push(format!("{path}: {e}")),
|
||||
},
|
||||
Ok(b) if crate::captions::detect(&path, &b).is_some() => {
|
||||
let fmt = crate::captions::detect(&path, &b).expect("detected");
|
||||
match crate::captions::import(s, &path, &b, fmt, None) {
|
||||
Ok(r) => reports.push(r),
|
||||
Err(e) => errors.push(format!("{path}: {e}")),
|
||||
}
|
||||
}
|
||||
Ok(b) if crate::interchange::detect(&path, &b).is_some() => {
|
||||
let fmt = crate::interchange::detect(&path, &b).expect("detected");
|
||||
match crate::interchange::import(s, &path, &b, fmt) {
|
||||
Ok(r) => {
|
||||
sequences.extend(r["sequences"].as_array().cloned().unwrap_or_default());
|
||||
reports.push(r);
|
||||
Ok(b) => {
|
||||
if let Some(fmt) = crate::captions::detect(&path, &b) {
|
||||
match crate::captions::import(s, &path, &b, fmt, None) {
|
||||
Ok(r) => reports.push(r),
|
||||
Err(e) => errors.push(format!("{path}: {e}")),
|
||||
}
|
||||
} else if let Some(fmt) = crate::interchange::detect(&path, &b) {
|
||||
match crate::interchange::import(s, &path, &b, fmt) {
|
||||
Ok(r) => {
|
||||
sequences.extend(r["sequences"].as_array().cloned().unwrap_or_default());
|
||||
reports.push(r);
|
||||
}
|
||||
Err(e) => errors.push(format!("{path}: {e}")),
|
||||
}
|
||||
} else {
|
||||
match import_bytes(s, &path, b.into(), bin) {
|
||||
Ok(id) => ids.push(id.0),
|
||||
Err(e) => errors.push(format!("{path}: {e}")),
|
||||
}
|
||||
Err(e) => errors.push(format!("{path}: {e}")),
|
||||
}
|
||||
}
|
||||
Ok(b) => match import_bytes(s, &path, b.into(), bin) {
|
||||
Ok(id) => ids.push(id.0),
|
||||
Err(e) => errors.push(format!("{path}: {e}")),
|
||||
},
|
||||
Err(e) => errors.push(format!("{path}: {e}")),
|
||||
}
|
||||
}
|
||||
@@ -1280,7 +1279,7 @@ fn build() -> Vec<CommandSpec> {
|
||||
let p = p.clone();
|
||||
s.edit("Sequence Settings", |pr, _| {
|
||||
if let Some(n) = str_p(&p, "name") {
|
||||
pr.item_mut(id).expect("seq").name = n.to_string();
|
||||
pr.item_mut(id).ok_or(EngineError::NoSequence)?.name = n.to_string();
|
||||
}
|
||||
let q = pr.sequence_mut(id).ok_or(EngineError::NoSequence)?;
|
||||
if let Some(w) = u64_p(&p, "width") {
|
||||
@@ -1793,7 +1792,21 @@ fn build() -> Vec<CommandSpec> {
|
||||
.map(Tick)
|
||||
.or_else(|| p.get("deltaFrames").and_then(Value::as_i64).map(|f| s.sequence_rate().tick_of(f)))
|
||||
.unwrap_or_default();
|
||||
s.edit_sequence("Slide", |q, ctx, _| Ok(edit::slide(q, c, d, ctx)?))?;
|
||||
let clips = with_links(s, &[c]);
|
||||
s.edit_sequence("Slide", |q, ctx, _| {
|
||||
// clamp across all linked partners, then slide them by the common delta
|
||||
let mut dd = d;
|
||||
for c in &clips {
|
||||
let x = edit::slide(&mut q.clone(), *c, dd, ctx)?;
|
||||
if x.abs() < dd.abs() {
|
||||
dd = x;
|
||||
}
|
||||
}
|
||||
for c in &clips {
|
||||
edit::slide(q, *c, dd, ctx)?;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Value::Null)
|
||||
}),
|
||||
cmd!("timeline.rateStretch", "Rate Stretch", [], None, r#"{"clip":id,"edge":"in|out","delta":ticks}"#, has_seq, |s, p| {
|
||||
@@ -2440,7 +2453,7 @@ fn nest(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit("Nest", |pr, st| {
|
||||
let nid = pr.new_sequence(&name, q.settings.clone(), q.video_tracks.len(), q.audio_tracks.len(), None);
|
||||
{
|
||||
let nq = pr.sequence_mut(nid).expect("new");
|
||||
let nq = pr.sequence_mut(nid).ok_or(EngineError::NoSequence)?;
|
||||
for (k, ti, it) in &items {
|
||||
let mut it = it.clone();
|
||||
it.start -= start;
|
||||
@@ -2451,11 +2464,15 @@ fn nest(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
let rate = q.settings.frame_rate;
|
||||
let mut v = pr.make_track_item(nid, TrackKind::Video, start, TimeRange::new(Tick::ZERO, end - start), rate).expect("item");
|
||||
let mut v = pr
|
||||
.make_track_item(nid, TrackKind::Video, start, TimeRange::new(Tick::ZERO, end - start), rate)
|
||||
.ok_or_else(|| EngineError::Other("cannot place the nested sequence".into()))?;
|
||||
for e in &mut v.effects {
|
||||
resolve_auto_points(e, (q.settings.width, q.settings.height), (q.settings.width, q.settings.height));
|
||||
}
|
||||
let a = pr.make_track_item(nid, TrackKind::Audio, start, TimeRange::new(Tick::ZERO, end - start), rate).expect("item");
|
||||
let a = pr
|
||||
.make_track_item(nid, TrackKind::Audio, start, TimeRange::new(Tick::ZERO, end - start), rate)
|
||||
.ok_or_else(|| EngineError::Other("cannot place the nested sequence".into()))?;
|
||||
let link = pr.alloc_id();
|
||||
let seq = pr.sequence_mut(seq_id).ok_or(EngineError::NoSequence)?;
|
||||
edit::delete_items(seq, &sel);
|
||||
|
||||
+20
-11
@@ -44,6 +44,15 @@ pub fn add_generator(p: &mut Project, pool: &MediaPool, src: GeneratorSource, na
|
||||
|
||||
/// Build the demo project. Returns (project, main sequence).
|
||||
pub fn demo_project(pool: &MediaPool) -> (Project, ItemId) {
|
||||
build_demo(pool).unwrap_or_else(|| {
|
||||
// Only reachable if the built-in items could not be placed: an empty demo, not a crash.
|
||||
let mut p = Project::new("FilmCraft Demo");
|
||||
let seq = p.new_sequence("Main Edit", SequenceSettings::default(), 3, 3, None);
|
||||
(p, seq)
|
||||
})
|
||||
}
|
||||
|
||||
fn build_demo(pool: &MediaPool) -> Option<(Project, ItemId)> {
|
||||
let mut p = Project::new("FilmCraft Demo");
|
||||
let footage = p.add_bin("Footage", None);
|
||||
let audio_bin = p.add_bin("Audio", None);
|
||||
@@ -108,8 +117,8 @@ pub fn demo_project(pool: &MediaPool) -> (Project, ItemId) {
|
||||
for (ci, sin, dur) in cuts {
|
||||
let src_range = TimeRange::new(r.snap(Tick::from_seconds_f64(sin)), r.snap(Tick::from_seconds_f64(dur)));
|
||||
let link = p.alloc_id();
|
||||
let mut v = p.make_track_item(clips[ci], TrackKind::Video, t, src_range, r).expect("item");
|
||||
let mut a = p.make_track_item(clips[ci], TrackKind::Audio, t, src_range, r).expect("item");
|
||||
let mut v = p.make_track_item(clips[ci], TrackKind::Video, t, src_range, r)?;
|
||||
let mut a = p.make_track_item(clips[ci], TrackKind::Audio, t, src_range, r)?;
|
||||
v.link = Some(link);
|
||||
a.link = Some(link);
|
||||
for e in &mut v.effects {
|
||||
@@ -118,14 +127,14 @@ pub fn demo_project(pool: &MediaPool) -> (Project, ItemId) {
|
||||
a.effect_mut("volume").map(|e| e.params.get_mut("level").map(|l| l.value = ParamValue::Float(-8.0)));
|
||||
let dur_t = v.duration;
|
||||
placed.push((v.id, a.id, t, dur_t));
|
||||
let s = p.sequence_mut(seq).expect("seq");
|
||||
let s = p.sequence_mut(seq)?;
|
||||
s.video_tracks[0].items.push(v);
|
||||
s.audio_tracks[0].items.push(a);
|
||||
t += dur_t;
|
||||
}
|
||||
// A lower-third-ish overlay on V2: the plasma clip scaled down in the corner with a drop shadow.
|
||||
let ov_range = TimeRange::new(Tick::ZERO, r.snap(Tick::from_seconds_f64(4.0)));
|
||||
let mut ov = p.make_track_item(clips[4], TrackKind::Video, r.snap(Tick::from_seconds_f64(6.0)), ov_range, r).expect("item");
|
||||
let mut ov = p.make_track_item(clips[4], TrackKind::Video, r.snap(Tick::from_seconds_f64(6.0)), ov_range, r)?;
|
||||
for e in &mut ov.effects {
|
||||
resolve_auto_points(e, (1920, 1080), (1920, 1080));
|
||||
}
|
||||
@@ -152,18 +161,18 @@ pub fn demo_project(pool: &MediaPool) -> (Project, ItemId) {
|
||||
lum.params.get_mut("temperature").map(|v| v.value = ParamValue::Float(18.0));
|
||||
lum.params.get_mut("contrast").map(|v| v.value = ParamValue::Float(22.0));
|
||||
lum.params.get_mut("vignette_amount").map(|v| v.value = ParamValue::Float(-1.4));
|
||||
let s = p.sequence_mut(seq).expect("seq");
|
||||
let s = p.sequence_mut(seq)?;
|
||||
if let Some(it) = s.video_tracks[0].items.get_mut(3) {
|
||||
it.effects.insert(0, lum);
|
||||
}
|
||||
}
|
||||
let s = p.sequence_mut(seq).expect("seq");
|
||||
let s = p.sequence_mut(seq)?;
|
||||
s.video_tracks[1].items.push(ov);
|
||||
// Transitions: cross dissolve between shots 1-2, dip to black 3-4, push 5-6, fade in at start.
|
||||
let tr_len = r.tick_of(24);
|
||||
let mk = |id: u64, eff: &str, from: Option<filmcraft_project::ClipId>, to: Option<filmcraft_project::ClipId>, start: Tick| Transition {
|
||||
id: TransitionId(id),
|
||||
effect: find_effect(eff).expect("fx").instance(),
|
||||
effect: crate::presets::effect_instance(eff),
|
||||
start,
|
||||
duration: tr_len,
|
||||
from,
|
||||
@@ -181,7 +190,7 @@ pub fn demo_project(pool: &MediaPool) -> (Project, ItemId) {
|
||||
s.video_tracks[0].transitions = trs;
|
||||
s.audio_tracks[0].transitions.push(Transition {
|
||||
id: TransitionId(900_010),
|
||||
effect: find_effect("constant_power").expect("fx").instance(),
|
||||
effect: crate::presets::effect_instance("constant_power"),
|
||||
start: cut(1) - tr_len.mul_ratio(1, 2),
|
||||
duration: tr_len,
|
||||
from: Some(placed[0].1),
|
||||
@@ -207,13 +216,13 @@ pub fn demo_project(pool: &MediaPool) -> (Project, ItemId) {
|
||||
});
|
||||
}
|
||||
// Music on A2 (under the whole edit), ducked -14 dB.
|
||||
let mut m = p.make_track_item(music, TrackKind::Audio, Tick::ZERO, TimeRange::new(Tick::ZERO, end), r).expect("music");
|
||||
let mut m = p.make_track_item(music, TrackKind::Audio, Tick::ZERO, TimeRange::new(Tick::ZERO, end), r)?;
|
||||
m.effect_mut("volume").map(|e| e.params.get_mut("level").map(|l| l.value = ParamValue::Float(-14.0)));
|
||||
m.label = Label::Caribbean;
|
||||
let s = p.sequence_mut(seq).expect("seq");
|
||||
let s = p.sequence_mut(seq)?;
|
||||
s.audio_tracks[1].items.push(m);
|
||||
for tr in s.all_tracks_mut() {
|
||||
tr.sort();
|
||||
}
|
||||
(p, seq)
|
||||
Some((p, seq))
|
||||
}
|
||||
@@ -411,6 +411,9 @@ fn ranges_resolve_and_sidecar_captions_are_written() {
|
||||
assert_eq!(frames(&mut s, json!({"preset": "PNG Sequence"})), io, "In/Out is the default");
|
||||
assert!(s.execute("export.resolve", json!({"range": "workArea"})).is_err(), "no work area");
|
||||
assert_eq!(frames(&mut s, json!({"preset": "Animated GIF 640×360", "range": "custom", "startSeconds": 0, "endSeconds": 2})), 30, "15 fps");
|
||||
let bare = frames(&mut s, json!({"preset": "Animated GIF 640×360", "startSeconds": 0, "endSeconds": 2}));
|
||||
assert_eq!(bare, 30, "start/end times without `range` are a custom range, not the In/Out or whole sequence");
|
||||
assert!(s.execute("export.resolve", json!({"startSeconds": 1})).is_err(), "a start without an end is an error");
|
||||
let res = s.execute("export.resolve", json!({"preset": "YouTube 2160p 4K Ultra HD"})).unwrap();
|
||||
assert_eq!(res["output"]["width"], 3840);
|
||||
assert!(res["summary"]["video"].as_str().unwrap().contains("3840x2160"));
|
||||
|
||||
@@ -244,12 +244,14 @@ pub fn sequence_param(s: &Session, p: &Value, cmd: &str) -> Result<ItemId> {
|
||||
}
|
||||
|
||||
/// Export ▸ Range: `entire` | `inOut` | `workArea` | `custom` (`start…` / `end…` times).
|
||||
/// None = the default (In/Out when set, else the whole sequence).
|
||||
/// None = the default (In/Out when set, else the whole sequence); bare `start…` / `end…` times
|
||||
/// without a `range` mean `custom`.
|
||||
pub fn range_param(s: &Session, project: &Project, seq: ItemId, v: Option<&Value>, p: &Value, cmd: &str) -> Result<Option<TimeRange>> {
|
||||
let q = project.sequence(seq).ok_or(EngineError::NoSequence)?;
|
||||
let fd = q.settings.frame_rate.frame_duration();
|
||||
let whole = TimeRange::from_bounds(Tick::ZERO, q.duration().max(fd));
|
||||
let mode = match v {
|
||||
None | Some(Value::Null) if time_p(s, p, "start").is_some() || time_p(s, p, "end").is_some() => "custom".to_string(),
|
||||
None | Some(Value::Null) => return Ok(None),
|
||||
Some(Value::String(m)) => m.to_ascii_lowercase().replace(['-', '_', ' ', '/'], ""),
|
||||
Some(o @ Value::Object(_)) => return range_param(s, project, seq, o.get("mode").or(Some(&json!("custom"))), o, cmd),
|
||||
@@ -385,6 +387,7 @@ pub fn spawn_export(s: &mut Session, project: Arc<Project>, seq: ItemId, mut set
|
||||
}
|
||||
*res.lock().unwrap_or_else(|x| x.into_inner()) = Some(r);
|
||||
};
|
||||
let run = crate::export_tools::guard_job(job.progress.clone(), job.result.clone(), run);
|
||||
s.jobs.push(job);
|
||||
if wait || cfg!(target_arch = "wasm32") {
|
||||
run();
|
||||
@@ -394,6 +397,24 @@ pub fn spawn_export(s: &mut Session, project: Arc<Project>, seq: ItemId, mut set
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
/// Last-resort guard for a background job body (export, previews, proxies, …): a panic marks the
|
||||
/// job failed with an error instead of killing its thread and leaving the job "running" forever.
|
||||
/// The panic hook has already logged where it happened.
|
||||
pub(crate) fn guard_job(
|
||||
progress: Arc<filmcraft_export::Progress>,
|
||||
result: Arc<std::sync::Mutex<Option<std::result::Result<filmcraft_export::Report, String>>>>,
|
||||
run: impl FnOnce() + Send + 'static,
|
||||
) -> impl FnOnce() + Send + 'static {
|
||||
move || {
|
||||
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)).is_err() {
|
||||
let msg = "internal error (see the crash log); the job was stopped".to_string();
|
||||
*progress.error.lock().unwrap_or_else(|x| x.into_inner()) = Some(msg.clone());
|
||||
progress.finished.store(true, Ordering::Relaxed);
|
||||
*result.lock().unwrap_or_else(|x| x.into_inner()) = Some(Err(msg));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn file_name(path: &str) -> String {
|
||||
Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| path.to_string())
|
||||
}
|
||||
@@ -1008,3 +1029,20 @@ pub(crate) fn commands() -> Vec<CommandSpec> {
|
||||
spec("export.quick", "Quick Export", r#"{"preset":str?,"path":str?,"wait":bool?, …settings params}"#, has_seq, quick, true),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod guard_tests {
|
||||
use super::*;
|
||||
|
||||
/// A panicking job body must leave the job finished with an error, not "running" forever.
|
||||
#[test]
|
||||
fn panicking_job_is_reported_as_failed() {
|
||||
let progress: Arc<filmcraft_export::Progress> = Arc::default();
|
||||
let result: Arc<std::sync::Mutex<Option<std::result::Result<filmcraft_export::Report, String>>>> = Arc::default();
|
||||
let run = guard_job(progress.clone(), result.clone(), || panic!("job bug"));
|
||||
std::thread::spawn(run).join().expect("the guard catches the panic");
|
||||
assert!(progress.finished.load(Ordering::Relaxed));
|
||||
assert!(progress.error.lock().unwrap().is_some());
|
||||
assert!(matches!(&*result.lock().unwrap(), Some(Err(_))));
|
||||
}
|
||||
}
|
||||
@@ -157,15 +157,15 @@ pub fn thumbnail(t: &GraphicsTemplate, w: u32) -> (u32, u32, Vec<u8>) {
|
||||
let mut p = Project::new("thumb");
|
||||
let rate = filmcraft_time::FrameRate::FPS_30;
|
||||
let g = p.add_item("G", Label::Rose, ItemKind::Graphic { width: t.canvas[0], height: t.canvas[1], rate }, None);
|
||||
let mut it = p
|
||||
.make_track_item(
|
||||
g,
|
||||
filmcraft_project::TrackKind::Video,
|
||||
Tick::ZERO,
|
||||
filmcraft_time::TimeRange::new(Tick::ZERO, t.duration.max(rate.frame_duration())),
|
||||
rate,
|
||||
)
|
||||
.expect("item");
|
||||
let Some(mut it) = p.make_track_item(
|
||||
g,
|
||||
filmcraft_project::TrackKind::Video,
|
||||
Tick::ZERO,
|
||||
filmcraft_time::TimeRange::new(Tick::ZERO, t.duration.max(rate.frame_duration())),
|
||||
rate,
|
||||
) else {
|
||||
return (w, h, vec![0; w as usize * h as usize * 4]);
|
||||
};
|
||||
it.effects.extend(t.layers.iter().cloned());
|
||||
it.graphic = Some(Box::new(meta));
|
||||
// a moment into the clip, so intro animations have settled a little
|
||||
|
||||
@@ -551,11 +551,11 @@ macro_rules! arrange_cmd {
|
||||
fn new_shape_cmd(s: &mut Session, p: &Value, shape: &str) -> Result<Value> {
|
||||
let mut q = p.clone();
|
||||
q["shape"] = json!(shape);
|
||||
(find_spec("graphics.newShape").run)(s, &q)
|
||||
(find_spec("graphics.newShape")?.run)(s, &q)
|
||||
}
|
||||
|
||||
fn find_spec(id: &str) -> CommandSpec {
|
||||
commands().into_iter().find(|c| c.id == id).expect("graphics command")
|
||||
fn find_spec(id: &str) -> Result<CommandSpec> {
|
||||
commands().into_iter().find(|c| c.id == id).ok_or_else(|| EngineError::UnknownCommand(id.to_string()))
|
||||
}
|
||||
|
||||
/// Graphics menu commands (New Layer shapes, Align/Distribute/Arrange/Select submenus, resets).
|
||||
@@ -576,7 +576,7 @@ fn menu_commands() -> Vec<CommandSpec> {
|
||||
|s, p| {
|
||||
let mut p = if p.is_object() { p.clone() } else { json!({}) };
|
||||
p["vertical"] = json!(true);
|
||||
(find_spec("graphics.newText").run)(s, &p)
|
||||
(find_spec("graphics.newText")?.run)(s, &p)
|
||||
},
|
||||
),
|
||||
spec(
|
||||
@@ -731,7 +731,7 @@ fn menu_commands() -> Vec<CommandSpec> {
|
||||
let rate = q.settings.frame_rate;
|
||||
let want = rate.snap_nearest(Tick::from_seconds_f64(seconds)).max(rate.frame_duration());
|
||||
let tr = q.video_tracks.iter_mut().find(|tr| tr.items.iter().any(|i| i.id == clip)).ok_or(filmcraft_edit::EditError::NoItem(clip))?;
|
||||
let i = tr.items.iter().position(|i| i.id == clip).expect("found");
|
||||
let i = tr.items.iter().position(|i| i.id == clip).ok_or(filmcraft_edit::EditError::NoItem(clip))?;
|
||||
let start = tr.items[i].start;
|
||||
let limit = tr.items.get(i + 1).map_or(want, |n| n.start - start);
|
||||
let d = want.min(limit).max(rate.frame_duration());
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
//! The project is an `Arc<Project>` edited copy-on-write; undo keeps whole-project snapshots (cheap
|
||||
//! thanks to structural sharing of untouched items).
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub mod aaf_omf;
|
||||
pub mod autosave;
|
||||
pub mod captions;
|
||||
@@ -599,7 +601,11 @@ impl Session {
|
||||
return Err(e);
|
||||
}
|
||||
self.exec_depth += 1;
|
||||
let r = (spec.run)(self, ¶ms);
|
||||
// Last-resort guard: a panic in a command (a bug that escaped the never-crash rules)
|
||||
// becomes an error for the caller (UI, CLI, control channel, MCP) instead of taking the
|
||||
// session and its unsaved project down. The panic hook has logged where it happened.
|
||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (spec.run)(self, ¶ms)))
|
||||
.unwrap_or_else(|_| Err(EngineError::Other(format!("internal error in `{id}` (see the crash log); the command did not complete"))));
|
||||
self.exec_depth -= 1;
|
||||
// source graphics: an edited instance updates the shared layers and the other instances
|
||||
if r.is_ok() && spec.journal && id.starts_with("graphics.") && !self.project.source_graphics.is_empty() {
|
||||
|
||||
@@ -634,6 +634,7 @@ fn track(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
prog.finished.store(true, Ordering::Relaxed);
|
||||
*lock(&res) = Some(r);
|
||||
};
|
||||
let run = crate::export_tools::guard_job(job.progress.clone(), job.result.clone(), run);
|
||||
s.jobs.push(job);
|
||||
s.mask_jobs.push(PendingTrack { job: id, target: MaskSel { clip, effect: ei, mask: mi }, seq: seq_id, keys, applied: 0 });
|
||||
let wait = crate::commands::bool_p(p, "wait").unwrap_or(false);
|
||||
|
||||
@@ -101,7 +101,7 @@ pub fn psnr(a: &[u8], b: &[u8]) -> f64 {
|
||||
assert_eq!(a.len(), b.len());
|
||||
let mut se = 0.0;
|
||||
let mut n = 0.0;
|
||||
for (x, y) in a.chunks_exact(4).zip(b.chunks_exact(4)) {
|
||||
for (x, y) in a.as_chunks::<4>().0.iter().zip(b.as_chunks::<4>().0) {
|
||||
for c in 0..3 {
|
||||
let d = x[c] as f64 - y[c] as f64;
|
||||
se += d * d;
|
||||
|
||||
@@ -554,7 +554,7 @@ fn merge_clips(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
a.link = Some(link);
|
||||
a_items.push(a);
|
||||
}
|
||||
let q = pr.sequence_mut(sid).expect("new sequence");
|
||||
let q = pr.sequence_mut(sid).ok_or(EngineError::NoSequence)?;
|
||||
for (t, it) in q.video_tracks.iter_mut().zip(v_items) {
|
||||
t.items.push(it);
|
||||
}
|
||||
@@ -616,7 +616,7 @@ fn create_multicam(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (mut vi, mut ai) = (0usize, 0usize);
|
||||
let mut placed: Vec<(TrackKind, usize, TrackItem)> = Vec::new();
|
||||
let (vtracks, atracks): (Vec<TrackId>, Vec<TrackId>) = {
|
||||
let q = pr.sequence(sid).expect("new sequence");
|
||||
let q = pr.sequence(sid).ok_or(EngineError::NoSequence)?;
|
||||
(q.video_tracks.iter().map(|t| t.id).collect(), q.audio_tracks.iter().map(|t| t.id).collect())
|
||||
};
|
||||
for (k, c) in clips.iter().enumerate() {
|
||||
@@ -640,7 +640,7 @@ fn create_multicam(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
cameras.push(cam);
|
||||
}
|
||||
{
|
||||
let q = pr.sequence_mut(sid).expect("new sequence");
|
||||
let q = pr.sequence_mut(sid).ok_or(EngineError::NoSequence)?;
|
||||
for (kind, idx, it) in placed {
|
||||
q.tracks_mut(kind)[idx].items.push(it);
|
||||
}
|
||||
@@ -892,10 +892,14 @@ fn record_start(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn apply_pass(s: &mut Session, end: Tick) -> Result<usize> {
|
||||
let Some(seq_id) = s.mcrec.seq else { return Ok(0) };
|
||||
let merging = s.history.merge_key.as_deref() == Some(s.mcrec.key.as_str()) && s.history.undo.last().is_some_and(|u| u.0 == RECORD_LABEL);
|
||||
if !merging || s.mcrec.base.is_none() {
|
||||
s.mcrec.base = Some(s.project.clone());
|
||||
}
|
||||
let base = s.mcrec.base.clone().expect("set above");
|
||||
let base = match s.mcrec.base.clone() {
|
||||
Some(b) if merging => b,
|
||||
_ => {
|
||||
let b = s.project.clone();
|
||||
s.mcrec.base = Some(b.clone());
|
||||
b
|
||||
}
|
||||
};
|
||||
let (tracks, cuts, key) = (s.mcrec.tracks.clone(), s.mcrec.cuts.clone(), s.mcrec.key.clone());
|
||||
let Some(source) = s.mcrec.source else { return Ok(0) };
|
||||
let media = s.media.clone();
|
||||
|
||||
@@ -151,8 +151,22 @@ fn kf(t_secs: f64, v: ParamValue) -> Keyframe {
|
||||
Keyframe::new(Tick((t_secs * TICKS_PER_SECOND as f64).round() as i64), v)
|
||||
}
|
||||
|
||||
/// A fresh instance of a built-in effect (a bare instance named `id` if the registry lacks it,
|
||||
/// which the preset tests rule out).
|
||||
pub(crate) fn effect_instance(id: &str) -> EffectInstance {
|
||||
filmcraft_project::find_effect(id).map(|d| d.instance()).unwrap_or_else(|| EffectInstance {
|
||||
effect: id.to_string(),
|
||||
enabled: true,
|
||||
params: Default::default(),
|
||||
masks: Vec::new(),
|
||||
post_fader: false,
|
||||
essential: false,
|
||||
layer: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn effect(id: &str, params: &[(&str, ParamValue)]) -> EffectInstance {
|
||||
let mut e = filmcraft_project::find_effect(id).unwrap_or_else(|| panic!("built-in preset uses unknown effect {id}")).instance();
|
||||
let mut e = effect_instance(id);
|
||||
for (k, v) in params {
|
||||
if let Some(p) = e.params.get_mut(*k) {
|
||||
p.value = v.clone();
|
||||
|
||||
@@ -543,6 +543,7 @@ pub fn render(s: &mut Session, mode: RenderMode, p: &Value) -> Result<Value> {
|
||||
prog.finished.store(true, Ordering::Relaxed);
|
||||
*res.lock().unwrap_or_else(|x| x.into_inner()) = Some(r);
|
||||
};
|
||||
let run = crate::export_tools::guard_job(job.progress.clone(), job.result.clone(), run);
|
||||
s.jobs.push(job);
|
||||
let wait = p.get("wait").and_then(Value::as_bool).unwrap_or(false);
|
||||
if wait || cfg!(target_arch = "wasm32") {
|
||||
@@ -629,6 +630,7 @@ pub fn render_audio(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
prog.finished.store(true, Ordering::Relaxed);
|
||||
*res.lock().unwrap_or_else(|x| x.into_inner()) = Some(r);
|
||||
};
|
||||
let run = crate::export_tools::guard_job(job.progress.clone(), job.result.clone(), run);
|
||||
s.jobs.push(job);
|
||||
if p.get("wait").and_then(Value::as_bool).unwrap_or(false) || cfg!(target_arch = "wasm32") {
|
||||
run();
|
||||
@@ -680,7 +682,7 @@ pub fn read_wav_f32(b: &[u8]) -> Option<Vec<f32>> {
|
||||
if !float_stereo {
|
||||
return None;
|
||||
}
|
||||
return Some(body.chunks_exact(4).map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect());
|
||||
return Some(body.as_chunks::<4>().0.iter().map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]])).collect());
|
||||
}
|
||||
i += 8 + len + (len & 1);
|
||||
}
|
||||
|
||||
@@ -229,6 +229,7 @@ pub fn run(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
prog.finished.store(true, Ordering::Relaxed);
|
||||
*res.lock().unwrap_or_else(|x| x.into_inner()) = Some(r);
|
||||
};
|
||||
let run = crate::export_tools::guard_job(job.progress.clone(), job.result.clone(), run);
|
||||
s.jobs.push(job);
|
||||
if bool_p(p, "wait").unwrap_or(false) || cfg!(target_arch = "wasm32") {
|
||||
run();
|
||||
|
||||
@@ -241,6 +241,7 @@ pub fn start_job(s: &mut Session, label: String, work: Vec<(ItemId, String, &'st
|
||||
prog.finished.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
*res.lock().unwrap_or_else(|x| x.into_inner()) = Some(r);
|
||||
};
|
||||
let run = crate::export_tools::guard_job(job.progress.clone(), job.result.clone(), run);
|
||||
s.jobs.push(job);
|
||||
s.media_jobs.push(PendingJob { job: id, on_done, outputs });
|
||||
if wait || cfg!(target_arch = "wasm32") {
|
||||
|
||||
@@ -35,12 +35,11 @@ pub fn fingerprint(size: u64, head: &[u8], tail: &[u8]) -> u64 {
|
||||
h ^= h >> 29;
|
||||
};
|
||||
for part in [head, tail] {
|
||||
let mut chunks = part.chunks_exact(8);
|
||||
for c in &mut chunks {
|
||||
mix(u64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]]));
|
||||
let (chunks, r) = part.as_chunks::<8>();
|
||||
for c in chunks {
|
||||
mix(u64::from_le_bytes(*c));
|
||||
}
|
||||
let mut last = [0u8; 8];
|
||||
let r = chunks.remainder();
|
||||
last[..r.len()].copy_from_slice(r);
|
||||
mix(u64::from_le_bytes(last) ^ (r.len() as u64) << 56);
|
||||
mix(part.len() as u64);
|
||||
|
||||
@@ -206,6 +206,7 @@ fn detect(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
*lock(&res) = Some(r);
|
||||
prog.finished.store(true, Ordering::Relaxed);
|
||||
};
|
||||
let run = crate::export_tools::guard_job(job.progress.clone(), job.result.clone(), run);
|
||||
s.jobs.push(job);
|
||||
s.scene_jobs.push(PendingScene { job: id, seq: seq_id, apply, results: results.clone() });
|
||||
let wait = bool_p(p, "wait").unwrap_or(false);
|
||||
|
||||
@@ -381,7 +381,10 @@ fn make_subsequence(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let offset = range.map(|r| r.start).unwrap_or_else(|| items.iter().map(|i| i.2.start).min().unwrap_or_default());
|
||||
let name = match str_p(p, "name") {
|
||||
Some(n) => n.to_string(),
|
||||
None => (1..).map(|k| format!("{seq_name}_Sub_{k:02}")).find(|n| !s.project.items.values().any(|i| &i.name == n)).expect("free name"),
|
||||
None => (1..)
|
||||
.map(|k| format!("{seq_name}_Sub_{k:02}"))
|
||||
.find(|n| !s.project.items.values().any(|i| &i.name == n))
|
||||
.unwrap_or_else(|| format!("{seq_name}_Sub")),
|
||||
};
|
||||
let id = s.edit("Make Subsequence", |pr, st| {
|
||||
let nid = pr.new_sequence(&name, q.settings.clone(), q.video_tracks.len(), q.audio_tracks.len(), None);
|
||||
|
||||
@@ -441,7 +441,9 @@ fn bindable(c: &CommandSpec) -> bool {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
fields.last_mut().expect("field").push(ch);
|
||||
if let Some(f) = fields.last_mut() {
|
||||
f.push(ch);
|
||||
}
|
||||
}
|
||||
fields.iter().all(|f| f.contains('?') || f.contains('='))
|
||||
}
|
||||
@@ -505,7 +507,7 @@ impl Shortcuts {
|
||||
dir: None,
|
||||
session_start: None,
|
||||
};
|
||||
s.bindings = s.builtin(DEFAULT_PRESET).expect("default preset");
|
||||
s.bindings = s.builtin(DEFAULT_PRESET).unwrap_or_default();
|
||||
s
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,18 @@ fn trim_linked_and_ripple_delete() {
|
||||
assert_eq!(q.video_tracks[0].items[0].start, s.sequence_rate().tick_of(12), "the trim gap remains");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slide_moves_linked_audio_with_the_video() {
|
||||
let mut s = demo();
|
||||
let v = s.active_sequence().unwrap().video_tracks[0].items[1].clone();
|
||||
s.execute("timeline.slide", json!({"clip": v.id.0, "deltaFrames": 5})).unwrap();
|
||||
let q = s.active_sequence().unwrap();
|
||||
let (_, nv) = q.find_item(v.id).unwrap();
|
||||
let a = q.audio_tracks[0].items.iter().find(|i| i.link == v.link).unwrap();
|
||||
assert_eq!(nv.start, v.start + s.sequence_rate().tick_of(5));
|
||||
assert_eq!((a.start, a.source_in), (nv.start, nv.source_in), "linked audio slides too and stays in sync");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_fps_is_refused() {
|
||||
let mut s = demo();
|
||||
|
||||
@@ -433,7 +433,7 @@ pub fn shuttle(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
tick(s, clock)?;
|
||||
}
|
||||
let rate = s.sequence_rate();
|
||||
let d = s.trim_play.dynamic.as_mut().expect("dynamic trim");
|
||||
let d = s.trim_play.dynamic.as_mut().ok_or_else(|| EngineError::Other("dynamic trimming is not active".into()))?;
|
||||
let speed = if slow {
|
||||
0.25 * dir
|
||||
} else if d.speed * dir > 0.0 && !d.at_limit {
|
||||
|
||||
@@ -568,7 +568,7 @@ impl VideoEncoder for MjpegEncoder {
|
||||
self.rate.num as u32
|
||||
}
|
||||
fn encode(&mut self, f: &EncoderFrame) -> Result<Vec<EncodedPacket>> {
|
||||
let rgb: Vec<u8> = f.rgba.chunks_exact(4).flat_map(|p| [p[0], p[1], p[2]]).collect();
|
||||
let rgb: Vec<u8> = f.rgba.as_chunks::<4>().0.iter().flat_map(|p| [p[0], p[1], p[2]]).collect();
|
||||
let mut out = Vec::new();
|
||||
let mut enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, self.quality);
|
||||
enc.encode(&rgb, f.width, f.height, image::ExtendedColorType::Rgb8).map_err(|e| ExportError::Encode(e.to_string()))?;
|
||||
|
||||
@@ -225,7 +225,7 @@ fn ffmpeg_oracle_op1a_and_op_atom() {
|
||||
let at = mx.track_of_kind(filmcraft_mxf::TrackKind::Sound).unwrap();
|
||||
let ours = mx.read_pcm(&bytes, at, 0, 48_000).unwrap();
|
||||
assert_eq!(pcm.len(), 48_000 * 2 * 4, "{codec:?}");
|
||||
for (i, c) in pcm.chunks_exact(4).enumerate() {
|
||||
for (i, c) in pcm.as_chunks::<4>().0.iter().enumerate() {
|
||||
let s = i32::from_le_bytes([c[0], c[1], c[2], c[3]]) >> 8;
|
||||
assert_eq!(s as f32 / 8_388_608.0, ours[i % 2][i / 2], "{codec:?}: sample {i}");
|
||||
}
|
||||
@@ -255,7 +255,7 @@ fn ffmpeg_oracle_op1a_and_op_atom() {
|
||||
assert_eq!((s["codec_name"].as_str(), s["channels"].as_u64(), s["sample_rate"].as_str()), (Some("pcm_s24le"), Some(1), Some("48000")), "{a}");
|
||||
let mono = ffmpeg_raw(&ffmpeg, a, "0:a:0", &["-f", "s32le", "-acodec", "pcm_s32le"]);
|
||||
assert_eq!(mono.len(), 48_000 * 4, "{a}");
|
||||
let want: Vec<u8> = stereo.chunks_exact(8).flat_map(|f| f[k * 4..k * 4 + 4].to_vec()).collect();
|
||||
let want: Vec<u8> = stereo.as_chunks::<8>().0.iter().flat_map(|f| f[k * 4..k * 4 + 4].to_vec()).collect();
|
||||
assert!(mono == want, "{a}: channel {k} differs from the WAV export");
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ impl Pipeline {
|
||||
if let Some(tf) = self.out_tf.as_ref().filter(|_| self.hdr_out) {
|
||||
let mut out = vec![0f32; img.w * img.h * 3];
|
||||
out.par_chunks_mut(img.w * 3).zip(img.px.par_chunks(img.w * 4)).for_each(|(o, s)| {
|
||||
for (o, p) in o.chunks_exact_mut(3).zip(s.chunks_exact(4)) {
|
||||
for (o, p) in o.as_chunks_mut::<3>().0.iter_mut().zip(s.as_chunks::<4>().0) {
|
||||
o.copy_from_slice(&tf.encode([p[0], p[1], p[2]]));
|
||||
}
|
||||
});
|
||||
@@ -239,7 +239,7 @@ fn load_overlay(path: &str, width: f32) -> Result<Image> {
|
||||
let rgba = image::load_from_memory(&bytes).map_err(|e| ExportError::Unsupported(format!("image overlay {path}: {e}")))?.to_rgba8();
|
||||
let (iw, ih) = (rgba.width() as usize, rgba.height() as usize);
|
||||
let mut img = Image::new(iw, ih);
|
||||
for (o, p) in img.px.chunks_exact_mut(4).zip(rgba.as_raw().chunks_exact(4)) {
|
||||
for (o, p) in img.px.as_chunks_mut::<4>().0.iter_mut().zip(rgba.as_raw().as_chunks::<4>().0) {
|
||||
let a = p[3] as f32 / 255.0;
|
||||
for c in 0..3 {
|
||||
o[c] = filmcraft_color::srgb_to_linear(p[c] as f32 / 255.0) * a;
|
||||
@@ -310,7 +310,7 @@ fn resample_axis(src: &Image, n: usize, horizontal: bool) -> Image {
|
||||
pub fn limit_rgba8(rgba: &mut [u8], min_percent: f32, max_percent: f32) {
|
||||
let (lo, hi) = (min_percent.clamp(0.0, 100.0) / 100.0, max_percent.clamp(0.0, 100.0) / 100.0);
|
||||
rgba.par_chunks_mut(4 * 1024).for_each(|chunk| {
|
||||
for p in chunk.chunks_exact_mut(4) {
|
||||
for p in chunk.as_chunks_mut::<4>().0 {
|
||||
let mut c = [p[0] as f32 / 255.0, p[1] as f32 / 255.0, p[2] as f32 / 255.0];
|
||||
limit_px(&mut c, lo, hi);
|
||||
for i in 0..3 {
|
||||
@@ -323,7 +323,7 @@ pub fn limit_rgba8(rgba: &mut [u8], min_percent: f32, max_percent: f32) {
|
||||
fn limit_f32(rgb: &mut [f32], min_percent: f32, max_percent: f32) {
|
||||
let (lo, hi) = (min_percent.clamp(0.0, 100.0) / 100.0, max_percent.clamp(0.0, 100.0) / 100.0);
|
||||
rgb.par_chunks_mut(3 * 1024).for_each(|chunk| {
|
||||
for p in chunk.chunks_exact_mut(3) {
|
||||
for p in chunk.as_chunks_mut::<3>().0 {
|
||||
let mut c = [p[0], p[1], p[2]];
|
||||
limit_px(&mut c, lo, hi);
|
||||
p.copy_from_slice(&c);
|
||||
|
||||
@@ -204,7 +204,7 @@ fn read_wav16(path: &str) -> Vec<Vec<f32>> {
|
||||
let ch = u16::from_le_bytes([b[22], b[23]]) as usize;
|
||||
let data = &b[44..];
|
||||
let mut out = vec![Vec::new(); ch];
|
||||
for (i, s) in data.chunks_exact(2).enumerate() {
|
||||
for (i, s) in data.as_chunks::<2>().0.iter().enumerate() {
|
||||
out[i % ch].push(i16::from_le_bytes([s[0], s[1]]) as f32 / 32768.0);
|
||||
}
|
||||
out
|
||||
|
||||
@@ -110,7 +110,7 @@ fn decode(path: &str) -> Option<Vec<Vec<f64>>> {
|
||||
let out =
|
||||
std::process::Command::new(ffmpeg).args(["-v", "error", "-i", path, "-map", "0:a:0", "-f", "s32le", "-acodec", "pcm_s32le", "-"]).output().unwrap();
|
||||
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
||||
let ints: Vec<i32> = out.stdout.chunks_exact(4).map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
let ints: Vec<i32> = out.stdout.as_chunks::<4>().0.iter().map(|b| i32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
let n = ints.len() / 6;
|
||||
Some((0..6).map(|c| (0..n).map(|i| ints[i * 6 + c] as f64).collect()).collect())
|
||||
}
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
//! per-character text styles (`TrackItem::graphic`, `EffectInstance::layer`) and source graphics
|
||||
//! (`Project::source_graphics`). No-op step; older builds would drop these fields when saving.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
pub mod atomic;
|
||||
pub mod autosave;
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
//! compositor. Pixel data is `Arc`-shared so caches, monitors and export share frames for free.
|
||||
//! - [`AudioBuffer`]: planar f32 samples.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use filmcraft_color::{ColorInfo, DecodeTable, Matrix, Range, linear_to_srgb_u8, normalize_c, normalize_y, srgb_u8_to_linear_table, to_linear, ycbcr_to_rgb};
|
||||
@@ -383,14 +385,14 @@ impl VideoFrame {
|
||||
pub fn luma8(&self) -> Vec<u8> {
|
||||
match &self.data {
|
||||
PixelData::Yuv8 { planes, .. } => planes[0].as_ref().clone(),
|
||||
_ => self.to_rgba8().chunks_exact(4).map(|p| (0.2126 * p[0] as f32 + 0.7152 * p[1] as f32 + 0.0722 * p[2] as f32) as u8).collect(),
|
||||
_ => self.to_rgba8().as_chunks::<4>().0.iter().map(|p| (0.2126 * p[0] as f32 + 0.7152 * p[1] as f32 + 0.0722 * p[2] as f32) as u8).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a row of premultiplied linear f32 RGBA into straight sRGB RGBA8.
|
||||
pub fn linear_premul_to_srgb8(src: &[f32], dst: &mut [u8]) {
|
||||
for (s, o) in src.chunks_exact(4).zip(dst.chunks_exact_mut(4)) {
|
||||
for (s, o) in src.as_chunks::<4>().0.iter().zip(dst.as_chunks_mut::<4>().0) {
|
||||
let a = s[3].clamp(0.0, 1.0);
|
||||
if a <= 0.0 {
|
||||
o.copy_from_slice(&[0, 0, 0, 0]);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! 2D geometry for FilmCraft: vectors, rectangles and affine transforms (Motion effect math).
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::{Add, Mul, Sub};
|
||||
|
||||
|
||||
@@ -185,7 +185,9 @@ impl MediaSource for Encoded {
|
||||
let m = filmcraft_color::spaces::to_f32(&filmcraft_color::spaces::gamut_matrix(Gamut::Bt709, self.target.gamut()));
|
||||
let curve = self.target.curve();
|
||||
let out: Vec<u8> = rgba
|
||||
.chunks_exact(4)
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.flat_map(|p| {
|
||||
let lin = [0, 1, 2].map(|k| filmcraft_color::srgb_to_linear(p[k] as f32 / 255.0) * self.gain);
|
||||
let c = filmcraft_color::spaces::apply3(&m, lin);
|
||||
@@ -723,7 +725,7 @@ fn lumetri_presets_grid() {
|
||||
fn scenes_are_distinct() {
|
||||
let imgs: Vec<(&str, Rgba8)> = scenes().into_iter().map(|(n, _, f)| (n, render_cpu(&f()))).collect();
|
||||
for (n, img) in &imgs {
|
||||
let lit = img.px.chunks_exact(4).filter(|p| p[0] as u32 + p[1] as u32 + p[2] as u32 > 30).count();
|
||||
let lit = img.px.as_chunks::<4>().0.iter().filter(|p| p[0] as u32 + p[1] as u32 + p[2] as u32 > 30).count();
|
||||
assert!(lit > img.px.len() / 4 / 4, "{n}: mostly black");
|
||||
}
|
||||
for i in 0..imgs.len() {
|
||||
@@ -758,7 +760,7 @@ fn gpu_matches_cpu_on_golden_scenes() {
|
||||
let kind = if matches!(plan, filmcraft_render::plan::FramePlan::Layers { .. }) { "layers" } else { "cpu image" };
|
||||
c.composite(&plan);
|
||||
let (gw, gh, mut px) = c.read_output().expect("GPU readback");
|
||||
px.chunks_exact_mut(4).for_each(|p| p[3] = 255);
|
||||
px.as_chunks_mut::<4>().0.iter_mut().for_each(|p| p[3] = 255);
|
||||
let gpu = Rgba8::new(gw, gh, px);
|
||||
let d = diff(&cpu, &gpu).unwrap();
|
||||
// mean over RGB only (alpha is equal by construction)
|
||||
|
||||
+17
-16
@@ -13,6 +13,8 @@
|
||||
//!
|
||||
//! The CPU plan executor (`filmcraft_render::plan::execute_cpu`) is the oracle; tests compare.
|
||||
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -135,7 +137,7 @@ const CHUNK: usize = 1 << 15;
|
||||
fn f32_to_f16_bytes(v: &[f32]) -> Vec<u8> {
|
||||
let mut out = vec![0u8; v.len() * 2];
|
||||
out.par_chunks_mut(CHUNK * 2).zip(v.par_chunks(CHUNK)).for_each(|(o, s)| {
|
||||
for (o, x) in o.chunks_exact_mut(2).zip(s) {
|
||||
for (o, x) in o.as_chunks_mut::<2>().0.iter_mut().zip(s) {
|
||||
o.copy_from_slice(&f32_to_f16(*x).to_le_bytes());
|
||||
}
|
||||
});
|
||||
@@ -157,7 +159,7 @@ fn codes_to_f16_bytes(v: &[u16], bits: u32) -> Vec<u8> {
|
||||
let table = code_table(bits);
|
||||
let mut out = vec![0u8; v.len() * 2];
|
||||
out.par_chunks_mut(CHUNK * 2).zip(v.par_chunks(CHUNK)).for_each(|(o, s)| {
|
||||
for (o, c) in o.chunks_exact_mut(2).zip(s) {
|
||||
for (o, c) in o.as_chunks_mut::<2>().0.iter_mut().zip(s) {
|
||||
o.copy_from_slice(&table[*c as usize]);
|
||||
}
|
||||
});
|
||||
@@ -279,9 +281,9 @@ impl GpuCompositor {
|
||||
h: u32,
|
||||
format: wgpu::TextureFormat,
|
||||
extra: wgpu::TextureUsages,
|
||||
) {
|
||||
if slot.as_ref().is_some_and(|s| s.2 == (w, h)) {
|
||||
return;
|
||||
) -> wgpu::TextureView {
|
||||
if let Some(s) = slot.as_ref().filter(|s| s.2 == (w, h)) {
|
||||
return s.1.clone();
|
||||
}
|
||||
let t = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("filmcraft-target"),
|
||||
@@ -294,7 +296,8 @@ impl GpuCompositor {
|
||||
view_formats: &[],
|
||||
});
|
||||
let v = t.create_view(&Default::default());
|
||||
*slot = Some((t, v, (w, h)));
|
||||
*slot = Some((t, v.clone(), (w, h)));
|
||||
v
|
||||
}
|
||||
|
||||
fn plane_texture(&mut self, w: u32, h: u32, format: wgpu::TextureFormat, bytes: &[u8], bpp: u32) -> wgpu::TextureView {
|
||||
@@ -443,13 +446,13 @@ impl GpuCompositor {
|
||||
}
|
||||
|
||||
/// Composite a plan; returns the output view (sRGB, over black) and its size.
|
||||
pub fn composite(&mut self, plan: &FramePlan) -> (&wgpu::TextureView, (u32, u32)) {
|
||||
pub fn composite(&mut self, plan: &FramePlan) -> (wgpu::TextureView, (u32, u32)) {
|
||||
self.composite_prepared(plan, None)
|
||||
}
|
||||
|
||||
/// [`composite`](Self::composite) with texel conversions already done by [`prepare`] (the
|
||||
/// result is identical; only the upload work on this thread differs).
|
||||
pub fn composite_prepared(&mut self, plan: &FramePlan, prep: Option<&PreparedPlan>) -> (&wgpu::TextureView, (u32, u32)) {
|
||||
pub fn composite_prepared(&mut self, plan: &FramePlan, prep: Option<&PreparedPlan>) -> (wgpu::TextureView, (u32, u32)) {
|
||||
let owned;
|
||||
let (w, h, layers): (u32, u32, &[PlanLayer]) = match plan {
|
||||
FramePlan::Layers { width, height, layers } => (*width as u32, *height as u32, layers.as_slice()),
|
||||
@@ -469,8 +472,8 @@ impl GpuCompositor {
|
||||
}
|
||||
};
|
||||
let (w, h) = (w.max(1), h.max(1));
|
||||
Self::target(&self.device, &mut self.accum, w, h, ACCUM_FORMAT, wgpu::TextureUsages::empty());
|
||||
Self::target(&self.device, &mut self.output, w, h, OUTPUT_FORMAT, wgpu::TextureUsages::COPY_SRC);
|
||||
let accum_view = Self::target(&self.device, &mut self.accum, w, h, ACCUM_FORMAT, wgpu::TextureUsages::empty());
|
||||
let out_view = Self::target(&self.device, &mut self.output, w, h, OUTPUT_FORMAT, wgpu::TextureUsages::COPY_SRC);
|
||||
let keys: Vec<(usize, u32, u32)> = layers
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -503,19 +506,17 @@ impl GpuCompositor {
|
||||
});
|
||||
bind_groups.push(bg);
|
||||
}
|
||||
let accum_view = &self.accum.as_ref().expect("accum").1;
|
||||
let out_view = &self.output.as_ref().expect("output").1;
|
||||
let final_bg = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("final"),
|
||||
layout: &self.final_bgl,
|
||||
entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(accum_view) }],
|
||||
entries: &[wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView(&accum_view) }],
|
||||
});
|
||||
let mut enc = self.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("filmcraft-composite") });
|
||||
{
|
||||
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("layers"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: accum_view,
|
||||
view: &accum_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), store: wgpu::StoreOp::Store },
|
||||
@@ -535,7 +536,7 @@ impl GpuCompositor {
|
||||
let mut pass = enc.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("resolve"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: out_view,
|
||||
view: &out_view,
|
||||
depth_slice: None,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store },
|
||||
@@ -550,7 +551,7 @@ impl GpuCompositor {
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
self.queue.submit([enc.finish()]);
|
||||
(&self.output.as_ref().expect("output").1, (w, h))
|
||||
(out_view, (w, h))
|
||||
}
|
||||
|
||||
/// Read the output back as RGBA8 (tests / screenshots / thumbnails).
|
||||
|
||||
@@ -112,7 +112,7 @@ impl GpuLut {
|
||||
let _ = d.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().ok()?.ok()?;
|
||||
let data = slice.get_mapped_range().ok()?;
|
||||
let out = data.chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
let out = data.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
drop(data);
|
||||
rb.unmap();
|
||||
Some(out)
|
||||
|
||||
@@ -162,7 +162,7 @@ impl GpuMask {
|
||||
let _ = d.poll(wgpu::PollType::wait_indefinitely());
|
||||
rx.recv().ok()?.ok()?;
|
||||
let data = slice.get_mapped_range().ok()?;
|
||||
let v = data.chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
let v = data.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect();
|
||||
drop(data);
|
||||
b.unmap();
|
||||
Some(v)
|
||||
|
||||
@@ -172,7 +172,7 @@ fn gpu_lut_matches_cpu_tetrahedral() {
|
||||
}
|
||||
let gpu = g.apply(&lut, &px).expect("gpu lut");
|
||||
let mut worst = 0f32;
|
||||
for (i, p) in px.chunks_exact(4).enumerate() {
|
||||
for (i, p) in px.as_chunks::<4>().0.iter().enumerate() {
|
||||
let c = lut.apply([p[0], p[1], p[2]]);
|
||||
for k in 0..3 {
|
||||
worst = worst.max((c[k] - gpu[i * 4 + k]).abs());
|
||||
|
||||
@@ -527,7 +527,9 @@ impl SliceDecoder<'_> {
|
||||
let pos = c.bit_pos().div_ceil(8);
|
||||
let data = c.data();
|
||||
ensure!(data.len() >= pos + 384, "truncated I_PCM macroblock");
|
||||
let samples: [u8; 384] = data[pos..pos + 384].try_into().unwrap();
|
||||
let Some(&samples) = data.get(pos..).and_then(|d| d.first_chunk::<384>()) else {
|
||||
return invalid("truncated I_PCM macroblock");
|
||||
};
|
||||
c.set_bit_pos((pos + 384) * 8);
|
||||
c.init_engine()?;
|
||||
self.finish_pcm();
|
||||
|
||||
@@ -32,7 +32,7 @@ impl VlcTable {
|
||||
}
|
||||
let suffix = &code[lz + 1..];
|
||||
assert!(suffix.len() <= 5, "suffix too long in {code}");
|
||||
let sbits = u32::from_str_radix(if suffix.is_empty() { "0" } else { suffix }, 2).unwrap();
|
||||
let sbits = suffix.bytes().fold(0u32, |a, b| (a << 1) | u32::from(b == b'1'));
|
||||
let free = 5 - suffix.len();
|
||||
for ext in 0..(1u32 << free) {
|
||||
let s = (sbits << free) | ext;
|
||||
|
||||
@@ -232,7 +232,10 @@ fn filter_vertical<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y: u
|
||||
let mut w = [0u64; N];
|
||||
for (i, w) in w.iter_mut().enumerate() {
|
||||
let o = (y + i) * stride + x - 4;
|
||||
*w = u64::from_le_bytes(pix[o..o + 8].try_into().unwrap());
|
||||
let Some(&b) = pix.get(o..).and_then(|p| p.first_chunk::<8>()) else {
|
||||
return;
|
||||
};
|
||||
*w = u64::from_le_bytes(b);
|
||||
}
|
||||
let mut s = [[0i16; N]; 8];
|
||||
for (k, row) in s.iter_mut().enumerate() {
|
||||
@@ -256,7 +259,9 @@ fn filter_horizontal<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y:
|
||||
let base = (y - 4) * stride + x;
|
||||
let mut s = [[0i16; N]; 8];
|
||||
for (k, row) in s.iter_mut().enumerate() {
|
||||
let src: &[u8; N] = pix[base + k * stride..base + k * stride + N].try_into().unwrap();
|
||||
let Some(src) = pix.get(base + k * stride..).and_then(|p| p.first_chunk::<N>()) else {
|
||||
return;
|
||||
};
|
||||
for i in 0..N {
|
||||
row[i] = src[i] as i16;
|
||||
}
|
||||
@@ -264,7 +269,9 @@ fn filter_horizontal<const N: usize>(pix: &mut [u8], stride: usize, x: usize, y:
|
||||
filter_lanes(&mut s, ep, chroma);
|
||||
let (k0, k1) = if chroma { (3, 5) } else { (1, 7) };
|
||||
for (k, row) in s.iter().enumerate().take(k1).skip(k0) {
|
||||
let dst: &mut [u8; N] = (&mut pix[base + k * stride..base + k * stride + N]).try_into().unwrap();
|
||||
let Some(dst) = pix.get_mut(base + k * stride..).and_then(|p| p.first_chunk_mut::<N>()) else {
|
||||
return;
|
||||
};
|
||||
for i in 0..N {
|
||||
dst[i] = row[i] as u8;
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ impl Decoder {
|
||||
if !o.frame.is_published(o.frame.mb_h() - 1) && self.out_queue.len() <= max_queue {
|
||||
break;
|
||||
}
|
||||
let o = self.out_queue.pop_front().expect("front exists");
|
||||
let Some(o) = self.out_queue.pop_front() else { break };
|
||||
pics.push(make_picture(&o));
|
||||
}
|
||||
Ok(pics)
|
||||
@@ -353,7 +353,9 @@ impl Decoder {
|
||||
found = Some((pps.clone(), sps.clone()));
|
||||
Ok((&**pps, &**sps))
|
||||
})?;
|
||||
let (pps, sps) = found.expect("lookup succeeded");
|
||||
let Some((pps, sps)) = found else {
|
||||
return Err(Error::MissingParameterSet("PPS".into()));
|
||||
};
|
||||
sps.check_supported()?;
|
||||
if pps.num_slice_groups > 1 {
|
||||
return unsupported("slice groups (FMO)");
|
||||
@@ -379,7 +381,9 @@ impl Decoder {
|
||||
self.start_picture(&sh, &sps, pts)?;
|
||||
}
|
||||
let ls = self.level_scale(&pps);
|
||||
let pending = self.pending.as_mut().expect("picture started");
|
||||
let Some(pending) = self.pending.as_mut() else {
|
||||
return invalid("slice without a started picture");
|
||||
};
|
||||
let refs = self.dpb.build_ref_lists(&sh, pending.poc.frame(), sps.max_frame_num())?;
|
||||
if !sh.slice_type.is_intra() {
|
||||
ensure!(!refs[0].is_empty(), "no reference pictures available for inter slice");
|
||||
|
||||
@@ -54,9 +54,9 @@ fn clip1(v: i32) -> u8 {
|
||||
#[inline(always)]
|
||||
fn copy_n(dst: &mut [u8], src: &[u8], n: usize) {
|
||||
fn fixed<const N: usize>(dst: &mut [u8], src: &[u8]) {
|
||||
let s: &[u8; N] = src[..N].try_into().unwrap();
|
||||
let d: &mut [u8; N] = (&mut dst[..N]).try_into().unwrap();
|
||||
*d = *s;
|
||||
if let (Some(d), Some(s)) = (dst.first_chunk_mut::<N>(), src.first_chunk::<N>()) {
|
||||
*d = *s;
|
||||
}
|
||||
}
|
||||
match n {
|
||||
16 => fixed::<16>(dst, src),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
// Index loops over fixed-size blocks read more clearly than iterator chains in codec code.
|
||||
#![allow(clippy::needless_range_loop, clippy::too_many_arguments)]
|
||||
#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable))]
|
||||
|
||||
mod cabac;
|
||||
mod cabac_mb;
|
||||
|
||||
@@ -277,9 +277,11 @@ impl Frame {
|
||||
/// bytes is copied instead (cheaper than a variable-length copy; `out` must then have room for `N`).
|
||||
#[inline(always)]
|
||||
fn copy_line<const N: usize>(line: &[u8], x0: i32, out: &mut [u8], w: usize) {
|
||||
if w <= N && x0 >= 0 && x0 as usize + N <= line.len() && out.len() >= N {
|
||||
let src: &[u8; N] = line[x0 as usize..x0 as usize + N].try_into().unwrap();
|
||||
let dst: &mut [u8; N] = (&mut out[..N]).try_into().unwrap();
|
||||
if w <= N
|
||||
&& x0 >= 0
|
||||
&& let Some(src) = line.get(x0 as usize..).and_then(|l| l.first_chunk::<N>())
|
||||
&& let Some(dst) = out.first_chunk_mut::<N>()
|
||||
{
|
||||
*dst = *src;
|
||||
} else {
|
||||
copy_clamped(line, x0, &mut out[..w]);
|
||||
|
||||
@@ -888,7 +888,9 @@ impl<'a> SliceDecoder<'a> {
|
||||
let stride = self.pic.planes.width;
|
||||
let px = self.mb_x * 16 + (raster & 3) * 4;
|
||||
let py = self.mb_y * 16 + (raster >> 2) * 4;
|
||||
let blk: &mut [i32; 16] = (&mut self.s.coef[raster * 16..raster * 16 + 16]).try_into().unwrap();
|
||||
let Some(blk) = self.s.coef.get_mut(raster * 16..).and_then(|c| c.first_chunk_mut::<16>()) else {
|
||||
return;
|
||||
};
|
||||
transform::idct4_add(blk, &mut self.pic.planes.y[py * stride + px..], stride);
|
||||
blk.fill(0);
|
||||
}
|
||||
@@ -900,7 +902,9 @@ impl<'a> SliceDecoder<'a> {
|
||||
let stride = self.pic.planes.width;
|
||||
let px = self.mb_x * 16 + (b8 & 1) * 8;
|
||||
let py = self.mb_y * 16 + (b8 >> 1) * 8;
|
||||
let blk: &mut [i32; 64] = (&mut self.s.coef[b8 * 64..b8 * 64 + 64]).try_into().unwrap();
|
||||
let Some(blk) = self.s.coef.get_mut(b8 * 64..).and_then(|c| c.first_chunk_mut::<64>()) else {
|
||||
return;
|
||||
};
|
||||
transform::idct8_add(blk, &mut self.pic.planes.y[py * stride + px..], stride);
|
||||
blk.fill(0);
|
||||
}
|
||||
@@ -917,7 +921,9 @@ impl<'a> SliceDecoder<'a> {
|
||||
}
|
||||
let px = self.mb_x * 8 + (b & 1) * 4;
|
||||
let py = self.mb_y * 8 + (b >> 1) * 4;
|
||||
let blk: &mut [i32; 16] = (&mut self.s.coef_c[c][b * 16..b * 16 + 16]).try_into().unwrap();
|
||||
let Some(blk) = self.s.coef_c[c].get_mut(b * 16..).and_then(|c| c.first_chunk_mut::<16>()) else {
|
||||
continue;
|
||||
};
|
||||
let plane = if c == 0 { &mut self.pic.planes.cb } else { &mut self.pic.planes.cr };
|
||||
transform::idct4_add(blk, &mut plane[py * stride + px..], stride);
|
||||
blk.fill(0);
|
||||
@@ -1216,7 +1222,9 @@ impl<'a> SliceDecoder<'a> {
|
||||
let pos = r.byte_pos();
|
||||
let data = r.data();
|
||||
ensure!(data.len() >= pos + 384, "truncated I_PCM macroblock");
|
||||
let samples: [u8; 384] = data[pos..pos + 384].try_into().unwrap();
|
||||
let Some(&samples) = data.get(pos..).and_then(|d| d.first_chunk::<384>()) else {
|
||||
return invalid("truncated I_PCM macroblock");
|
||||
};
|
||||
r.skip(384 * 8)?;
|
||||
self.finish_pcm();
|
||||
self.write_pcm(&samples);
|
||||
|
||||
@@ -32,13 +32,13 @@ impl LevelScale {
|
||||
/// Scale one 4x4 AC coefficient (8.5.12.1, not the DC of Intra16x16 / chroma blocks).
|
||||
#[inline(always)]
|
||||
pub fn scale4(c: i32, ls: i32, qp: i32) -> i32 {
|
||||
if qp >= 24 { (c * ls) << (qp / 6 - 4) } else { (c * ls + (1 << (3 - qp / 6))) >> (4 - qp / 6) }
|
||||
if qp >= 24 { c.wrapping_mul(ls) << (qp / 6 - 4) } else { c.wrapping_mul(ls).wrapping_add(1 << (3 - qp / 6)) >> (4 - qp / 6) }
|
||||
}
|
||||
|
||||
/// Scale one 8x8 coefficient (8.5.13.1).
|
||||
#[inline(always)]
|
||||
pub fn scale8(c: i32, ls: i32, qp: i32) -> i32 {
|
||||
if qp >= 36 { (c * ls) << (qp / 6 - 6) } else { (c * ls + (1 << (5 - qp / 6))) >> (6 - qp / 6) }
|
||||
if qp >= 36 { c.wrapping_mul(ls) << (qp / 6 - 6) } else { c.wrapping_mul(ls).wrapping_add(1 << (5 - qp / 6)) >> (6 - qp / 6) }
|
||||
}
|
||||
|
||||
/// Intra16x16 luma DC: inverse Hadamard + scaling (8.5.10). `c` is raster order (row*4+col);
|
||||
@@ -65,20 +65,21 @@ pub fn luma_dc_dequant(c: &mut [i32; 16], qp: i32, ls00: i32) {
|
||||
let d23 = t[8 + j] - t[12 + j];
|
||||
let f = [s01 + s23, s01 - s23, d01 - d23, d01 + d23];
|
||||
for i in 0..4 {
|
||||
c[i * 4 + j] = if qp >= 36 { (f[i] * ls00) << (qp / 6 - 6) } else { (f[i] * ls00 + (1 << (5 - qp / 6))) >> (6 - qp / 6) };
|
||||
c[i * 4 + j] =
|
||||
if qp >= 36 { f[i].wrapping_mul(ls00) << (qp / 6 - 6) } else { f[i].wrapping_mul(ls00).wrapping_add(1 << (5 - qp / 6)) >> (6 - qp / 6) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 4:2:0 chroma DC: 2x2 transform + scaling (8.5.11). `c` = [c00, c01, c10, c11].
|
||||
pub fn chroma_dc_dequant_420(c: &mut [i32; 4], qp: i32, ls00: i32) {
|
||||
let s0 = c[0] + c[1];
|
||||
let d0 = c[0] - c[1];
|
||||
let s1 = c[2] + c[3];
|
||||
let d1 = c[2] - c[3];
|
||||
let f = [s0 + s1, d0 + d1, s0 - s1, d0 - d1];
|
||||
let s0 = c[0].wrapping_add(c[1]);
|
||||
let d0 = c[0].wrapping_sub(c[1]);
|
||||
let s1 = c[2].wrapping_add(c[3]);
|
||||
let d1 = c[2].wrapping_sub(c[3]);
|
||||
let f = [s0.wrapping_add(s1), d0.wrapping_add(d1), s0.wrapping_sub(s1), d0.wrapping_sub(d1)];
|
||||
for k in 0..4 {
|
||||
c[k] = ((f[k] * ls00) << (qp / 6)) >> 5;
|
||||
c[k] = (f[k].wrapping_mul(ls00) << (qp / 6)) >> 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +114,9 @@ pub fn idct4_add(d: &[i32; 16], dst: &mut [u8], stride: usize) {
|
||||
o[3][j] = e - h;
|
||||
}
|
||||
for (i, row) in o.iter().enumerate() {
|
||||
let line: &mut [u8; 4] = (&mut dst[i * stride..i * stride + 4]).try_into().unwrap();
|
||||
let Some(line) = dst.get_mut(i * stride..).and_then(|d| d.first_chunk_mut::<4>()) else {
|
||||
return;
|
||||
};
|
||||
for (p, &v) in line.iter_mut().zip(row) {
|
||||
*p = clip_u8(*p as i32 + ((v + 32) >> 6));
|
||||
}
|
||||
@@ -156,8 +159,8 @@ fn idct8_1d(d: [i32; 8]) -> [i32; 8] {
|
||||
/// the column pass and reconstruction across all columns at once (they vectorise).
|
||||
pub fn idct8_add(d: &[i32; 64], dst: &mut [u8], stride: usize) {
|
||||
let mut t = [[0i32; 8]; 8];
|
||||
for (i, row) in t.iter_mut().enumerate() {
|
||||
*row = idct8_1d(d[i * 8..i * 8 + 8].try_into().unwrap());
|
||||
for (row, src) in t.iter_mut().zip(d.as_chunks::<8>().0) {
|
||||
*row = idct8_1d(*src);
|
||||
}
|
||||
let mut o = [[0i32; 8]; 8];
|
||||
for j in 0..8 {
|
||||
@@ -167,7 +170,9 @@ pub fn idct8_add(d: &[i32; 64], dst: &mut [u8], stride: usize) {
|
||||
}
|
||||
}
|
||||
for (i, row) in o.iter().enumerate() {
|
||||
let line: &mut [u8; 8] = (&mut dst[i * stride..i * stride + 8]).try_into().unwrap();
|
||||
let Some(line) = dst.get_mut(i * stride..).and_then(|d| d.first_chunk_mut::<8>()) else {
|
||||
return;
|
||||
};
|
||||
for (p, &v) in line.iter_mut().zip(row) {
|
||||
*p = clip_u8(*p as i32 + ((v + 32) >> 6));
|
||||
}
|
||||
@@ -283,4 +288,17 @@ mod tests {
|
||||
assert_eq!(scale8(1, 320, 36), 320);
|
||||
assert_eq!(scale8(1, 320, 30), (320 + 1) >> 1);
|
||||
}
|
||||
|
||||
/// Corrupt streams can carry huge coefficient levels: dequantisation wraps (as release
|
||||
/// builds always did) instead of panicking with "attempt to multiply with overflow" in
|
||||
/// debug builds (found by mutation fuzzing an H.264 transport stream).
|
||||
#[test]
|
||||
fn huge_levels_dequantise_without_overflow_panics() {
|
||||
let mut c = [i32::MAX / 2, i32::MAX / 3, -i32::MAX / 2, 7];
|
||||
chroma_dc_dequant_420(&mut c, 51, 16 * 25);
|
||||
let _ = scale4(i32::MAX, 16 * 25, 51);
|
||||
let _ = scale4(i32::MAX, 16 * 25, 0);
|
||||
let _ = scale8(i32::MIN, 16 * 25, 51);
|
||||
let _ = scale8(i32::MIN, 16 * 25, 0);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ fn main() {
|
||||
enc.set_recon_capture(false);
|
||||
}
|
||||
for (i, f) in frames.iter().enumerate() {
|
||||
pk.extend(enc.encode(&f.frame(), i as i64));
|
||||
pk.extend(enc.encode(&f.frame(), i as i64).unwrap());
|
||||
}
|
||||
pk.extend(enc.flush());
|
||||
let el = t0.elapsed().as_secs_f64();
|
||||
|
||||
@@ -296,7 +296,8 @@ pub fn write_mb<S: BinSink>(s: &mut S, nb: &NbCtx, c: &MbCode) {
|
||||
s.decision(if bins[1] != 1 { 16 } else { 17 }, bins[2]);
|
||||
}
|
||||
MbKind::BDirect | MbKind::BInter => write_b_bins(s, nb, B_BINS[c.b_type_number() as usize]),
|
||||
_ => unreachable!("skip/none macroblocks are not written with write_mb"),
|
||||
// Skip / none macroblocks are not written with write_mb.
|
||||
_ => debug_assert!(false, "write_mb on a skipped macroblock"),
|
||||
}
|
||||
let is_inxn = c.kind.is_inxn();
|
||||
if c.kind == MbKind::PInter && c.part == Part::P8x8 {
|
||||
|
||||
@@ -151,7 +151,8 @@ pub fn write_mb(w: &mut BitWriter, nb: &NbCtx, c: &MbCode) {
|
||||
Part::P8x8 => 3,
|
||||
}),
|
||||
MbKind::BDirect | MbKind::BInter => w.write_ue(c.b_type_number()),
|
||||
_ => unreachable!(),
|
||||
// Skip / none macroblocks are not written with write_mb.
|
||||
_ => debug_assert!(false, "write_mb on a skipped macroblock"),
|
||||
}
|
||||
if c.kind == MbKind::PInter && c.part == Part::P8x8 {
|
||||
for _ in 0..4 {
|
||||
|
||||
@@ -16,8 +16,9 @@ pub fn sad(a: &[u8], sa: usize, b: &[u8], sb: usize, w: usize, h: usize) -> u32
|
||||
pub fn sad16(a: &[u8], sa: usize, b: &[u8], sb: usize, h: usize) -> u32 {
|
||||
let mut s = 0u32;
|
||||
for r in 0..h {
|
||||
let ra: &[u8; 16] = a[r * sa..r * sa + 16].try_into().unwrap();
|
||||
let rb: &[u8; 16] = b[r * sb..r * sb + 16].try_into().unwrap();
|
||||
let (Some(ra), Some(rb)) = (a.get(r * sa..).and_then(|x| x.first_chunk::<16>()), b.get(r * sb..).and_then(|x| x.first_chunk::<16>())) else {
|
||||
break;
|
||||
};
|
||||
let mut acc = 0u16;
|
||||
for i in 0..16 {
|
||||
acc += ra[i].abs_diff(rb[i]) as u16;
|
||||
|
||||
Loaded 100 of 198 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user