effectcraft/crates/export/src/webm.rs

427 lines
17 KiB
Rust

//! WebM output (VP9 video, optional VP9 alpha, Opus audio) and audio-only WAV / AIFF.
//!
//! The WebM container is written from the Matroska / WebM specifications (IETF RFC 8794 EBML,
//! the Matroska element registry and the WebM container guidelines): EBML header, Segment
//! (size patched at the end), Info, Tracks and one Cluster per render batch. Video frames are
//! SimpleBlocks; with alpha (Channels: RGB + Alpha) each frame is a BlockGroup whose
//! BlockAdditional (BlockAddID 1) carries a second VP9 frame coding the alpha channel as luma,
//! with `AlphaMode` 1 on the track, as WebM defines. VP9 comes from `effectcraft-vp9enc` (key
//! frames every two seconds, inter frames between them: SimpleBlocks carry the key flag, inter
//! BlockGroups a ReferenceBlock; the alpha stream's key frames line up with the colour stream's),
//! Opus from `effectcraft-opusenc` (CELT, 48 kHz stereo, 20 ms packets,
//! `CodecDelay` = pre-skip). WAV (RIFF, 16-bit PCM) and AIFF (big-endian 16-bit, 80-bit
//! extended sample rate) follow their published file layouts.
use std::io::{Seek, SeekFrom, Write};
use effectcraft_project::Comp;
use effectcraft_project::render_queue::{AudioFormat, Channels};
use effectcraft_time::{TICKS_PER_SECOND, Tick};
use crate::encode::{mix, pcm_bytes};
use crate::{Cx, Report, Result, State, batch_size, io, wants_audio};
pub(crate) const OPUS_RATE: u32 = 48_000;
// Element ids (with their length markers).
pub(crate) const EBML: u32 = 0x1A45_DFA3;
pub(crate) const SEGMENT: u32 = 0x1853_8067;
pub(crate) const INFO: u32 = 0x1549_A966;
pub(crate) const TRACKS: u32 = 0x1654_AE6B;
pub(crate) const TRACK_ENTRY: u32 = 0xAE;
pub(crate) const CLUSTER: u32 = 0x1F43_B675;
pub(crate) fn id_bytes(id: u32) -> Vec<u8> {
let b = id.to_be_bytes();
let skip = b.iter().position(|x| *x != 0).unwrap_or(3);
b[skip..].to_vec()
}
/// EBML variable-size integer for an element data size (shortest form).
pub(crate) fn vint(n: u64) -> Vec<u8> {
for len in 1..=8u32 {
let max = (1u64 << (7 * len)) - 2;
if n <= max {
let marked = n | (1u64 << (7 * len));
return marked.to_be_bytes()[8 - len as usize..].to_vec();
}
}
let mut v = vec![0x01];
v.extend_from_slice(&n.to_be_bytes()[1..]);
v
}
pub(crate) fn el(out: &mut Vec<u8>, id: u32, data: &[u8]) {
out.extend(id_bytes(id));
out.extend(vint(data.len() as u64));
out.extend_from_slice(data);
}
pub(crate) fn el_uint(out: &mut Vec<u8>, id: u32, v: u64) {
let b = v.to_be_bytes();
let skip = b.iter().position(|x| *x != 0).unwrap_or(7);
el(out, id, &b[skip..]);
}
pub(crate) fn el_float(out: &mut Vec<u8>, id: u32, v: f64) {
el(out, id, &v.to_be_bytes());
}
pub(crate) fn el_str(out: &mut Vec<u8>, id: u32, s: &str) {
el(out, id, s.as_bytes());
}
/// A block payload: track number, timestamp relative to the cluster, flags, frame.
fn block(track: u64, rel: i16, flags: u8, data: &[u8]) -> Vec<u8> {
let mut b = vint(track);
b.extend_from_slice(&rel.to_be_bytes());
b.push(flags);
b.extend_from_slice(data);
b
}
struct Frame {
ms: i64,
track: u64,
/// A key frame (VP9) or any audio packet.
key: bool,
/// Timestamp of the previous video frame (ReferenceBlock of inter frames).
prev_ms: Option<i64>,
data: Vec<u8>,
alpha: Option<Vec<u8>>,
}
fn cluster(frames: &mut [Frame]) -> Vec<u8> {
frames.sort_by_key(|f| (f.ms, f.track));
let base = frames.first().map_or(0, |f| f.ms);
let mut body = vec![];
el_uint(&mut body, 0xE7, base.max(0) as u64);
for f in frames.iter() {
let rel = (f.ms - base).clamp(i16::MIN as i64, i16::MAX as i64) as i16;
match &f.alpha {
None => el(&mut body, 0xA3, &block(f.track, rel, if f.key { 0x80 } else { 0x00 }, &f.data)),
Some(a) => {
let mut g = vec![];
el(&mut g, 0xA1, &block(f.track, rel, 0x00, &f.data));
if let (false, Some(p)) = (f.key, f.prev_ms) {
// ReferenceBlock: a signed timestamp relative to this block (an inter frame).
let d = (p - f.ms).clamp(i16::MIN as i64, -1) as i16;
el(&mut g, 0xFB, &d.to_be_bytes());
}
let mut more = vec![];
el_uint(&mut more, 0xEE, 1);
el(&mut more, 0xA5, a);
let mut adds = vec![];
el(&mut adds, 0xA6, &more);
el(&mut g, 0x75A1, &adds);
el(&mut body, 0xA0, &g);
}
}
}
let mut out = vec![];
el(&mut out, CLUSTER, &body);
out
}
/// The Opus encoder of a WebM output: the module's channel count, bitrate and application.
pub(crate) fn opus_encoder(job: &Cx) -> effectcraft_opusenc::OpusEncoder {
let channels = if job.output.audio_channels == 1 { 1 } else { 2 };
let app = match job.output.opus_application {
effectcraft_project::render_queue::OpusApplication::Audio => effectcraft_opusenc::Application::Audio,
effectcraft_project::render_queue::OpusApplication::Voip => effectcraft_opusenc::Application::Voip,
};
effectcraft_opusenc::OpusEncoder::with_application(channels, job.output.opus_bitrate_kbps.clamp(6, 510) * 1000, app)
}
pub(crate) async fn webm(job: &Cx<'_>, comp: &Comp, w: u32, h: u32, st: &mut State<'_>) -> Result<Report> {
if job.output.webm_codec == effectcraft_project::render_queue::WebmVideoCodec::Av1 {
return crate::webm_av1::webm_av1(job, comp, w, h, st).await;
}
let rate = job.settings.rate(comp);
let alpha = job.output.channels == Channels::Rgba;
let colour_channels = if job.output.channels == Channels::Alpha { Channels::Alpha } else { Channels::Rgb };
let quality = job.output.quality.clamp(1, 100);
let fps = rate.as_f64().max(1.0);
let cfg = effectcraft_vp9enc::EncoderConfig {
width: w,
height: h,
quality,
full_range: false,
keyframe_interval: if job.output.keyframe_interval > 0 { job.output.keyframe_interval } else { (fps * 2.0).round().max(1.0) as u32 },
target_kbps: job.output.webm_bitrate.then_some(job.output.bitrate_kbps.max(50)),
frame_rate: fps,
loop_filter: true,
};
let mut color_enc = effectcraft_vp9enc::Vp9Encoder::new(cfg.clone());
let mut alpha_enc = alpha.then(|| effectcraft_vp9enc::Vp9Encoder::new(cfg.clone()));
let with_audio = wants_audio(job);
let opus_channels = if job.output.audio_channels == 1 { 1usize } else { 2 };
let mut opus = with_audio.then(|| opus_encoder(job));
let total = st.total;
let frame_ms = |k: u64| -> i64 { ((k as i128 * 1000 * rate.den as i128 + rate.num as i128 / 2) / rate.num as i128) as i64 };
let duration_ms = frame_ms(total) as f64;
let path = job.place(job.path, 1);
let mut file = job.create(&path)?;
// EBML header.
let mut head = vec![];
let mut eb = vec![];
el_uint(&mut eb, 0x4286, 1);
el_uint(&mut eb, 0x42F7, 1);
el_uint(&mut eb, 0x42F2, 4);
el_uint(&mut eb, 0x42F3, 8);
el_str(&mut eb, 0x4282, "webm");
el_uint(&mut eb, 0x4287, 4);
el_uint(&mut eb, 0x4285, 2);
el(&mut head, EBML, &eb);
// Segment with an 8-byte size patched at the end.
head.extend(id_bytes(SEGMENT));
let size_pos = head.len() as u64;
head.extend_from_slice(&[0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
let seg_start = head.len() as u64;
let mut info = vec![];
el_uint(&mut info, 0x2AD7B1, 1_000_000);
el_float(&mut info, 0x4489, duration_ms);
el_str(&mut info, 0x4D80, "EffectCraft");
el_str(&mut info, 0x5741, "EffectCraft");
el(&mut head, INFO, &info);
let mut tracks = vec![];
let mut v = vec![];
el_uint(&mut v, 0xD7, 1);
el_uint(&mut v, 0x73C5, 1);
el_uint(&mut v, 0x83, 1);
el_uint(&mut v, 0x9C, 0);
el_str(&mut v, 0x86, "V_VP9");
el_uint(&mut v, 0x23E383, (1_000_000_000u128 * rate.den as u128 / rate.num.max(1) as u128) as u64);
if alpha {
el_uint(&mut v, 0x55EE, 1);
}
let mut video = vec![];
el_uint(&mut video, 0xB0, w as u64);
el_uint(&mut video, 0xBA, h as u64);
if alpha {
el_uint(&mut video, 0x53C0, 1);
}
el(&mut v, 0xE0, &video);
el(&mut tracks, TRACK_ENTRY, &v);
if let Some(o) = &opus {
let mut a = vec![];
el_uint(&mut a, 0xD7, 2);
el_uint(&mut a, 0x73C5, 2);
el_uint(&mut a, 0x83, 2);
el_uint(&mut a, 0x9C, 0);
el_str(&mut a, 0x86, "A_OPUS");
el(&mut a, 0x63A2, &o.opus_head());
el_uint(&mut a, 0x56AA, o.pre_skip() as u64 * 1_000_000_000 / OPUS_RATE as u64);
el_uint(&mut a, 0x56BB, 80_000_000);
let mut au = vec![];
el_float(&mut au, 0xB5, OPUS_RATE as f64);
el_uint(&mut au, 0x9F, opus_channels as u64);
el(&mut a, 0xE1, &au);
el(&mut tracks, TRACK_ENTRY, &a);
}
el(&mut head, TRACKS, &tracks);
file.write_all(&head).map_err(io)?;
let mut seg_len = head.len() as u64 - seg_start;
// Audio: mixed in blocks, encoded in 20 ms packets.
let (span_start, span_end) = job.settings.span(comp);
let mut cursor = span_start.to_units_floor(OPUS_RATE as i64);
let mut pcm_left: Vec<f32> = vec![];
let mut packets = 0u64;
let mut audio_until = |until: Tick, frames: &mut Vec<Frame>, finish: bool| {
let Some(enc) = opus.as_mut() else { return };
let end = until.to_units_floor(OPUS_RATE as i64);
if end > cursor {
let n = (end - cursor) as usize;
let start = Tick(((cursor as i128 * TICKS_PER_SECOND as i128) / OPUS_RATE as i128) as i64);
pcm_left.extend(mix(job, start, n, OPUS_RATE));
cursor = end;
}
let fs = effectcraft_opusenc::OpusEncoder::FRAME_SIZE * opus_channels;
if finish && !pcm_left.is_empty() {
// Pad the last packet (pre-skip + padding cover the encoder delay).
let pad = (fs - pcm_left.len() % fs) % fs + fs;
pcm_left.extend(std::iter::repeat_n(0.0, pad));
}
while pcm_left.len() >= fs {
let pkt = enc.encode_float(&pcm_left[..fs]);
pcm_left.drain(..fs);
frames.push(Frame { ms: packets as i64 * 20, track: 2, key: true, prev_ms: None, data: pkt, alpha: None });
packets += 1;
}
};
let batch = batch_size();
let mut i = 0;
while i < total {
let end = (i + batch).min(total);
// Frames render in parallel; the VP9 streams (inter-coded) encode in order, colour and
// alpha side by side.
let pixels: Vec<Vec<u8>> =
job.frames(comp, (i..end).collect(), |_, img| job.pixels(&img, comp, if alpha { Channels::Rgba } else { colour_channels }, w, h)).await;
let mut frames: Vec<Frame> = Vec::with_capacity(pixels.len());
for (j, px) in pixels.iter().enumerate() {
let (color, a) = rayon::join(
|| {
let (y, u, vv) = effectcraft_vp9enc::rgba_to_yuv420(px, w, h, false);
color_enc.encode_frame(&y, &u, &vv, false)
},
|| {
alpha_enc.as_mut().map(|ea| {
let (y, u, vv) = effectcraft_vp9enc::alpha_to_yuv420(px, w, h);
ea.encode_frame(&y, &u, &vv, false).data
})
},
);
let k = i + j as u64;
frames.push(Frame { ms: frame_ms(k), track: 1, key: color.key, prev_ms: (k > 0).then(|| frame_ms(k - 1)), data: color.data, alpha: a });
}
drop(pixels);
let t_end = if end >= total { span_end } else { job.settings.frame_time(comp, end) };
audio_until(t_end, &mut frames, end >= total);
let c = cluster(&mut frames);
file.write_all(&c).map_err(io)?;
seg_len += c.len() as u64;
st.advance(end - i)?;
i = end;
}
if total == 0 {
let mut frames = vec![];
audio_until(span_end, &mut frames, true);
if !frames.is_empty() {
let c = cluster(&mut frames);
file.write_all(&c).map_err(io)?;
seg_len += c.len() as u64;
}
}
// Patch the Segment size.
let mut size = [0u8; 8];
size[0] = 0x01;
size[1..].copy_from_slice(&seg_len.to_be_bytes()[1..]);
file.seek(SeekFrom::Start(size_pos)).map_err(io)?;
file.write_all(&size).map_err(io)?;
file.seek(SeekFrom::End(0)).map_err(io)?;
let bytes = file.finish()?;
Ok(Report { path, frames: 0, width: w, height: h, seconds: 0.0, bytes, audio: with_audio, log: None, overflow: vec![] })
}
// ---------------------------------------------------------------- audio-only outputs
fn ieee_extended(v: f64) -> [u8; 10] {
// 80-bit IEEE 754 extended precision (AIFF COMM sample rate).
let mut out = [0u8; 10];
if v <= 0.0 {
return out;
}
let e = v.log2().floor() as i32;
let mant = (v / 2f64.powi(e) * (1u64 << 63) as f64) as u64;
out[..2].copy_from_slice(&((e + 16383) as u16).to_be_bytes());
out[2..].copy_from_slice(&mant.to_be_bytes());
out
}
/// WAV or AIFF: the comp's audio over the render span (Audio Output: sample rate, mono/stereo,
/// 16/24-bit integer or 32-bit float; AIFF has no float in plain AIFF, so it writes 24-bit).
pub(crate) fn audio_file(job: &Cx, comp: &Comp, aiff: bool, st: &mut State) -> Result<Report> {
let sr = job.output.audio_sample_rate.clamp(8_000, 192_000);
let channels = if job.output.audio_channels == 1 { 1u16 } else { 2 };
let fmt = match job.output.audio_format {
AudioFormat::F32 if aiff => AudioFormat::S24,
f => f,
};
let bytes_per_sample: u16 = match fmt {
AudioFormat::S16 => 2,
AudioFormat::S24 => 3,
AudioFormat::F32 => 4,
};
let (span_start, span_end) = job.settings.span(comp);
let start = span_start.to_units_floor(sr as i64);
let end = span_end.to_units_floor(sr as i64);
let n = (end - start).max(0) as usize;
let mut pcm = Vec::with_capacity(n * (channels * bytes_per_sample) as usize);
// Mix in one-second blocks, reporting progress in frames.
let block = sr as usize;
let mut done = 0usize;
let total_frames = st.total.max(1);
let mut reported = 0u64;
while done < n {
let k = block.min(n - done);
let t = Tick((((start + done as i64) as i128 * TICKS_PER_SECOND as i128) / sr as i128) as i64);
pcm.extend(pcm_bytes(&mix(job, t, k, sr), fmt, aiff));
done += k;
let now = (done as u64 * total_frames / n.max(1) as u64).min(total_frames);
if now > reported && st.total > 0 {
st.advance(now - reported)?;
reported = now;
}
}
if reported < st.total {
st.advance(st.total - reported)?;
}
let block_align = channels * bytes_per_sample;
let frames = (pcm.len() / block_align as usize) as u32;
let mut out = vec![];
if aiff {
let mut comm = vec![];
comm.extend_from_slice(&channels.to_be_bytes());
comm.extend_from_slice(&frames.to_be_bytes());
comm.extend_from_slice(&(bytes_per_sample * 8).to_be_bytes());
comm.extend_from_slice(&ieee_extended(sr as f64));
let ssnd_len = 8 + pcm.len();
out.extend_from_slice(b"FORM");
out.extend_from_slice(&((4 + 8 + comm.len() + 8 + ssnd_len + ssnd_len % 2) as u32).to_be_bytes());
out.extend_from_slice(b"AIFF");
out.extend_from_slice(b"COMM");
out.extend_from_slice(&(comm.len() as u32).to_be_bytes());
out.extend_from_slice(&comm);
out.extend_from_slice(b"SSND");
out.extend_from_slice(&(ssnd_len as u32).to_be_bytes());
out.extend_from_slice(&[0; 8]);
out.extend_from_slice(&pcm);
if ssnd_len % 2 == 1 {
out.push(0);
}
} else {
// WAVE_FORMAT_PCM (1) or WAVE_FORMAT_IEEE_FLOAT (3).
let tag: u16 = if fmt == AudioFormat::F32 { 3 } else { 1 };
out.extend_from_slice(b"RIFF");
out.extend_from_slice(&((36 + pcm.len() + pcm.len() % 2) as u32).to_le_bytes());
out.extend_from_slice(b"WAVEfmt ");
out.extend_from_slice(&16u32.to_le_bytes());
out.extend_from_slice(&tag.to_le_bytes());
out.extend_from_slice(&channels.to_le_bytes());
out.extend_from_slice(&sr.to_le_bytes());
out.extend_from_slice(&(sr * block_align as u32).to_le_bytes());
out.extend_from_slice(&block_align.to_le_bytes());
out.extend_from_slice(&(bytes_per_sample * 8).to_le_bytes());
out.extend_from_slice(b"data");
out.extend_from_slice(&(pcm.len() as u32).to_le_bytes());
out.extend_from_slice(&pcm);
if pcm.len() % 2 == 1 {
out.push(0);
}
}
let path = job.place(job.path, out.len() as u64);
let mut file = job.create(&path)?;
file.write_all(&out).map_err(io)?;
let bytes = file.finish()?;
Ok(Report { path, frames: 0, width: 0, height: 0, seconds: 0.0, bytes, audio: true, log: None, overflow: vec![] })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vints_and_ids() {
assert_eq!(vint(0), vec![0x80]);
assert_eq!(vint(126), vec![0xFE]);
assert_eq!(vint(127), vec![0x40, 0x7F]);
assert_eq!(id_bytes(0xA3), vec![0xA3]);
assert_eq!(id_bytes(EBML), vec![0x1A, 0x45, 0xDF, 0xA3]);
assert_eq!(ieee_extended(44100.0), [0x40, 0x0E, 0xAC, 0x44, 0, 0, 0, 0, 0, 0]);
assert_eq!(ieee_extended(48000.0)[..4], [0x40, 0x0E, 0xBB, 0x80]);
}
}