M7.7: project/render/engine: 53 Premiere audio effects in folders, DSP mappings, clip and track application

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-02 11:04:47 -04:00
co-authored by Claude Opus 5.5
parent 9ef525441a
commit 4c0f01166f
7 changed files with 1048 additions and 107 deletions
+113
View File
@@ -0,0 +1,113 @@
//! The Premiere audio-effect set through engine commands: listed with folders by
//! `effects.list`, applicable as clip effects (`effects.apply`) and track effects
//! (`mixer.addInsert`), parameters settable by id, and the sequence still renders.
use super::*;
use filmcraft_project::ParamValue;
use filmcraft_project::effect::PREMIERE_AUDIO_EFFECTS;
use serde_json::json;
fn demo() -> Session {
let mut s = Session::default();
s.execute("file.openDemoProject", json!({})).unwrap();
s
}
fn a1_clip(s: &Session) -> u64 {
s.active_sequence().unwrap().audio_tracks[0].items[0].id.0
}
fn render(s: &Session, frames: usize) -> filmcraft_frame::AudioBuffer {
let provider = s.media.provider(s.project.clone(), s.services.clone());
filmcraft_render::audio::mix_sequence(&s.project, s.active_sequence().unwrap(), 0, frames, &provider)
}
#[test]
fn effects_list_has_every_audio_effect_in_its_folder() {
let mut s = demo();
let list = s.execute("effects.list", json!({})).unwrap();
let arr = list.as_array().unwrap();
for id in PREMIERE_AUDIO_EFFECTS {
let e = arr.iter().find(|e| e["id"] == id).unwrap_or_else(|| panic!("{id} not listed"));
assert_eq!(e["kind"], "Audio", "{id}");
assert_eq!(e["category"][0], "Audio Effects", "{id}");
}
let folder = |id: &str| arr.iter().find(|e| e["id"] == id).unwrap()["category"][1].clone();
assert_eq!(folder("tube_compressor"), "Amplitude and Compression");
assert_eq!(folder("chorus_flanger"), "Modulation");
assert_eq!(folder("binauralizer"), "Special");
assert!(folder("volume_a").is_null(), "loose item");
}
#[test]
fn every_audio_effect_applies_to_clips_and_tracks_and_renders() {
let mut s = demo();
let clip = a1_clip(&s);
for id in PREMIERE_AUDIO_EFFECTS {
let n0 = s.active_sequence().unwrap().find_item(ClipId(clip)).unwrap().1.effects.len();
s.execute("effects.apply", json!({"clips": [clip], "effect": id})).unwrap_or_else(|e| panic!("{id}: {e}"));
let it = s.active_sequence().unwrap().find_item(ClipId(clip)).unwrap().1.clone();
assert_eq!(it.effects.len(), n0 + 1, "{id}");
assert_eq!(it.effects.last().unwrap().effect, id);
assert!(filmcraft_render::audio_fx::supported(id), "{id}");
s.execute("mixer.addInsert", json!({"strip": "A1", "effect": id})).unwrap_or_else(|e| panic!("{id}: {e}"));
let b = render(&s, 2400);
assert_eq!(b.frames(), 2400);
assert!(b.channels.iter().flatten().all(|v| v.is_finite()), "{id}: non-finite mix");
s.execute("mixer.removeInsert", json!({"strip": "A1", "slot": 0})).unwrap();
s.execute("effects.remove", json!({"clip": clip, "index": n0})).unwrap();
}
}
#[test]
fn parameters_are_set_by_id_and_change_the_sound() {
let mut s = demo();
let clip = a1_clip(&s);
let before = render(&s, 4800);
let peak = |b: &filmcraft_frame::AudioBuffer| b.channels.iter().flatten().fold(0.0f32, |a, v| a.max(v.abs()));
assert!(peak(&before) > 1e-3, "the demo has audio on A1");
// Mute (a loose item) silences the clip.
s.execute("effects.apply", json!({"clips": [clip], "effect": "Mute"})).unwrap();
let it = s.active_sequence().unwrap().find_item(ClipId(clip)).unwrap().1.clone();
let idx = it.effects.iter().position(|e| e.effect == "mute").unwrap();
s.execute("effects.setParam", json!({"clip": clip, "effect": idx, "param": "mute", "value": true})).unwrap();
let it = s.active_sequence().unwrap().find_item(ClipId(clip)).unwrap().1.clone();
assert_eq!(it.effects[idx].param("mute").unwrap().value, ParamValue::Bool(true));
// the clip's own signal (other tracks of the demo keep playing)
let it = s.active_sequence().unwrap().find_item(ClipId(clip)).unwrap().1.clone();
let provider = s.media.provider(s.project.clone(), s.services.clone());
let a0 = it.start.to_units_floor(48000);
let sig = filmcraft_render::audio::clip_signal(&it, a0, 4800, 48000, &provider).unwrap();
let pk = sig.iter().flatten().fold(0.0f32, |a, v| a.max(v.abs()));
assert!(pk < 1e-6, "muted clip peak {pk}");
s.execute("edit.undo", json!({})).unwrap();
s.execute("edit.undo", json!({})).unwrap();
// A track-level Graphic Equalizer, its master gain set through mixer.setInsert (A1 soloed so
// only it is heard).
s.execute("mixer.setStrip", json!({"strip": "A1", "solo": true})).unwrap();
let before = render(&s, 4800);
s.execute("mixer.addInsert", json!({"strip": "A1", "effect": "graphic_eq_30"})).unwrap();
s.execute("mixer.setInsert", json!({"strip": "A1", "slot": 0, "params": {"gain": -12.0}})).unwrap();
let quieter = render(&s, 4800);
let rms = |b: &filmcraft_frame::AudioBuffer| {
(b.channels.iter().flatten().map(|v| (*v as f64).powi(2)).sum::<f64>() / (b.frames() * b.channel_count()) as f64).sqrt()
};
let d = 20.0 * (rms(&quieter) / rms(&before)).log10();
assert!((d + 12.0).abs() < 1.0, "master gain −12 dB → {d}");
}
#[test]
fn shared_names_resolve_by_track_kind() {
let mut s = demo();
let clip = a1_clip(&s);
for (name, id) in [("Invert", "invert_a"), ("Volume", "volume_a"), ("Channel Volume", "channel_volume_a"), ("Balance", "balance_a")] {
s.execute("effects.apply", json!({"clips": [clip], "effect": name})).unwrap();
let it = s.active_sequence().unwrap().find_item(ClipId(clip)).unwrap().1.clone();
assert_eq!(it.effects.last().unwrap().effect, id, "{name}");
}
// On a video clip "Invert" is still the video effect.
let v = s.active_sequence().unwrap().video_tracks[0].items[0].id.0;
s.execute("effects.apply", json!({"clips": [v], "effect": "Invert"})).unwrap();
let it = s.active_sequence().unwrap().find_item(ClipId(v)).unwrap().1.clone();
assert!(it.effects.iter().any(|e| e.effect == "invert"));
}
+21 -1
View File
@@ -152,6 +152,26 @@ pub(crate) fn time_p(s: &Session, p: &Value, prefix: &str) -> Option<Tick> {
pub(crate) fn clip_p(p: &Value, k: &str) -> Option<ClipId> {
u64_p(p, k).map(ClipId)
}
/// Effect by id or display name. Names shared by several definitions ("Invert", "Volume",
/// "Channel Volume") resolve to an applicable (non-intrinsic) effect whose kind matches the
/// target clips' tracks.
fn resolve_effect_name(s: &Session, p: &Value, name: &str) -> Option<&'static filmcraft_project::EffectDef> {
if let Some(d) = filmcraft_project::find_effect(name) {
return Some(d);
}
let n = name.to_ascii_lowercase();
let cands: Vec<_> = filmcraft_project::effect_defs().iter().filter(|e| e.name.to_ascii_lowercase() == n).collect();
if cands.len() <= 1 {
return cands.first().copied().or_else(|| filmcraft_project::find_effect(&n));
}
let on_audio = s.active_sequence().is_some_and(|q| {
let clips = clips_p(s, p);
!clips.is_empty() && clips.iter().all(|c| q.find_item(*c).and_then(|(t, _)| q.track(t)).is_some_and(|t| t.kind == TrackKind::Audio))
});
let want = if on_audio { filmcraft_project::EffectKind::Audio } else { filmcraft_project::EffectKind::Video };
cands.iter().find(|d| !d.intrinsic && d.kind == want).or_else(|| cands.iter().find(|d| !d.intrinsic)).or(cands.first()).copied()
}
pub(crate) fn clips_p(s: &Session, p: &Value) -> Vec<ClipId> {
match p.get("clips").and_then(Value::as_array) {
Some(a) => a.iter().filter_map(|v| v.as_u64().map(ClipId)).collect(),
@@ -1723,7 +1743,7 @@ fn build() -> Vec<CommandSpec> {
// ================= Effects =================
cmd!("effects.apply", "Apply Effect", [], None, r#"{"clips":[id]?,"effect":"gaussian_blur|Gaussian Blur|…"}"#, has_seq, |s, p| {
let name = str_p(p, "effect").ok_or_else(|| bad("effects.apply", "need `effect`"))?;
let def = filmcraft_project::effect::find_effect_by_name(name).ok_or_else(|| bad("effects.apply", format!("unknown effect `{name}`")))?;
let def = resolve_effect_name(s, p, name).ok_or_else(|| bad("effects.apply", format!("unknown effect `{name}`")))?;
if filmcraft_project::graphic::is_layer_id(def.id) {
return Err(bad("effects.apply", "graphic layers are added with graphics.newText / graphics.newShape"));
}
+2
View File
@@ -722,6 +722,8 @@ pub fn media_duration(p: &Project, _pool: &MediaPool, id: ItemId) -> Option<Tick
}
}
#[cfg(test)]
mod audio_effects_tests;
#[cfg(test)]
mod autosave_tests;
#[cfg(test)]
+20 -1
View File
@@ -452,7 +452,7 @@ pub fn strip_json(s: &Session, seq: &Sequence, id: TrackId, t: Tick) -> Value {
"soloSafe": tr.mixer.solo_safe,
"inputMap": tr.mixer.input_map.label(),
"output": tr.mixer.output.map(|o| strip_label(seq, o)).unwrap_or_else(|| if id == MASTER_STRIP { String::new() } else { "Mix".into() }),
"inserts": tr.effects.iter().enumerate().map(|(i, e)| json!({"slot": i, "effect": e.effect, "name": e.def().map(|d| d.name).unwrap_or(""), "enabled": e.enabled, "postFader": e.post_fader})).collect::<Vec<_>>(),
"inserts": tr.effects.iter().enumerate().map(|(i, e)| json!({"slot": i, "effect": e.effect, "name": e.def().map(|d| d.name).unwrap_or(""), "enabled": e.enabled, "postFader": e.post_fader, "params": insert_params(e)})).collect::<Vec<_>>(),
"sends": tr.mixer.sends.iter().enumerate().map(|(i, sd)| json!({"send": i, "target": strip_label(seq, sd.target), "levelDb": sd.level_db, "pan": sd.pan, "preFader": sd.pre_fader, "muted": sd.muted})).collect::<Vec<_>>(),
"at": {"time": t.0, "volumeDb": shown(LANE_VOLUME), "pan": shown(LANE_PAN), "mute": shown(LANE_MUTE) >= 0.5},
"lanes": lanes,
@@ -583,6 +583,19 @@ fn slot_p(p: &Value, cmd: &str) -> Result<usize> {
u64_p(p, "slot").map(|v| v as usize).ok_or_else(|| bad(cmd, "need `slot` (0-based)"))
}
/// An insert's scalar parameter values (numbers; toggles as booleans) for `mixer.inspect`.
fn insert_params(e: &filmcraft_project::EffectInstance) -> Value {
let mut m = serde_json::Map::new();
for (k, p) in &e.params {
let v = match &p.value {
ParamValue::Bool(b) => json!(b),
v => v.as_f64().map_or(Value::Null, |x| json!(x)),
};
m.insert(k.clone(), v);
}
Value::Object(m)
}
fn add_insert(s: &mut Session, p: &Value) -> Result<Value> {
let id = strip_p(s, p, "mixer.addInsert")?;
let eid = str_p(p, "effect").ok_or_else(|| bad("mixer.addInsert", "need `effect` (an audio effect id)"))?;
@@ -628,6 +641,12 @@ fn set_insert(s: &mut Session, p: &Value) -> Result<Value> {
}
if let Some(obj) = p.get("params").and_then(Value::as_object) {
for (k, v) in obj {
// instances saved before a parameter existed get it from the definition
if e.param(k).is_none()
&& let Some(d) = e.def().and_then(|d| d.param(k))
{
e.params.insert(k.clone(), filmcraft_project::Param::new(d.default.clone()));
}
let par = e.param_mut(k).ok_or_else(|| bad("mixer.setInsert", format!("no parameter `{k}`")))?;
par.value = match (&par.value, v) {
(ParamValue::Bool(_), Value::Bool(b)) => ParamValue::Bool(*b),
+5 -104
View File
@@ -11,7 +11,10 @@ use filmcraft_geom::Vec2;
use serde::{Deserialize, Serialize};
use crate::keyframe::{Param, ParamValue};
mod audio;
use crate::mask::Mask;
pub use audio::{GEQ10_LABELS, GEQ20_LABELS, GEQ30_LABELS, PREMIERE_AUDIO_EFFECTS};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum EffectKind {
@@ -819,110 +822,7 @@ fn build_effects() -> Vec<EffectDef> {
vtrans("cube_spin", "Cube Spin", MOTION3D, vec![]),
vtrans("flip_over", "Flip Over", MOTION3D, vec![]),
// ---- audio effects ----
audio("amplify", "Amplify", A_AMP, vec![fs("gain", "Gain", 0.0, (-96.0, 48.0), (-24.0, 24.0), "dB", 1)]),
audio(
"dynamics",
"Dynamics Processing",
A_AMP,
vec![
fs("threshold", "Threshold", -20.0, (-60.0, 0.0), (-60.0, 0.0), "dB", 1),
fs("ratio", "Ratio", 4.0, (1.0, 30.0), (1.0, 30.0), ":1", 1),
fs("attack", "Attack", 10.0, (0.1, 500.0), (0.1, 500.0), "ms", 1),
fs("release", "Release", 100.0, (1.0, 5000.0), (1.0, 5000.0), "ms", 0),
],
),
audio(
"hard_limiter",
"Hard Limiter",
A_AMP,
vec![
fs("max", "Maximum Amplitude", -0.1, (-30.0, 0.0), (-30.0, 0.0), "dB", 1),
fs("boost", "Input Boost", 0.0, (-30.0, 30.0), (-30.0, 30.0), "dB", 1),
fs("lookahead", "Look-Ahead Time", 7.0, (0.0, 30.0), (0.0, 30.0), "ms", 1),
fs("release", "Release Time", 100.0, (1.0, 1000.0), (1.0, 1000.0), "ms", 0),
],
),
audio("multiband_compressor", "Multiband Compressor", A_AMP, vec![]),
audio("channel_mixer_a", "Channel Mixer", A_AMP, vec![]),
audio(
"delay",
"Delay",
A_DELAY,
vec![
fs("delay", "Delay", 1.0, (0.0, 2.0), (0.0, 2.0), "s", 2),
f("feedback", "Feedback", 0.0, 0.0, 100.0, "%"),
f("mix", "Mix", 50.0, 0.0, 100.0, "%"),
],
),
audio("analog_delay", "Analog Delay", A_DELAY, vec![]),
audio(
"parametric_eq",
"Parametric Equalizer",
A_FILTER,
vec![
fs("low_freq", "Low Frequency", 100.0, (20.0, 20000.0), (20.0, 1000.0), "Hz", 0),
fs("low_gain", "Low Gain", 0.0, (-24.0, 24.0), (-24.0, 24.0), "dB", 1),
fs("mid_freq", "Mid Frequency", 1000.0, (20.0, 20000.0), (200.0, 8000.0), "Hz", 0),
fs("mid_gain", "Mid Gain", 0.0, (-24.0, 24.0), (-24.0, 24.0), "dB", 1),
fs("mid_q", "Mid Q", 1.0, (0.1, 20.0), (0.1, 10.0), "", 2),
fs("high_freq", "High Frequency", 8000.0, (20.0, 20000.0), (2000.0, 20000.0), "Hz", 0),
fs("high_gain", "High Gain", 0.0, (-24.0, 24.0), (-24.0, 24.0), "dB", 1),
],
),
audio("highpass", "Highpass", A_FILTER, vec![fs("cutoff", "Cutoff", 80.0, (20.0, 20000.0), (20.0, 2000.0), "Hz", 0)]),
audio("lowpass", "Lowpass", A_FILTER, vec![fs("cutoff", "Cutoff", 8000.0, (20.0, 20000.0), (500.0, 20000.0), "Hz", 0)]),
audio(
"bandpass",
"Bandpass",
A_FILTER,
vec![fs("center", "Center", 1000.0, (20.0, 20000.0), (20.0, 20000.0), "Hz", 0), fs("q", "Q", 1.0, (0.1, 20.0), (0.1, 20.0), "", 2)],
),
audio("notch", "Notch Filter", A_FILTER, vec![]),
audio("simple_eq", "Simple Parametric EQ", A_FILTER, vec![]),
audio("graphic_eq", "Graphic Equalizer (10 Bands)", A_FILTER, vec![]),
audio("denoise", "DeNoise", A_NOISE, vec![f("amount", "Amount", 40.0, 0.0, 100.0, "%")]),
audio("dehummer", "DeHummer", A_NOISE, vec![ch("freq", "Frequency", &["50 Hz", "60 Hz"], 1), f("gain", "Gain", -40.0, -80.0, 0.0, "dB")]),
audio("declicker", "Automatic Click Remover", A_NOISE, vec![]),
audio(
"dereverb",
"DeReverb",
A_NOISE,
vec![f("amount", "Amount", 50.0, 0.0, 100.0, "%"), fs("rt60", "Decay Time (RT60)", 0.8, (0.1, 5.0), (0.1, 3.0), "s", 2)],
),
audio(
"deesser",
"DeEsser",
A_AMP,
vec![
fs("frequency", "Frequency", 6000.0, (2000.0, 12000.0), (2000.0, 12000.0), "Hz", 0),
fs("threshold", "Threshold", -12.0, (-40.0, 0.0), (-40.0, 0.0), "dB", 1),
fs("reduction", "Maximum Reduction", 8.0, (0.0, 24.0), (0.0, 24.0), "dB", 1),
],
),
audio("speech_enhance", "Enhance Speech", A_NOISE, vec![f("mix", "Mix", 100.0, 0.0, 100.0, "%"), ch("tone", "Voice", &["Low Tone", "High Tone"], 0)]),
audio("stereo_width", "Stereo Width", A_STEREO, vec![fs("width", "Width", 100.0, (0.0, 200.0), (0.0, 200.0), "%", 0)]),
audio(
"studio_reverb",
"Studio Reverb",
A_REVERB,
vec![
f("room", "Room Size", 50.0, 0.0, 100.0, "%"),
f("decay", "Decay", 50.0, 0.0, 100.0, "%"),
f("damping", "High Frequency Damping", 50.0, 0.0, 100.0, "%"),
f("dry", "Dry", 90.0, 0.0, 100.0, "%"),
f("wet", "Wet", 35.0, 0.0, 100.0, "%"),
],
),
audio("convolution_reverb", "Convolution Reverb", A_REVERB, vec![]),
audio("surround_reverb", "Surround Reverb", A_REVERB, vec![]),
audio("loudness_radar", "Loudness Meter", A_SPECIAL, vec![]),
audio("invert_a", "Invert", A_SPECIAL, vec![]),
audio(
"pitch_shifter",
"Pitch Shifter",
A_TIME,
vec![fs("semitones", "Semi-tones", 0.0, (-12.0, 12.0), (-12.0, 12.0), "", 0), fs("cents", "Cents", 0.0, (-100.0, 100.0), (-100.0, 100.0), "", 0)],
),
// (defined in effect/audio.rs; appended below)
EffectDef {
id: "constant_power",
name: "Constant Power",
@@ -957,6 +857,7 @@ fn build_effects() -> Vec<EffectDef> {
yuv: false,
},
];
v.extend(audio::defs());
v.extend(crate::graphic::layer_defs());
// YUV badge for the colour/intrinsic set
for e in &mut v {
+669
View File
@@ -0,0 +1,669 @@
//! Audio effect definitions: Premiere's Audio Effects bin (Amplitude and Compression, Delay and
//! Echo, Filter and EQ, Modulation, Noise Reduction/Restoration, Reverb, Special, Stereo
//! Imagery, Time and Pitch, and the loose Balance / Mute / Volume items).
//!
//! Parameter ids match the `filmcraft-audio-dsp` effect parameters wherever the mapping is
//! direct (`render::audio_fx` checks this), so agents can drive every DSP parameter by id.
use super::{A_AMP, A_DELAY, A_FILTER, A_NOISE, A_REVERB, A_SPECIAL, A_STEREO, A_TIME, EffectDef, ParamDef, audio, b, ch, f, fs, grp};
const A_MOD: &[&str] = &["Audio Effects", "Modulation"];
/// Loose items at the end of the Audio Effects bin.
const A_ROOT: &[&str] = &["Audio Effects"];
fn db(id: &'static str, label: &'static str, def: f64, min: f64, max: f64) -> ParamDef {
fs(id, label, def, (min, max), (min, max), "dB", 1)
}
fn hz(id: &'static str, label: &'static str, def: f64, min: f64, max: f64) -> ParamDef {
fs(id, label, def, (min, max), (min, max), "Hz", 0)
}
fn ms(id: &'static str, label: &'static str, def: f64, min: f64, max: f64) -> ParamDef {
fs(id, label, def, (min, max), (min, max), "ms", 1)
}
fn pct(id: &'static str, label: &'static str, def: f64, min: f64, max: f64) -> ParamDef {
fs(id, label, def, (min, max), (min, max), "%", 0)
}
fn num(id: &'static str, label: &'static str, def: f64, min: f64, max: f64, unit: &'static str, decimals: u8) -> ParamDef {
fs(id, label, def, (min, max), (min, max), unit, decimals)
}
/// A non-animatable choice/bool already; non-animatable float (filter designs, impulses…).
fn fixed(mut p: ParamDef) -> ParamDef {
p.animatable = false;
p
}
fn group(g: &'static str, ps: Vec<ParamDef>) -> Vec<ParamDef> {
ps.into_iter().map(|p| grp(p, g)).collect()
}
/// Band labels of the 10/20/30-band graphic equalisers (parameters `b1`…`bN`, plus `gain`).
pub const GEQ10_LABELS: [&str; 10] = ["31.5 Hz", "63 Hz", "125 Hz", "250 Hz", "500 Hz", "1 kHz", "2 kHz", "4 kHz", "8 kHz", "16 kHz"];
pub const GEQ20_LABELS: [&str; 20] = [
"31.5 Hz", "44 Hz", "63 Hz", "88 Hz", "125 Hz", "177 Hz", "250 Hz", "355 Hz", "500 Hz", "710 Hz", "1 kHz", "1.4 kHz", "2 kHz", "2.8 kHz", "4 kHz",
"5.6 kHz", "8 kHz", "11.2 kHz", "16 kHz", "22.4 kHz",
];
pub const GEQ30_LABELS: [&str; 30] = [
"25 Hz", "31.5 Hz", "40 Hz", "50 Hz", "63 Hz", "80 Hz", "100 Hz", "125 Hz", "160 Hz", "200 Hz", "250 Hz", "315 Hz", "400 Hz", "500 Hz", "630 Hz", "800 Hz",
"1 kHz", "1.25 kHz", "1.6 kHz", "2 kHz", "2.5 kHz", "3.15 kHz", "4 kHz", "5 kHz", "6.3 kHz", "8 kHz", "10 kHz", "12.5 kHz", "16 kHz", "20 kHz",
];
const BAND_IDS: [&str; 30] = [
"b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "b10", "b11", "b12", "b13", "b14", "b15", "b16", "b17", "b18", "b19", "b20", "b21", "b22", "b23",
"b24", "b25", "b26", "b27", "b28", "b29", "b30",
];
fn geq(labels: &[&'static str]) -> Vec<ParamDef> {
let mut v: Vec<ParamDef> = labels.iter().zip(BAND_IDS).map(|(l, id)| db(id, l, 0.0, -24.0, 24.0)).collect();
v.push(db("gain", "Master Gain", 0.0, -24.0, 24.0));
v
}
const SLOPES: &[&str] = &["12 dB/oct", "24 dB/oct", "36 dB/oct", "48 dB/oct"];
fn peq_band(p: [&'static str; 4], name: &'static str, freq: f64, q: f64) -> Vec<ParamDef> {
group(name, vec![b(p[0], "On", true), hz(p[1], "Frequency", freq, 20.0, 20000.0), db(p[2], "Gain", 0.0, -30.0, 30.0), num(p[3], "Q", q, 0.1, 30.0, "", 2)])
}
fn parametric_eq() -> Vec<ParamDef> {
let mut v = vec![db("master_gain", "Master Gain", 0.0, -30.0, 30.0)];
v.extend(group("High Pass", vec![b("hp_on", "On", false), hz("hp_freq", "Frequency", 30.0, 10.0, 20000.0), ch("hp_slope", "Slope", SLOPES, 0)]));
// Low shelf / band 3 / high shelf keep the ids of the earlier three-band version.
v.extend(peq_band(["low_on", "low_freq", "low_gain", "low_q"], "Low Shelf", 100.0, 0.71));
v.extend(peq_band(["b1_on", "b1_freq", "b1_gain", "b1_q"], "Band 1", 200.0, 1.0));
v.extend(peq_band(["b2_on", "b2_freq", "b2_gain", "b2_q"], "Band 2", 500.0, 1.0));
v.extend(peq_band(["mid_on", "mid_freq", "mid_gain", "mid_q"], "Band 3", 1000.0, 1.0));
v.extend(peq_band(["b4_on", "b4_freq", "b4_gain", "b4_q"], "Band 4", 2000.0, 1.0));
v.extend(peq_band(["b5_on", "b5_freq", "b5_gain", "b5_q"], "Band 5", 5000.0, 1.0));
v.extend(peq_band(["high_on", "high_freq", "high_gain", "high_q"], "High Shelf", 8000.0, 0.71));
v.extend(group("Low Pass", vec![b("lp_on", "On", false), hz("lp_freq", "Frequency", 18000.0, 20.0, 22000.0), ch("lp_slope", "Slope", SLOPES, 0)]));
v
}
fn multiband() -> Vec<ParamDef> {
let mut v =
group("Crossovers", vec![hz("xo1", "Low", 120.0, 20.0, 1000.0), hz("xo2", "Mid", 2000.0, 100.0, 8000.0), hz("xo3", "High", 10000.0, 1000.0, 20000.0)]);
let ids: [[&'static str; 7]; 4] = [
["b1_threshold", "b1_ratio", "b1_attack", "b1_release", "b1_gain", "b1_solo", "b1_bypass"],
["b2_threshold", "b2_ratio", "b2_attack", "b2_release", "b2_gain", "b2_solo", "b2_bypass"],
["b3_threshold", "b3_ratio", "b3_attack", "b3_release", "b3_gain", "b3_solo", "b3_bypass"],
["b4_threshold", "b4_ratio", "b4_attack", "b4_release", "b4_gain", "b4_solo", "b4_bypass"],
];
for (k, i) in ids.iter().enumerate() {
let name = ["Band 1", "Band 2", "Band 3", "Band 4"][k];
v.extend(group(
name,
vec![
db(i[0], "Threshold", -18.0, -60.0, 0.0),
num(i[1], "Ratio", 3.0, 1.0, 30.0, ":1", 1),
ms(i[2], "Attack", 10.0, 0.1, 500.0),
ms(i[3], "Release", 100.0, 1.0, 5000.0),
db(i[4], "Gain", 0.0, -18.0, 18.0),
b(i[5], "Solo", false),
b(i[6], "Bypass", false),
],
));
}
v.extend(group(
"Output",
vec![
db("output", "Output Gain", 0.0, -18.0, 18.0),
b("lim_on", "Limiter", false),
db("lim_threshold", "Limiter Threshold", -0.1, -30.0, 0.0),
ms("lim_release", "Limiter Release", 50.0, 1.0, 1000.0),
b("link", "Link Channels", true),
],
));
v
}
fn dynamics() -> Vec<ParamDef> {
let mut v = group(
"Auto Gate",
vec![
b("gate_on", "Auto Gate", false),
db("gate_threshold", "Threshold", -50.0, -80.0, 0.0),
ms("gate_attack", "Attack", 2.0, 0.1, 100.0),
ms("gate_release", "Release", 100.0, 1.0, 3000.0),
ms("gate_hold", "Hold", 50.0, 0.0, 1000.0),
],
);
v.extend(group(
"Compressor",
vec![
b("comp_on", "Compressor", true),
db("comp_threshold", "Threshold", -20.0, -60.0, 0.0),
num("comp_ratio", "Ratio", 2.0, 1.0, 30.0, ":1", 1),
ms("comp_attack", "Attack", 10.0, 0.1, 300.0),
ms("comp_release", "Release", 100.0, 1.0, 3000.0),
b("comp_auto", "Auto Makeup", false),
db("comp_makeup", "Makeup", 0.0, 0.0, 30.0),
],
));
v.extend(group(
"Expander",
vec![b("exp_on", "Expander", false), db("exp_threshold", "Threshold", -60.0, -80.0, 0.0), num("exp_ratio", "Ratio", 2.0, 1.0, 30.0, ":1", 1)],
));
v.extend(group(
"Limiter",
vec![
b("lim_on", "Limiter", false),
db("lim_threshold", "Threshold", -1.0, -30.0, 0.0),
ms("lim_release", "Release", 50.0, 1.0, 1000.0),
b("soft_clip", "Soft Clip", false),
],
));
v.push(db("output", "Output Gain", 0.0, -30.0, 30.0));
v
}
fn notch_filter() -> Vec<ParamDef> {
let ids: [[&'static str; 3]; 6] = [
["n1_on", "n1_freq", "n1_gain"],
["n2_on", "n2_freq", "n2_gain"],
["n3_on", "n3_freq", "n3_gain"],
["n4_on", "n4_freq", "n4_gain"],
["n5_on", "n5_freq", "n5_gain"],
["n6_on", "n6_freq", "n6_gain"],
];
let mut v = Vec::new();
for (k, i) in ids.iter().enumerate() {
let name = ["Notch 1", "Notch 2", "Notch 3", "Notch 4", "Notch 5", "Notch 6"][k];
v.extend(group(name, vec![b(i[0], "On", false), hz(i[1], "Frequency", 60.0 * (k + 1) as f64, 20.0, 20000.0), db(i[2], "Gain", -30.0, -90.0, 0.0)]));
}
v.push(ch("width", "Notch Width", &["Narrow", "Very Narrow", "Super Narrow"], 0));
v
}
fn fft_filter() -> Vec<ParamDef> {
let ids: [[&'static str; 2]; 8] = [
["p1_freq", "p1_gain"],
["p2_freq", "p2_gain"],
["p3_freq", "p3_gain"],
["p4_freq", "p4_gain"],
["p5_freq", "p5_gain"],
["p6_freq", "p6_gain"],
["p7_freq", "p7_gain"],
["p8_freq", "p8_gain"],
];
let freqs = [40.0, 100.0, 250.0, 600.0, 1500.0, 4000.0, 9000.0, 16000.0];
let mut v = Vec::new();
for (k, i) in ids.iter().enumerate() {
let name = ["Point 1", "Point 2", "Point 3", "Point 4", "Point 5", "Point 6", "Point 7", "Point 8"][k];
v.extend(group(name, vec![hz(i[0], "Frequency", freqs[k], 20.0, 20000.0), db(i[1], "Gain", 0.0, -60.0, 20.0)]));
}
v.push(ch("interp", "Interpolation", &["Linear", "Smooth"], 1));
v
}
/// All audio effect definitions, in Effects-panel order.
pub(super) fn defs() -> Vec<EffectDef> {
vec![
// ---- Amplitude and Compression ----
audio("amplify", "Amplify", A_AMP, vec![fs("gain", "Gain", 0.0, (-96.0, 24.0), (-24.0, 24.0), "dB", 1)]),
audio(
"channel_mixer_a",
"Channel Mixer",
A_AMP,
vec![
pct("l_from_l", "Left - Left", 100.0, -100.0, 100.0),
pct("l_from_r", "Left - Right", 0.0, -100.0, 100.0),
b("invert_l", "Invert Left", false),
pct("r_from_l", "Right - Left", 0.0, -100.0, 100.0),
pct("r_from_r", "Right - Right", 100.0, -100.0, 100.0),
b("invert_r", "Invert Right", false),
],
),
audio(
"channel_volume_a",
"Channel Volume",
A_AMP,
vec![fs("left", "Left", 0.0, (-96.0, 24.0), (-60.0, 6.0), "dB", 1), fs("right", "Right", 0.0, (-96.0, 24.0), (-60.0, 6.0), "dB", 1)],
),
audio(
"deesser",
"DeEsser",
A_AMP,
vec![
hz("frequency", "Frequency", 6000.0, 2000.0, 12000.0),
db("threshold", "Threshold", -12.0, -40.0, 0.0),
db("reduction", "Maximum Reduction", 8.0, 0.0, 24.0),
],
),
audio("dynamics_rack", "Dynamics", A_AMP, dynamics()),
audio(
"dynamics",
"Dynamics Processing",
A_AMP,
vec![
db("threshold", "Threshold", -20.0, -60.0, 0.0),
num("ratio", "Ratio", 4.0, 1.0, 30.0, ":1", 1),
ms("attack", "Attack", 10.0, 0.1, 500.0),
fs("release", "Release", 100.0, (1.0, 5000.0), (1.0, 5000.0), "ms", 0),
],
),
audio(
"hard_limiter",
"Hard Limiter",
A_AMP,
vec![
db("max", "Maximum Amplitude", -0.1, -30.0, 0.0),
db("boost", "Input Boost", 0.0, -30.0, 30.0),
db_ms_lookahead(),
fs("release", "Release Time", 100.0, (1.0, 1000.0), (1.0, 1000.0), "ms", 0),
],
),
audio("multiband_compressor", "Multiband Compressor", A_AMP, multiband()),
audio(
"single_band_compressor",
"Single-band Compressor",
A_AMP,
vec![
db("threshold", "Threshold", -20.0, -60.0, 0.0),
num("ratio", "Ratio", 4.0, 1.0, 30.0, ":1", 1),
ms("attack", "Attack", 10.0, 0.1, 300.0),
ms("release", "Release", 100.0, 1.0, 3000.0),
db("output", "Output Gain", 0.0, -30.0, 30.0),
],
),
audio(
"tube_compressor",
"Tube-modeled Compressor",
A_AMP,
vec![
db("threshold", "Threshold", -20.0, -60.0, 0.0),
num("ratio", "Ratio", 4.0, 1.0, 30.0, ":1", 1),
ms("attack", "Attack", 10.0, 0.1, 500.0),
ms("release", "Release", 100.0, 1.0, 5000.0),
db("output", "Output Gain", 0.0, -30.0, 30.0),
],
),
// ---- Delay and Echo ----
audio(
"analog_delay",
"Analog Delay",
A_DELAY,
vec![
ch("mode", "Mode", &["Tape", "Tape/Tube", "Analog"], 0),
pct("dry", "Dry Out", 100.0, 0.0, 100.0),
pct("wet", "Wet Out", 40.0, 0.0, 100.0),
ms("delay", "Delay", 250.0, 5.0, 8000.0),
pct("feedback", "Feedback", 40.0, 0.0, 200.0),
pct("trash", "Trash", 0.0, 0.0, 200.0),
pct("spread", "Spread", 0.0, 0.0, 200.0),
],
),
audio(
"delay",
"Delay",
A_DELAY,
vec![
fs("delay", "Delay", 1.0, (0.0, 2.0), (0.0, 2.0), "s", 2),
f("feedback", "Feedback", 0.0, 0.0, 100.0, "%"),
f("mix", "Mix", 50.0, 0.0, 100.0, "%"),
],
),
audio(
"multitap_delay",
"Multitap Delay",
A_DELAY,
vec![
ms("delay1", "Delay 1", 250.0, 1.0, 4000.0),
pct("feedback1", "Feedback 1", 0.0, 0.0, 95.0),
db("level1", "Level 1", -6.0, -96.0, 0.0),
ms("delay2", "Delay 2", 500.0, 1.0, 4000.0),
pct("feedback2", "Feedback 2", 0.0, 0.0, 95.0),
db("level2", "Level 2", -9.0, -96.0, 0.0),
ms("delay3", "Delay 3", 750.0, 1.0, 4000.0),
pct("feedback3", "Feedback 3", 0.0, 0.0, 95.0),
db("level3", "Level 3", -12.0, -96.0, 0.0),
ms("delay4", "Delay 4", 1000.0, 1.0, 4000.0),
pct("feedback4", "Feedback 4", 0.0, 0.0, 95.0),
db("level4", "Level 4", -15.0, -96.0, 0.0),
pct("mix", "Mix", 50.0, 0.0, 100.0),
],
),
// ---- Filter and EQ ----
audio("bandpass", "Bandpass", A_FILTER, vec![hz("center", "Center", 1000.0, 20.0, 20000.0), num("q", "Q", 1.0, 0.1, 20.0, "", 2)]),
audio("bass", "Bass", A_FILTER, vec![db("boost", "Boost", 0.0, -24.0, 24.0)]),
audio("fft_filter", "FFT Filter", A_FILTER, fft_filter()),
audio("graphic_eq", "Graphic Equalizer (10 Bands)", A_FILTER, geq(&GEQ10_LABELS)),
audio("graphic_eq_20", "Graphic Equalizer (20 Bands)", A_FILTER, geq(&GEQ20_LABELS)),
audio("graphic_eq_30", "Graphic Equalizer (30 Bands)", A_FILTER, geq(&GEQ30_LABELS)),
audio("highpass", "Highpass", A_FILTER, vec![fs("cutoff", "Cutoff", 80.0, (20.0, 20000.0), (20.0, 2000.0), "Hz", 0)]),
audio("lowpass", "Lowpass", A_FILTER, vec![fs("cutoff", "Cutoff", 8000.0, (20.0, 20000.0), (500.0, 20000.0), "Hz", 0)]),
audio("notch", "Notch Filter", A_FILTER, notch_filter()),
audio("parametric_eq", "Parametric Equalizer", A_FILTER, parametric_eq()),
audio(
"scientific_filter",
"Scientific Filter",
A_FILTER,
vec![
ch("type", "Type", &["Bessel", "Butterworth", "Chebyshev", "Elliptical"], 1),
ch("mode", "Mode", &["Low Pass", "High Pass", "Band Pass", "Band Stop"], 0),
fixed(num("order", "Order", 6.0, 1.0, 12.0, "", 0)),
hz("cutoff", "Cutoff", 1000.0, 20.0, 20000.0),
hz("high_cutoff", "High Cutoff", 4000.0, 20.0, 20000.0),
fixed(num("ripple", "Passband Ripple", 1.0, 0.01, 6.0, "dB", 2)),
fixed(db("stop_atten", "Stopband Attenuation", 60.0, 20.0, 120.0)),
db("gain", "Master Gain", 0.0, -30.0, 30.0),
],
),
audio("simple_notch", "Simple Notch Filter", A_FILTER, vec![hz("center", "Center", 1000.0, 20.0, 20000.0), num("q", "Q", 10.0, 0.1, 100.0, "", 1)]),
audio(
"simple_eq",
"Simple Parametric EQ",
A_FILTER,
vec![hz("center", "Center", 1000.0, 20.0, 20000.0), num("q", "Q", 1.0, 0.1, 20.0, "", 2), db("boost", "Boost", 0.0, -24.0, 24.0)],
),
audio("treble", "Treble", A_FILTER, vec![db("boost", "Boost", 0.0, -24.0, 24.0)]),
// ---- Modulation ----
audio(
"chorus_flanger",
"Chorus/Flanger",
A_MOD,
vec![
ch("mode", "Mode", &["Chorus", "Flanger"], 0),
num("speed", "Speed", 0.8, 0.05, 10.0, "Hz", 2),
pct("width", "Width", 50.0, 0.0, 100.0),
pct("intensity", "Intensity", 30.0, 0.0, 100.0),
pct("transience", "Transience", 0.0, 0.0, 100.0),
pct("mix", "Mix", 50.0, 0.0, 100.0),
],
),
audio(
"flanger",
"Flanger",
A_MOD,
vec![
ms("initial_delay", "Initial Delay", 1.0, 0.1, 20.0),
ms("final_delay", "Final Delay", 5.0, 0.1, 20.0),
num("stereo_phasing", "Stereo Phasing", 90.0, 0.0, 180.0, "°", 0),
pct("feedback", "Feedback", 50.0, -100.0, 100.0),
num("rate", "Modulation Rate", 0.5, 0.01, 10.0, "Hz", 2),
b("inverted", "Inverted", false),
b("special", "Special Effects", false),
b("sinusoidal", "Sinusoidal", true),
pct("mix", "Mix", 50.0, 0.0, 100.0),
],
),
audio(
"phaser",
"Phaser",
A_MOD,
vec![
fixed(num("stages", "Stages", 4.0, 2.0, 12.0, "", 0)),
pct("intensity", "Intensity", 100.0, 0.0, 100.0),
pct("depth", "Depth", 70.0, 0.0, 100.0),
num("rate", "Modulation Rate", 0.5, 0.01, 10.0, "Hz", 2),
num("phase_diff", "Phase Difference", 90.0, 0.0, 180.0, "°", 0),
hz("upper_freq", "Upper Frequency", 2500.0, 100.0, 20000.0),
pct("feedback", "Feedback", 0.0, -100.0, 100.0),
pct("mix", "Mix", 50.0, 0.0, 100.0),
db("output", "Output Gain", 0.0, -24.0, 24.0),
],
),
// ---- Noise Reduction/Restoration ----
audio(
"declicker",
"Automatic Click Remover",
A_NOISE,
vec![num("threshold", "Threshold", 30.0, 1.0, 100.0, "", 0), num("complexity", "Complexity", 16.0, 1.0, 100.0, "", 0)],
),
audio(
"dehummer",
"DeHummer",
A_NOISE,
vec![
ch("freq", "Frequency", &["50 Hz", "60 Hz"], 1),
f("gain", "Gain", -40.0, -80.0, 0.0, "dB"),
fixed(num("harmonics", "Number of Harmonics", 5.0, 1.0, 10.0, "", 0)),
num("q", "Q", 30.0, 1.0, 100.0, "", 0),
],
),
audio("denoise", "DeNoise", A_NOISE, vec![f("amount", "Amount", 40.0, 0.0, 100.0, "%")]),
audio(
"dereverb",
"DeReverb",
A_NOISE,
vec![f("amount", "Amount", 50.0, 0.0, 100.0, "%"), fs("rt60", "Decay Time (RT60)", 0.8, (0.1, 5.0), (0.1, 3.0), "s", 2)],
),
audio("speech_enhance", "Enhance Speech", A_NOISE, vec![f("mix", "Mix", 100.0, 0.0, 100.0, "%"), ch("tone", "Voice", &["Low Tone", "High Tone"], 0)]),
// ---- Reverb ----
audio(
"convolution_reverb",
"Convolution Reverb",
A_REVERB,
vec![
ch("impulse", "Impulse", &["Small Room", "Medium Room", "Large Hall", "Cathedral", "Plate", "Ambience", "Vocal Booth"], 1),
pct("mix", "Mix", 30.0, 0.0, 100.0),
fixed(pct("room_size", "Room Size", 100.0, 10.0, 100.0)),
fixed(hz("damping_lf", "Damping LF", 10.0, 10.0, 1000.0)),
fixed(hz("damping_hf", "Damping HF", 20000.0, 1000.0, 20000.0)),
ms("predelay", "Pre-Delay", 0.0, 0.0, 500.0),
pct("width", "Width", 100.0, 0.0, 100.0),
db("gain", "Gain", 0.0, -24.0, 12.0),
],
),
audio(
"studio_reverb",
"Studio Reverb",
A_REVERB,
vec![
f("room", "Room Size", 50.0, 0.0, 100.0, "%"),
f("decay", "Decay", 50.0, 0.0, 100.0, "%"),
f("damping", "High Frequency Damping", 50.0, 0.0, 100.0, "%"),
f("dry", "Dry", 90.0, 0.0, 100.0, "%"),
f("wet", "Wet", 35.0, 0.0, 100.0, "%"),
],
),
audio(
"surround_reverb",
"Surround Reverb",
A_REVERB,
vec![
pct("center_input", "Center Input", 100.0, 0.0, 100.0),
pct("room_size", "Room Size", 50.0, 0.0, 100.0),
num("decay", "Decay", 1.5, 0.1, 20.0, "s", 2),
ms("predelay", "Pre-Delay", 20.0, 0.0, 200.0),
pct("damping", "Damping", 40.0, 0.0, 100.0),
pct("early", "Early Reflections", 40.0, 0.0, 100.0),
hz("low_cut", "Low Frequency Cut", 20.0, 20.0, 1000.0),
hz("high_cut", "High Frequency Cut", 12000.0, 1000.0, 20000.0),
pct("width", "Width", 100.0, 0.0, 100.0),
pct("dry", "Dry", 80.0, 0.0, 100.0),
pct("wet", "Wet", 30.0, 0.0, 100.0),
],
),
// ---- Special ----
audio(
"binauralizer",
"Binauralizer - Ambisonics",
A_SPECIAL,
vec![
num("angle", "Speaker Angle", 30.0, 0.0, 90.0, "°", 0),
num("head_size", "Head Size", 17.5, 12.0, 24.0, "cm", 1),
pct("mix", "Mix", 100.0, 0.0, 100.0),
],
),
audio(
"distortion",
"Distortion",
A_SPECIAL,
vec![
db("drive", "Drive", 12.0, 0.0, 48.0),
ch("curve", "Curve", &["Soft Clip", "Hard Clip", "Tube", "Foldback"], 0),
pct("symmetry", "Asymmetry", 0.0, -100.0, 100.0),
hz("tone", "Tone", 20000.0, 500.0, 20000.0),
db("output", "Output Gain", 0.0, -48.0, 12.0),
pct("mix", "Mix", 100.0, 0.0, 100.0),
],
),
audio("fill_left", "Fill Left with Right", A_SPECIAL, vec![]),
audio("fill_right", "Fill Right with Left", A_SPECIAL, vec![]),
audio(
"guitar_suite",
"GuitarSuite",
A_SPECIAL,
vec![
pct("compressor", "Compressor", 30.0, 0.0, 100.0),
pct("distortion", "Distortion", 40.0, 0.0, 100.0),
ch("dist_type", "Distortion Type", &["Soft", "Hard", "Fuzz"], 0),
ch("amp", "Amplifier", &["None", "Clean Combo", "British Stack", "Tweed", "Modern High Gain"], 1),
ch("filter", "Filter", &["None", "Low Pass", "High Pass", "Band Pass"], 0),
hz("filter_freq", "Filter Frequency", 2000.0, 100.0, 10000.0),
pct("filter_res", "Filter Resonance", 20.0, 0.0, 100.0),
pct("mix", "Mix", 100.0, 0.0, 100.0),
db("output", "Output Gain", 0.0, -24.0, 12.0),
],
),
audio("invert_a", "Invert", A_SPECIAL, vec![]),
audio("loudness_radar", "Loudness Meter", A_SPECIAL, vec![num("target", "Target Loudness", -23.0, -36.0, -5.0, "LUFS", 1)]),
audio(
"mastering",
"Mastering",
A_SPECIAL,
vec![
db("eq_low", "Low Shelf Gain", 0.0, -12.0, 12.0),
hz("eq_mid_freq", "Peak Frequency", 1000.0, 100.0, 10000.0),
db("eq_mid", "Peak Gain", 0.0, -12.0, 12.0),
db("eq_high", "High Shelf Gain", 0.0, -12.0, 12.0),
pct("reverb", "Reverb Amount", 0.0, 0.0, 100.0),
pct("exciter", "Exciter Amount", 0.0, 0.0, 100.0),
ch("exciter_mode", "Exciter Mode", &["Retro", "Tape", "Tube"], 1),
pct("widener", "Widener", 100.0, 0.0, 200.0),
pct("loudness", "Loudness Maximizer", 0.0, 0.0, 100.0),
db("output", "Output Gain", 0.0, -24.0, 12.0),
],
),
audio(
"panner_ambisonics",
"Panner - Ambisonics",
A_SPECIAL,
vec![num("pan", "Pan", 0.0, -180.0, 180.0, "°", 1), num("tilt", "Tilt", 0.0, -90.0, 90.0, "°", 1), num("roll", "Roll", 0.0, -180.0, 180.0, "°", 1)],
),
audio("swap_channels", "Swap Channels", A_SPECIAL, vec![]),
audio("vocal_enhancer", "Vocal Enhancer", A_SPECIAL, vec![ch("mode", "Mode", &["Male", "Female", "Music"], 0)]),
// ---- Stereo Imagery ----
audio(
"stereo_expander",
"Stereo Expander",
A_STEREO,
vec![num("center_pan", "Center Channel Pan", 0.0, -100.0, 100.0, "", 0), pct("expand", "Stereo Expand", 100.0, 0.0, 300.0)],
),
audio("stereo_width", "Stereo Width", A_STEREO, vec![fs("width", "Width", 100.0, (0.0, 200.0), (0.0, 200.0), "%", 0)]),
// ---- Time and Pitch ----
audio(
"pitch_shifter",
"Pitch Shifter",
A_TIME,
vec![fs("semitones", "Semi-tones", 0.0, (-12.0, 12.0), (-12.0, 12.0), "", 0), fs("cents", "Cents", 0.0, (-100.0, 100.0), (-100.0, 100.0), "", 0)],
),
// ---- loose items ----
audio("balance_a", "Balance", A_ROOT, vec![num("balance", "Balance", 0.0, -100.0, 100.0, "", 1)]),
audio("mute", "Mute", A_ROOT, vec![mute_param()]),
audio("volume_a", "Volume", A_ROOT, vec![b("bypass", "Bypass", false), fs("level", "Level", 0.0, (-96.0, 15.0), (-60.0, 15.0), "dB", 1)]),
]
}
fn db_ms_lookahead() -> ParamDef {
fs("lookahead", "Look-Ahead Time", 7.0, (0.0, 30.0), (0.0, 30.0), "ms", 1)
}
/// Mute is keyframed to silence passages, so it is animatable (unlike most toggles).
fn mute_param() -> ParamDef {
let mut p = b("mute", "Mute", false);
p.animatable = true;
p
}
/// Ids of the effects Premiere lists in its Audio Effects bin (excluding Audio Units).
pub const PREMIERE_AUDIO_EFFECTS: [&str; 53] = [
"amplify",
"channel_mixer_a",
"channel_volume_a",
"deesser",
"dynamics_rack",
"dynamics",
"hard_limiter",
"multiband_compressor",
"single_band_compressor",
"tube_compressor",
"analog_delay",
"delay",
"multitap_delay",
"bandpass",
"bass",
"fft_filter",
"graphic_eq",
"graphic_eq_20",
"graphic_eq_30",
"highpass",
"lowpass",
"notch",
"parametric_eq",
"scientific_filter",
"simple_notch",
"simple_eq",
"treble",
"chorus_flanger",
"flanger",
"phaser",
"declicker",
"dehummer",
"denoise",
"dereverb",
"convolution_reverb",
"studio_reverb",
"surround_reverb",
"binauralizer",
"distortion",
"fill_left",
"fill_right",
"guitar_suite",
"invert_a",
"loudness_radar",
"mastering",
"panner_ambisonics",
"swap_channels",
"vocal_enhancer",
"stereo_expander",
"pitch_shifter",
"balance_a",
"mute",
"volume_a",
];
#[cfg(test)]
mod tests {
use super::*;
use crate::effect::{EffectKind, find_effect};
#[test]
fn premiere_audio_set_is_complete_and_foldered() {
let mut seen = std::collections::HashSet::new();
for id in PREMIERE_AUDIO_EFFECTS {
assert!(seen.insert(id), "duplicate {id}");
let d = find_effect(id).unwrap_or_else(|| panic!("missing {id}"));
assert_eq!(d.kind, EffectKind::Audio, "{id}");
assert!(!d.intrinsic, "{id}");
assert_eq!(d.category.first(), Some(&"Audio Effects"), "{id}");
}
let folder = |id: &str| find_effect(id).unwrap().category.get(1).copied();
assert_eq!(folder("multiband_compressor"), Some("Amplitude and Compression"));
assert_eq!(folder("multitap_delay"), Some("Delay and Echo"));
assert_eq!(folder("graphic_eq_30"), Some("Filter and EQ"));
assert_eq!(folder("phaser"), Some("Modulation"));
assert_eq!(folder("declicker"), Some("Noise Reduction/Restoration"));
assert_eq!(folder("surround_reverb"), Some("Reverb"));
assert_eq!(folder("guitar_suite"), Some("Special"));
assert_eq!(folder("stereo_expander"), Some("Stereo Imagery"));
assert_eq!(folder("pitch_shifter"), Some("Time and Pitch"));
assert_eq!(folder("mute"), None, "loose item");
}
#[test]
fn parametric_eq_keeps_the_three_band_ids() {
let d = find_effect("parametric_eq").unwrap();
for id in ["low_freq", "low_gain", "mid_freq", "mid_gain", "mid_q", "high_freq", "high_gain"] {
assert!(d.param(id).is_some(), "{id}");
}
}
}
+218 -1
View File
@@ -92,6 +92,8 @@ pub(crate) fn mapping(id: &str) -> Option<Mapping> {
apply: |d, e, t| {
d.set_param("frequency", e.param("freq").and_then(|p| p.value_at(t).as_f64()).unwrap_or(1.0) as f32);
d.set_param("amount", (1.0 - 10f32.powf(f(e, "gain", t).min(0.0) / 20.0)) * 100.0);
set_if_changed(d, "harmonics", f(e, "harmonics", t));
set_if_changed(d, "q", f(e, "q", t));
},
},
"deesser" => Mapping {
@@ -137,10 +139,161 @@ pub(crate) fn mapping(id: &str) -> Option<Mapping> {
preroll: |_, _| 0.1,
apply: |d, e, t| ignore(d.set_param("semitones", (f(e, "semitones", t) + f(e, "cents", t) / 100.0).clamp(-12.0, 12.0))),
},
_ => return None,
"single_band_compressor" => Mapping {
dsp: "compressor",
preroll: |e, t| (f(e, "release", t) as f64 / 1000.0 * 5.0).clamp(0.05, 3.0),
apply: |d, e, t| {
set_if_changed(d, "knee", 0.0);
for id in ["threshold", "ratio", "attack", "release"] {
set_if_changed(d, id, f(e, id, t));
}
set_if_changed(d, "makeup", f(e, "output", t));
},
},
"bass" => Mapping { dsp: "parametric_eq", preroll: |_, _| 0.05, apply: |d, e, t| shelf(d, 1.0, 200.0, f(e, "boost", t)) },
"treble" => Mapping { dsp: "parametric_eq", preroll: |_, _| 0.05, apply: |d, e, t| shelf(d, 2.0, 4000.0, f(e, "boost", t)) },
"simple_notch" => Mapping { dsp: "parametric_eq", preroll: |_, _| 0.1, apply: |d, e, t| band(d, 5.0, f(e, "center", t), f(e, "q", t)) },
"simple_eq" => Mapping {
dsp: "parametric_eq",
preroll: |_, _| 0.05,
apply: |d, e, t| {
band(d, 0.0, f(e, "center", t), f(e, "q", t));
set_if_changed(d, "b1.gain", f(e, "boost", t));
},
},
"volume_a" => Mapping {
dsp: "amplify",
preroll: |_, _| 0.0,
apply: |d, e, t| {
let bypass = e.f64_at("bypass", t) >= 0.5;
set_if_changed(d, "gain", if bypass { 0.0 } else { f(e, "level", t).clamp(-96.0, 24.0) });
},
},
"analog_delay" => Mapping {
dsp: "analog_delay",
preroll: |e, t| {
let time = f(e, "delay", t).max(1.0) as f64 / 1000.0;
let fb = (f(e, "feedback", t) as f64 / 100.0).clamp(0.0, 0.95);
let repeats = if fb > 0.001 { (-4.0 / fb.log10()).clamp(1.0, 60.0) } else { 1.0 };
(time * repeats).clamp(0.12, 8.0)
},
apply: direct,
},
"multitap_delay" => Mapping {
dsp: "multitap_delay",
preroll: |e, t| {
let mut worst = 0.0f64;
for k in 1..=4 {
let time = e.f64_at(["delay1", "delay2", "delay3", "delay4"][k - 1], t).max(1.0) / 1000.0;
let fb = (e.f64_at(["feedback1", "feedback2", "feedback3", "feedback4"][k - 1], t) / 100.0).clamp(0.0, 0.95);
let repeats = if fb > 0.001 { (-4.0 / fb.log10()).clamp(1.0, 60.0) } else { 1.0 };
worst = worst.max(time * repeats);
}
worst.clamp(0.12, 8.0)
},
apply: direct,
},
"convolution_reverb" => Mapping {
dsp: "convolution_reverb",
preroll: |e, t| {
let rt = [0.45, 0.8, 1.9, 3.2, 1.6, 0.3, 0.2][(e.f64_at("impulse", t) as usize).min(6)];
(rt * 1.1 * e.f64_at("room_size", t) / 100.0 + e.f64_at("predelay", t) / 1000.0).clamp(0.12, 4.0)
},
apply: direct,
},
"surround_reverb" => {
Mapping { dsp: "surround_reverb", preroll: |e, t| (e.f64_at("decay", t) + e.f64_at("predelay", t) / 1000.0).clamp(0.12, 8.0), apply: direct }
}
_ => {
let &(_, dsp, _) = DIRECT.iter().find(|(p, _, _)| *p == id)?;
Mapping { dsp, preroll: |e, _| direct_preroll(&e.effect), apply: direct }
}
})
}
/// Project effects whose parameters map one-to-one (same ids, same units) onto a DSP effect:
/// (project id, DSP id, pre-roll seconds for a fresh chain).
pub const DIRECT: &[(&str, &str, f64)] = &[
("channel_mixer_a", "channel_mixer", 0.0),
("channel_volume_a", "channel_volume", 0.0),
("dynamics_rack", "dynamics_rack", 1.0),
("multiband_compressor", "multiband_compressor", 1.0),
("tube_compressor", "tube_compressor", 1.5),
("fft_filter", "fft_filter", 0.1),
("graphic_eq", "graphic_eq_10", 0.1),
("graphic_eq_20", "graphic_eq_20", 0.1),
("graphic_eq_30", "graphic_eq_30", 0.1),
("notch", "notch_filter", 0.1),
("parametric_eq", "parametric_eq_full", 0.1),
("scientific_filter", "scientific_filter", 0.2),
("chorus_flanger", "chorus_flanger", 0.1),
("flanger", "flanger", 0.2),
("phaser", "phaser", 0.1),
("declicker", "click_remover", 0.05),
("binauralizer", "binauralizer", 0.01),
("distortion", "distortion", 0.05),
("fill_left", "fill_left", 0.0),
("fill_right", "fill_right", 0.0),
("swap_channels", "swap_channels", 0.0),
("guitar_suite", "guitar_suite", 0.2),
("loudness_radar", "loudness_meter", 0.0),
("mastering", "mastering", 2.0),
("panner_ambisonics", "ambisonics_panner", 0.0),
("vocal_enhancer", "vocal_enhancer", 0.3),
("stereo_expander", "stereo_expander", 0.0),
("balance_a", "balance", 0.0),
("mute", "mute", 0.0),
("analog_delay", "analog_delay", 0.0),
("multitap_delay", "multitap_delay", 0.0),
("convolution_reverb", "convolution_reverb", 0.0),
("surround_reverb", "surround_reverb", 0.0),
];
/// Pre-roll of a direct mapping: its own tail, and at least long enough for the DSP's
/// parameter smoothing (≤ 100 ms) to settle from the defaults to the clip's settings.
fn direct_preroll(id: &str) -> f64 {
DIRECT.iter().find(|(p, _, _)| *p == id).map_or(0.0, |d| d.2).max(0.12)
}
/// Set a DSP parameter only when it changed (keeps parameter-dependent rebuilds — impulse
/// responses, filter designs — off the per-block path).
fn set_if_changed(d: &mut dyn AudioEffect, id: &str, v: f32) {
if d.param(id) != Some(v) {
d.set_param(id, v);
}
}
/// Copy every scalar project parameter (Float / Choice / Bool) to the DSP parameter of the
/// same id.
fn direct(d: &mut dyn AudioEffect, e: &EffectInstance, t: Tick) {
let Some(def) = e.def() else { return };
for p in &def.params {
let v = e.params.get(p.id).map_or_else(|| p.default.as_f64().unwrap_or(0.0), |q| q.scalar_at(t));
// sanitise like the DSP would, so unchanged values compare equal
let v = d.params().iter().find(|s| s.id == p.id).map_or(v as f32, |s| s.sanitize(v as f32));
set_if_changed(d, p.id, v);
}
}
/// A DSP instance configured like the project effect `e` at media time `t` (for the graphical
/// effect editors: [`AudioEffect::response_db`] / [`AudioEffect::transfer_db`]).
pub fn configured(e: &EffectInstance, t: Tick, sample_rate: u32) -> Option<Box<dyn AudioEffect>> {
let m = mapping(&e.effect)?;
let mut d = filmcraft_audio_dsp::create_effect(m.dsp, sample_rate as f32, 2)?;
(m.apply)(d.as_mut(), e, t);
Some(d)
}
/// Processing latency (samples) of a project audio effect at `sample_rate`.
pub fn latency(effect_id: &str, sample_rate: u32) -> usize {
mapping(effect_id).and_then(|m| filmcraft_audio_dsp::create_effect(m.dsp, sample_rate as f32, 2)).map_or(0, |d| d.latency())
}
fn shelf(d: &mut dyn AudioEffect, kind: f32, freq: f32, gain: f32) {
band(d, kind, freq, std::f32::consts::FRAC_1_SQRT_2);
set_if_changed(d, "b1.gain", gain);
}
/// Whether a project audio effect has a DSP implementation (clip effects and mixer inserts).
pub fn supported(effect_id: &str) -> bool {
mapping(effect_id).is_some()
@@ -257,3 +410,67 @@ pub fn process(item: &TrackItem, a0: i64, n: usize, sr: u32, read: &dyn Fn(i64,
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use filmcraft_audio_dsp::Unit;
use filmcraft_project::{EffectKind, ParamKind, effect_defs};
#[test]
fn every_audio_effect_has_dsp() {
for d in effect_defs().iter().filter(|d| d.kind == EffectKind::Audio && !d.intrinsic) {
let m = mapping(d.id).unwrap_or_else(|| panic!("{} has no DSP mapping", d.id));
assert!(filmcraft_audio_dsp::effect_info(m.dsp).is_some(), "{} → unknown DSP {}", d.id, m.dsp);
let inst = d.instance();
let mut fx = configured(&inst, Tick::ZERO, 48000).unwrap();
let mut l: Vec<f32> = (0..4800).map(|i| ((i * 7919 % 1000) as f32 / 1000.0 - 0.5) * 0.5).collect();
let mut r = l.clone();
fx.process(&mut [&mut l, &mut r]);
assert!(l.iter().chain(&r).all(|v| v.is_finite()), "{}", d.id);
}
assert_eq!(filmcraft_project::effect::PREMIERE_AUDIO_EFFECTS.len(), 53);
}
#[test]
fn direct_mappings_match_dsp_parameters() {
for &(pid, dsp, _) in DIRECT {
let def = filmcraft_project::find_effect(pid).unwrap_or_else(|| panic!("{pid}"));
let info = filmcraft_audio_dsp::effect_info(dsp).unwrap_or_else(|| panic!("{dsp}"));
for p in &def.params {
let s = info.params.iter().find(|s| s.id == p.id).unwrap_or_else(|| panic!("{pid}.{} missing in {dsp}", p.id));
match &p.kind {
ParamKind::Float { min, max, .. } => {
assert!(*min >= s.min as f64 - 1e-3 && *max <= s.max as f64 + 1e-3, "{pid}.{}: range {min}..{max} vs {}..{}", p.id, s.min, s.max);
let dv = p.default.as_f64().unwrap();
assert!((dv - s.default as f64).abs() < 0.02, "{pid}.{}: default {dv} vs {}", p.id, s.default);
}
ParamKind::Choice(opts) => {
assert_eq!(s.unit, Unit::Choice, "{pid}.{}", p.id);
assert_eq!(opts.len(), s.choices.len(), "{pid}.{}", p.id);
assert_eq!(p.default.as_f64().unwrap(), s.default as f64, "{pid}.{}", p.id);
}
ParamKind::Bool => {
assert_eq!(s.unit, Unit::Toggle, "{pid}.{}", p.id);
assert_eq!(p.default.as_f64().unwrap(), s.default as f64, "{pid}.{}", p.id);
}
k => panic!("{pid}.{}: unsupported kind {k:?}", p.id),
}
}
}
}
#[test]
fn editor_curves_follow_project_parameters() {
let def = filmcraft_project::find_effect("graphic_eq").unwrap();
let mut inst = def.instance();
inst.param_mut("b6").unwrap().value = filmcraft_project::ParamValue::Float(9.0);
let fx = configured(&inst, Tick::ZERO, 48000).unwrap();
assert!((fx.response_db(1000.0).unwrap() - 9.0).abs() < 1.0);
let def = filmcraft_project::find_effect("dynamics_rack").unwrap();
let fx = configured(&def.instance(), Tick::ZERO, 48000).unwrap();
assert!((fx.transfer_db(0, 0.0).unwrap() + 10.0).abs() < 1e-3, "−20 dB threshold, 2:1");
assert_eq!(latency("fft_filter", 48000), 2048);
assert_eq!(latency("amplify", 48000), 0);
}
}