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 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-03 09:17:57 -04:00
co-authored by Claude Opus 5.5
parent 3eeba31c43
commit 1906d9d639
2 changed files with 36 additions and 3 deletions
+5 -2
View File
@@ -126,9 +126,12 @@ fn cluster(frames: &mut [Frame]) -> Vec<u8> {
/// 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<Report> {
+31 -1
View File
@@ -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<Item = f32>) -> 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