From 1906d9d639840865876b0bd1cc05c65dcaaf15ce Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 3 Oct 2026 09:17:57 -0400 Subject: [PATCH] M13.4: WebM Opus uses the module's bitrate and application (SILK / hybrid / CELT), low-bitrate export test Co-Authored-By: Claude Opus 5.5 --- crates/export/src/webm.rs | 7 +++-- crates/export/tests/hevc_av1_roundtrip.rs | 32 ++++++++++++++++++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/export/src/webm.rs b/crates/export/src/webm.rs index bb3daa039..de7c86841 100644 --- a/crates/export/src/webm.rs +++ b/crates/export/src/webm.rs @@ -126,9 +126,12 @@ fn cluster(frames: &mut [Frame]) -> Vec { /// 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 { - // TEMP(M13.4): application mapping lands with the SILK encoder. let channels = if job.output.audio_channels == 1 { 1 } else { 2 }; - effectcraft_opusenc::OpusEncoder::new(channels, job.output.opus_bitrate_kbps.clamp(6, 510) * 1000) + 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) fn webm(job: &Cx, comp: &Comp, w: u32, h: u32, st: &mut State) -> Result { diff --git a/crates/export/tests/hevc_av1_roundtrip.rs b/crates/export/tests/hevc_av1_roundtrip.rs index c56616742..93a82719a 100644 --- a/crates/export/tests/hevc_av1_roundtrip.rs +++ b/crates/export/tests/hevc_av1_roundtrip.rs @@ -9,7 +9,9 @@ use std::path::{Path, PathBuf}; use std::process::Command; use effectcraft_color::Label; -use effectcraft_export::render_queue::{AudioOutput, CodecProfile, OutputFormat, OutputModule, RateControlMode, RenderSettings, TimeSpan, WebmVideoCodec}; +use effectcraft_export::render_queue::{ + AudioOutput, CodecProfile, OpusApplication, OutputFormat, OutputModule, RateControlMode, RenderSettings, TimeSpan, WebmVideoCodec, +}; use effectcraft_export::{Job, Progress, export}; use effectcraft_media::MediaPool; use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, Project, Solid, build}; @@ -250,6 +252,34 @@ fn av1_webm_with_opus() { ffmpeg_check(&path, Some("libdav1d"), 0.08); } +/// Low-bitrate WebM audio goes through Opus SILK (voice) or hybrid (audio) coding and still +/// decodes to the tone (FilmCraft's decoder, and ffmpeg when installed). +#[test] +fn webm_low_bitrate_opus_modes() { + let d = out_dir("opus-modes"); + let wav = d.join("tone.wav"); + write_tone(&wav, 2.0); + let (p, cid) = project(Some(&wav)); + let pool = MediaPool::new(); + for (kbps, app, name) in [(24u32, OpusApplication::Voip, "voice24.webm"), (40, OpusApplication::Audio, "audio40.webm")] { + let mut om = OutputModule::for_format(OutputFormat::WebM); + om.audio = AudioOutput::On; + om.opus_bitrate_kbps = kbps; + om.opus_application = app; + let path = d.join(name); + assert!(run(&p, cid, &pool, &om, &path).audio); + let f = effectcraft_media::probe(&path).expect("probe"); + let s = MediaPool::new().audio_samples(&f, Tick::from_seconds_f64(0.3), 24_000, 48_000); + let l = rms(s.iter().step_by(2).copied()); + assert!((l - 0.3536).abs() < 0.08, "{name}: left RMS {l}"); + if have("ffmpeg") { + let out = + Command::new("ffmpeg").args(["-v", "error", "-xerror", "-i"]).arg(&path).args(["-map", "0:a", "-f", "null", "-"]).output().expect("ffmpeg"); + assert!(out.status.success() && out.stderr.is_empty(), "{name}: {}", String::from_utf8_lossy(&out.stderr)); + } + } +} + fn rms(v: impl Iterator) -> f32 { let (s, n) = v.fold((0.0f64, 0usize), |(s, n), x| (s + (x as f64) * (x as f64), n + 1)); (s / n.max(1) as f64).sqrt() as f32