mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-24 10:10:05 -06:00
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:
co-authored by
Claude Opus 5.5
parent
9ef525441a
commit
4c0f01166f
@@ -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"));
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user