mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 21:48:20 -06:00
Merge main into M13.4 (codec completion): HEVC/AV1 on the export pipeline (Cx), one key-frame interval for all movie codecs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+58
-22
@@ -5,12 +5,12 @@
|
||||
use std::io::Write;
|
||||
|
||||
use effectcraft_project::Comp;
|
||||
use effectcraft_project::render_queue::{Channels, OutputFormat, ProResProfile};
|
||||
use effectcraft_project::render_queue::{AudioFormat, Channels, OutputFormat, ProResProfile};
|
||||
use effectcraft_time::{FrameRate, TICKS_PER_SECOND, Tick};
|
||||
use filmcraft_isobmff::{Brand, FourCc, Mp4Writer, PcmConfig, SampleEntry, TrackConfig, WriteSample, WriterOptions};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::{ExportError, Job, Report, Result, State, batch_size, render_frame, rgba8, wants_audio};
|
||||
use crate::{Cx, ExportError, Report, Result, State, batch_size, wants_audio};
|
||||
|
||||
fn enc(e: impl std::fmt::Display) -> ExportError {
|
||||
ExportError::Encode(e.to_string())
|
||||
@@ -209,31 +209,62 @@ enum Audio {
|
||||
Pcm,
|
||||
}
|
||||
|
||||
const AUDIO_CHANNELS: u32 = 2;
|
||||
fn deinterleave(buf: &[f32], channels: usize) -> Vec<Vec<f32>> {
|
||||
(0..channels).map(|c| buf.chunks_exact(channels).map(|p| p[c]).collect()).collect()
|
||||
}
|
||||
|
||||
fn deinterleave(buf: &[f32]) -> [Vec<f32>; 2] {
|
||||
let l = buf.chunks_exact(2).map(|p| p[0]).collect();
|
||||
let r = buf.chunks_exact(2).map(|p| p[1]).collect();
|
||||
[l, r]
|
||||
/// The comp's audio over `n` samples from `start`, interleaved with the module's channel count
|
||||
/// (mono = the average of left and right).
|
||||
pub(crate) fn mix(cx: &Cx, start: effectcraft_time::Tick, n: usize, sr: u32) -> Vec<f32> {
|
||||
let st = effectcraft_render::audio::mix_comp(&cx.project, cx.footage, cx.expr, cx.comp, start, n, sr);
|
||||
if cx.output.audio_channels == 1 { st.chunks_exact(2).map(|p| (p[0] + p[1]) * 0.5).collect() } else { st }
|
||||
}
|
||||
|
||||
/// PCM bytes of samples in `fmt` (little or big endian).
|
||||
pub(crate) fn pcm_bytes(buf: &[f32], fmt: AudioFormat, big_endian: bool) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(buf.len() * 4);
|
||||
for &s in buf {
|
||||
let s = s.clamp(-1.0, 1.0);
|
||||
match fmt {
|
||||
AudioFormat::S16 => {
|
||||
let v = (s * 32767.0).round() as i16;
|
||||
out.extend_from_slice(&if big_endian { v.to_be_bytes() } else { v.to_le_bytes() });
|
||||
}
|
||||
AudioFormat::S24 => {
|
||||
let v = (s * 8_388_607.0).round() as i32;
|
||||
let b = v.to_le_bytes();
|
||||
if big_endian {
|
||||
out.extend_from_slice(&[b[2], b[1], b[0]]);
|
||||
} else {
|
||||
out.extend_from_slice(&b[..3]);
|
||||
}
|
||||
}
|
||||
AudioFormat::F32 => out.extend_from_slice(&if big_endian { s.to_be_bytes() } else { s.to_le_bytes() }),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- the movie
|
||||
|
||||
type Writer<'f, 's> = Mp4Writer<&'f mut crate::out::Out<'s>>;
|
||||
|
||||
pub(crate) fn movie(job: &Job, comp: &Comp, w: u32, h: u32, st: &mut State) -> Result<Report> {
|
||||
pub(crate) fn movie(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Result<Report> {
|
||||
let rate = job.settings.rate(comp);
|
||||
let fmt = job.output.format;
|
||||
let channels = if fmt == OutputFormat::ProRes { job.output.channels } else { Channels::Rgb };
|
||||
let channels = match job.output.channels {
|
||||
Channels::Rgba if fmt != OutputFormat::ProRes => Channels::Rgb,
|
||||
c => c,
|
||||
};
|
||||
let mut venc: Box<dyn VideoEncoder> = match fmt {
|
||||
OutputFormat::H264 => Box::new(H264::new(w, h, rate, job.output.bitrate_kbps)?),
|
||||
OutputFormat::Hevc => Box::new(crate::hevc_av1::Hevc::new(w, h, rate, job.output)?),
|
||||
OutputFormat::Av1 => Box::new(crate::hevc_av1::Av1::new(w, h, rate, job.output)?),
|
||||
_ => Box::new(ProRes::new(w, h, job.output.prores_profile, channels)),
|
||||
_ => Box::new(ProRes::new(w, h, job.output.prores_profile, if channels == Channels::Alpha { Channels::Rgb } else { channels })),
|
||||
};
|
||||
let brand = if fmt == OutputFormat::ProRes { Brand::Mov } else { Brand::Mp4 };
|
||||
let batch = batch_size();
|
||||
let render = |k: u64| -> Vec<u8> { rgba8(&render_frame(job, comp, k), comp, channels, w, h) };
|
||||
let render = |k: u64| -> Vec<u8> { job.pixels(&job.frame(comp, k), comp, channels, w, h) };
|
||||
|
||||
// Encode the first batch before creating tracks: encoders finalise their config on frame 1.
|
||||
let first_end = batch.min(st.total);
|
||||
@@ -245,7 +276,8 @@ pub(crate) fn movie(job: &Job, comp: &Comp, w: u32, h: u32, st: &mut State) -> R
|
||||
drop(first);
|
||||
st.advance(first_end)?;
|
||||
|
||||
let mut file = crate::out::create(job.sink, job.path)?;
|
||||
let path = job.place(job.path, 1);
|
||||
let mut file = crate::out::create(job.sink, &path)?;
|
||||
let opts = WriterOptions::new(brand);
|
||||
let movie_ts = opts.movie_timescale.max(1) as i128;
|
||||
let mut mux: Writer = Mp4Writer::new(&mut file, opts).map_err(mux_err)?;
|
||||
@@ -260,15 +292,22 @@ pub(crate) fn movie(job: &Job, comp: &Comp, w: u32, h: u32, st: &mut State) -> R
|
||||
let vt = mux.add_track(vcfg).map_err(mux_err)?;
|
||||
let sr = job.output.audio_sample_rate.clamp(8_000, 192_000);
|
||||
let with_audio = wants_audio(job);
|
||||
let chans = if job.output.audio_channels == 1 { 1u32 } else { 2 };
|
||||
let afmt = job.output.audio_format;
|
||||
let mut audio = None;
|
||||
if with_audio {
|
||||
let (a, entry, start) = if brand == Brand::Mp4 {
|
||||
let e = filmcraft_aac::Encoder::new(filmcraft_aac::EncoderConfig::cbr(sr, AUDIO_CHANNELS as usize, 320_000)).map_err(enc)?;
|
||||
let entry = SampleEntry::aac(e.audio_specific_config(), AUDIO_CHANNELS, sr);
|
||||
let e = filmcraft_aac::Encoder::new(filmcraft_aac::EncoderConfig::cbr(sr, chans as usize, 160_000 * chans)).map_err(enc)?;
|
||||
let entry = SampleEntry::aac(e.audio_specific_config(), chans, sr);
|
||||
let priming = e.priming_samples() as i64;
|
||||
(Audio::Aac(Box::new(e)), entry, Some(priming))
|
||||
} else {
|
||||
let pcm = PcmConfig { bits: 16, float: false, big_endian: false, signed: true, channels: AUDIO_CHANNELS, sample_rate: sr as f64 };
|
||||
let (bits, float) = match afmt {
|
||||
AudioFormat::S16 => (16, false),
|
||||
AudioFormat::S24 => (24, false),
|
||||
AudioFormat::F32 => (32, true),
|
||||
};
|
||||
let pcm = PcmConfig { bits, float, big_endian: false, signed: true, channels: chans, sample_rate: sr as f64 };
|
||||
(Audio::Pcm, SampleEntry::pcm(pcm), None)
|
||||
};
|
||||
let mut c = TrackConfig::new(entry, sr);
|
||||
@@ -293,21 +332,18 @@ pub(crate) fn movie(job: &Job, comp: &Comp, w: u32, h: u32, st: &mut State) -> R
|
||||
}
|
||||
let n = (end - *cursor) as usize;
|
||||
let start = Tick(((*cursor as i128 * TICKS_PER_SECOND as i128) / sr as i128) as i64);
|
||||
let buf = effectcraft_render::audio::mix_comp(job.project, job.footage, job.expr, job.comp, start, n, sr);
|
||||
let buf = mix(job, start, n, sr);
|
||||
*cursor = end;
|
||||
match a {
|
||||
Audio::Aac(e) => {
|
||||
let planar = deinterleave(&buf);
|
||||
let planar = deinterleave(&buf, chans as usize);
|
||||
let refs: Vec<&[f32]> = planar.iter().map(Vec::as_slice).collect();
|
||||
for au in e.encode(&refs) {
|
||||
mux.write_sample(*at, WriteSample { data: &au, duration: 1024, composition_offset: 0, is_sync: true }).map_err(mux_err)?;
|
||||
}
|
||||
}
|
||||
Audio::Pcm => {
|
||||
let mut pcm = Vec::with_capacity(n * 4);
|
||||
for s in buf {
|
||||
pcm.extend_from_slice(&((s.clamp(-1.0, 1.0) * 32767.0).round() as i16).to_le_bytes());
|
||||
}
|
||||
let pcm = pcm_bytes(&buf, afmt, false);
|
||||
mux.write_sample(*at, WriteSample { data: &pcm, duration: n as u32, composition_offset: 0, is_sync: true }).map_err(mux_err)?;
|
||||
}
|
||||
}
|
||||
@@ -339,7 +375,7 @@ pub(crate) fn movie(job: &Job, comp: &Comp, w: u32, h: u32, st: &mut State) -> R
|
||||
let out = mux.finish().map_err(mux_err)?;
|
||||
out.flush().map_err(mux_err)?;
|
||||
let bytes = file.finish()?;
|
||||
Ok(Report { path: job.path.to_string(), frames: 0, width: w, height: h, seconds: 0.0, bytes, audio: with_audio })
|
||||
Ok(Report { path, frames: 0, width: w, height: h, seconds: 0.0, bytes, audio: with_audio, log: None, overflow: vec![] })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user