Skip to main content

renderbox_sdk/ops/
audio.rs

1use std::collections::BTreeMap;
2
3use crate::ops::{add_audio_filter, params, OptParam};
4use crate::sorts::Audio;
5use crate::graph::Stream;
6
7// ---------------------------------------------------------------------------
8// Level / normalization
9// ---------------------------------------------------------------------------
10
11pub fn volume(level: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
12    move |input| add_audio_filter(input, "volume", params! { "volume" => level })
13}
14
15#[derive(Default)]
16pub struct Loudnorm {
17    pub target: Option<f64>,
18}
19
20pub fn loudnorm(opts: Loudnorm) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
21    move |input| {
22        let mut p = BTreeMap::new();
23        opts.target.insert_into("target", &mut p);
24        add_audio_filter(input, "loudnorm", p)
25    }
26}
27
28// ---------------------------------------------------------------------------
29// EQ / frequency
30// ---------------------------------------------------------------------------
31
32pub fn lowpass(freq: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
33    move |input| add_audio_filter(input, "lowpass", params! { "f" => freq })
34}
35
36pub fn highpass(freq: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
37    move |input| add_audio_filter(input, "highpass", params! { "f" => freq })
38}
39
40pub fn equalizer(
41    freq: f64,
42    width_type: &str,
43    width: f64,
44    gain: f64,
45) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
46    let width_type = width_type.to_string();
47    move |input| {
48        add_audio_filter(
49            input,
50            "equalizer",
51            params! { "f" => freq, "width_type" => width_type.as_str(), "w" => width, "g" => gain },
52        )
53    }
54}
55
56pub fn bass(gain: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
57    move |input| add_audio_filter(input, "bass", params! { "g" => gain })
58}
59
60pub fn treble(gain: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
61    move |input| add_audio_filter(input, "treble", params! { "g" => gain })
62}
63
64// ---------------------------------------------------------------------------
65// Effects
66// ---------------------------------------------------------------------------
67
68#[derive(Default)]
69pub struct Aecho {
70    pub in_gain: Option<f64>,
71    pub out_gain: Option<f64>,
72    pub delays: Option<String>,
73    pub decays: Option<String>,
74}
75
76pub fn aecho(opts: Aecho) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
77    move |input| {
78        let mut p = BTreeMap::new();
79        opts.in_gain.insert_into("in_gain", &mut p);
80        opts.out_gain.insert_into("out_gain", &mut p);
81        opts.delays.as_deref().insert_into("delays", &mut p);
82        opts.decays.as_deref().insert_into("decays", &mut p);
83        add_audio_filter(input, "aecho", p)
84    }
85}
86
87pub fn atempo(tempo: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
88    move |input| add_audio_filter(input, "atempo", params! { "tempo" => tempo })
89}
90
91pub fn aresample(sample_rate: u32) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
92    move |input| add_audio_filter(input, "aresample", params! { "sample_rate" => sample_rate })
93}
94
95pub fn adelay(delays: &str) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
96    let delays = delays.to_string();
97    move |input| add_audio_filter(input, "adelay", params! { "delays" => delays.as_str() })
98}
99
100pub fn afade_in(duration: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
101    move |input| {
102        add_audio_filter(input, "afade", params! { "t" => "in", "d" => duration })
103    }
104}
105
106pub fn afade_out(start: f64, duration: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
107    move |input| {
108        add_audio_filter(
109            input,
110            "afade",
111            params! { "t" => "out", "st" => start, "d" => duration },
112        )
113    }
114}
115
116pub fn atrim(start: f64, end: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
117    move |input| add_audio_filter(input, "atrim", params! { "start" => start, "end" => end })
118}
119
120pub fn asetpts(expr: &str) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
121    let expr = expr.to_string();
122    move |input| add_audio_filter(input, "asetpts", params! { "expr" => expr.as_str() })
123}
124
125pub fn areverse() -> impl Fn(Stream<Audio>) -> Stream<Audio> {
126    move |input| add_audio_filter(input, "areverse", params! {})
127}
128
129pub fn aloop(count: i32, size: u32, start: u32) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
130    move |input| {
131        add_audio_filter(
132            input,
133            "aloop",
134            params! { "loop" => count, "size" => size, "start" => start },
135        )
136    }
137}
138
139pub fn silenceremove() -> impl Fn(Stream<Audio>) -> Stream<Audio> {
140    move |input| add_audio_filter(input, "silenceremove", params! {})
141}
142
143pub fn compand(
144    attacks: &str,
145    decays: &str,
146    points: &str,
147) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
148    let attacks = attacks.to_string();
149    let decays = decays.to_string();
150    let points = points.to_string();
151    move |input| {
152        add_audio_filter(
153            input,
154            "compand",
155            params! { "attacks" => attacks.as_str(), "decays" => decays.as_str(), "points" => points.as_str() },
156        )
157    }
158}
159
160#[derive(Default)]
161pub struct Dynaudnorm {
162    pub peak: Option<f64>,
163    pub maxgain: Option<f64>,
164    pub framelen: Option<f64>,
165    pub gausssize: Option<f64>,
166    pub targetrms: Option<f64>,
167    pub compress: Option<f64>,
168    pub threshold: Option<f64>,
169}
170
171pub fn dynaudnorm(opts: Dynaudnorm) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
172    move |input| {
173        let mut p = BTreeMap::new();
174        opts.peak.insert_into("peak", &mut p);
175        opts.maxgain.insert_into("maxgain", &mut p);
176        opts.framelen.insert_into("framelen", &mut p);
177        opts.gausssize.insert_into("gausssize", &mut p);
178        opts.targetrms.insert_into("targetrms", &mut p);
179        opts.compress.insert_into("compress", &mut p);
180        opts.threshold.insert_into("threshold", &mut p);
181        add_audio_filter(input, "dynaudnorm", p)
182    }
183}
184
185// ---------------------------------------------------------------------------
186// Dynamics / gating / limiting
187// ---------------------------------------------------------------------------
188
189#[derive(Default)]
190pub struct Acompressor {
191    pub threshold: Option<f64>,
192    pub ratio: Option<f64>,
193}
194
195pub fn acompressor(opts: Acompressor) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
196    move |input| {
197        let mut p = BTreeMap::new();
198        opts.threshold.insert_into("threshold", &mut p);
199        opts.ratio.insert_into("ratio", &mut p);
200        add_audio_filter(input, "acompressor", p)
201    }
202}
203
204#[derive(Default)]
205pub struct Agate {
206    pub threshold: Option<f64>,
207    pub ratio: Option<f64>,
208    pub attack: Option<f64>,
209    pub release: Option<f64>,
210    pub range: Option<f64>,
211}
212
213pub fn agate(opts: Agate) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
214    move |input| {
215        let mut p = BTreeMap::new();
216        opts.threshold.insert_into("threshold", &mut p);
217        opts.ratio.insert_into("ratio", &mut p);
218        opts.attack.insert_into("attack", &mut p);
219        opts.release.insert_into("release", &mut p);
220        opts.range.insert_into("range", &mut p);
221        add_audio_filter(input, "agate", p)
222    }
223}
224
225#[derive(Default)]
226pub struct Alimiter {
227    pub limit: Option<f64>,
228    pub attack: Option<f64>,
229    pub release: Option<f64>,
230    pub level_in: Option<f64>,
231    pub level_out: Option<f64>,
232    pub asc: Option<bool>,
233}
234
235pub fn alimiter(opts: Alimiter) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
236    move |input| {
237        let mut p = BTreeMap::new();
238        opts.limit.insert_into("limit", &mut p);
239        opts.attack.insert_into("attack", &mut p);
240        opts.release.insert_into("release", &mut p);
241        opts.level_in.insert_into("level_in", &mut p);
242        opts.level_out.insert_into("level_out", &mut p);
243        opts.asc.insert_into("asc", &mut p);
244        add_audio_filter(input, "alimiter", p)
245    }
246}
247
248// ---------------------------------------------------------------------------
249// Noise reduction / speech
250// ---------------------------------------------------------------------------
251
252#[derive(Default)]
253pub struct Afftdn {
254    pub nr: Option<f64>,
255    pub nt: Option<String>,
256    pub bn: Option<String>,
257}
258
259pub fn afftdn(opts: Afftdn) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
260    move |input| {
261        let mut p = BTreeMap::new();
262        opts.nr.insert_into("nr", &mut p);
263        opts.nt.as_deref().insert_into("nt", &mut p);
264        opts.bn.as_deref().insert_into("bn", &mut p);
265        add_audio_filter(input, "afftdn", p)
266    }
267}
268
269#[derive(Default)]
270pub struct Deesser {
271    pub i: Option<f64>,
272    pub m: Option<f64>,
273    pub f: Option<f64>,
274}
275
276pub fn deesser(opts: Deesser) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
277    move |input| {
278        let mut p = BTreeMap::new();
279        opts.i.insert_into("i", &mut p);
280        opts.m.insert_into("m", &mut p);
281        opts.f.insert_into("f", &mut p);
282        add_audio_filter(input, "deesser", p)
283    }
284}
285
286#[derive(Default)]
287pub struct Dialoguenhance {
288    pub original: Option<f64>,
289    pub enhance: Option<f64>,
290    pub voice: Option<f64>,
291}
292
293pub fn dialoguenhance(opts: Dialoguenhance) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
294    move |input| {
295        let mut p = BTreeMap::new();
296        opts.original.insert_into("original", &mut p);
297        opts.enhance.insert_into("enhance", &mut p);
298        opts.voice.insert_into("voice", &mut p);
299        add_audio_filter(input, "dialoguenhance", p)
300    }
301}
302
303#[derive(Default)]
304pub struct Speechnorm {
305    pub peak: Option<f64>,
306    pub expansion: Option<f64>,
307    pub compression: Option<f64>,
308    pub threshold: Option<f64>,
309}
310
311pub fn speechnorm(opts: Speechnorm) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
312    move |input| {
313        let mut p = BTreeMap::new();
314        opts.peak.insert_into("peak", &mut p);
315        opts.expansion.insert_into("expansion", &mut p);
316        opts.compression.insert_into("compression", &mut p);
317        opts.threshold.insert_into("threshold", &mut p);
318        add_audio_filter(input, "speechnorm", p)
319    }
320}
321
322pub fn acontrast(contrast: f64) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
323    move |input| add_audio_filter(input, "acontrast", params! { "contrast" => contrast })
324}
325
326// ---------------------------------------------------------------------------
327// Spatial / stereo
328// ---------------------------------------------------------------------------
329
330#[derive(Default)]
331pub struct Aphaser {
332    pub in_gain: Option<f64>,
333    pub out_gain: Option<f64>,
334    pub delay: Option<f64>,
335    pub decay: Option<f64>,
336    pub speed: Option<f64>,
337}
338
339pub fn aphaser(opts: Aphaser) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
340    move |input| {
341        let mut p = BTreeMap::new();
342        opts.in_gain.insert_into("in_gain", &mut p);
343        opts.out_gain.insert_into("out_gain", &mut p);
344        opts.delay.insert_into("delay", &mut p);
345        opts.decay.insert_into("decay", &mut p);
346        opts.speed.insert_into("speed", &mut p);
347        add_audio_filter(input, "aphaser", p)
348    }
349}
350
351#[derive(Default)]
352pub struct Crossfeed {
353    pub strength: Option<f64>,
354    pub range: Option<f64>,
355    pub slope: Option<f64>,
356    pub level_in: Option<f64>,
357    pub level_out: Option<f64>,
358}
359
360pub fn crossfeed(opts: Crossfeed) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
361    move |input| {
362        let mut p = BTreeMap::new();
363        opts.strength.insert_into("strength", &mut p);
364        opts.range.insert_into("range", &mut p);
365        opts.slope.insert_into("slope", &mut p);
366        opts.level_in.insert_into("level_in", &mut p);
367        opts.level_out.insert_into("level_out", &mut p);
368        add_audio_filter(input, "crossfeed", p)
369    }
370}
371
372#[derive(Default)]
373pub struct Asubboost {
374    pub dry: Option<f64>,
375    pub wet: Option<f64>,
376    pub decay: Option<f64>,
377    pub feedback: Option<f64>,
378    pub cutoff: Option<f64>,
379    pub slope: Option<f64>,
380    pub delay: Option<f64>,
381}
382
383pub fn asubboost(opts: Asubboost) -> impl Fn(Stream<Audio>) -> Stream<Audio> {
384    move |input| {
385        let mut p = BTreeMap::new();
386        opts.dry.insert_into("dry", &mut p);
387        opts.wet.insert_into("wet", &mut p);
388        opts.decay.insert_into("decay", &mut p);
389        opts.feedback.insert_into("feedback", &mut p);
390        opts.cutoff.insert_into("cutoff", &mut p);
391        opts.slope.insert_into("slope", &mut p);
392        opts.delay.insert_into("delay", &mut p);
393        add_audio_filter(input, "asubboost", p)
394    }
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400    use renderbox_dsl::ParamValue;
401    use crate::graph::input;
402
403    #[test]
404    fn volume_produces_correct_opnode() {
405        let (_, a) = input("test.mp4");
406        let a = a.pipe(volume(0.5));
407        let graph = a.graph.borrow();
408        let node = graph.arena.get(a.node_id);
409        match node {
410            renderbox_dsl::OpNode::AudioFilter { name, params, .. } => {
411                assert_eq!(name, "volume");
412                assert_eq!(params["volume"], ParamValue::Number(0.5));
413            }
414            other => panic!("expected AudioFilter, got {:?}", other),
415        }
416    }
417
418    #[test]
419    fn loudnorm_defaults() {
420        let (_, a) = input("test.mp4");
421        let a = a.pipe(loudnorm(Loudnorm::default()));
422        let graph = a.graph.borrow();
423        let node = graph.arena.get(a.node_id);
424        match node {
425            renderbox_dsl::OpNode::AudioFilter { name, params, .. } => {
426                assert_eq!(name, "loudnorm");
427                assert!(params.is_empty());
428            }
429            other => panic!("expected AudioFilter, got {:?}", other),
430        }
431    }
432
433    #[test]
434    fn loudnorm_with_target() {
435        let (_, a) = input("test.mp4");
436        let a = a.pipe(loudnorm(Loudnorm {
437            target: Some(-16.0),
438        }));
439        let graph = a.graph.borrow();
440        match graph.arena.get(a.node_id) {
441            renderbox_dsl::OpNode::AudioFilter { name, params, .. } => {
442                assert_eq!(name, "loudnorm");
443                assert_eq!(params["target"], ParamValue::Number(-16.0));
444            }
445            other => panic!("expected AudioFilter, got {:?}", other),
446        }
447    }
448
449    #[test]
450    fn aecho_builder_partial_params() {
451        let (_, a) = input("test.mp4");
452        let a = a.pipe(aecho(Aecho {
453            delays: Some("500".into()),
454            decays: Some("0.5".into()),
455            ..Default::default()
456        }));
457        let graph = a.graph.borrow();
458        match graph.arena.get(a.node_id) {
459            renderbox_dsl::OpNode::AudioFilter { name, params, .. } => {
460                assert_eq!(name, "aecho");
461                assert_eq!(params["delays"], ParamValue::String("500".into()));
462                assert_eq!(params["decays"], ParamValue::String("0.5".into()));
463                assert!(!params.contains_key("in_gain"));
464                assert!(!params.contains_key("out_gain"));
465            }
466            other => panic!("expected AudioFilter, got {:?}", other),
467        }
468    }
469
470    #[test]
471    fn afade_in_produces_correct_type() {
472        let (_, a) = input("test.mp4");
473        let a = a.pipe(afade_in(2.0));
474        let graph = a.graph.borrow();
475        match graph.arena.get(a.node_id) {
476            renderbox_dsl::OpNode::AudioFilter { name, params, .. } => {
477                assert_eq!(name, "afade");
478                assert_eq!(params["t"], ParamValue::String("in".into()));
479                assert_eq!(params["d"], ParamValue::Number(2.0));
480            }
481            other => panic!("expected AudioFilter, got {:?}", other),
482        }
483    }
484
485    #[test]
486    fn afade_out_produces_correct_type() {
487        let (_, a) = input("test.mp4");
488        let a = a.pipe(afade_out(5.0, 2.0));
489        let graph = a.graph.borrow();
490        match graph.arena.get(a.node_id) {
491            renderbox_dsl::OpNode::AudioFilter { name, params, .. } => {
492                assert_eq!(name, "afade");
493                assert_eq!(params["t"], ParamValue::String("out".into()));
494                assert_eq!(params["st"], ParamValue::Number(5.0));
495                assert_eq!(params["d"], ParamValue::Number(2.0));
496            }
497            other => panic!("expected AudioFilter, got {:?}", other),
498        }
499    }
500
501    #[test]
502    fn audio_pipeline_chains() {
503        let (v, a) = input("test.mp4");
504        let a = a
505            .pipe(volume(0.8))
506            .pipe(lowpass(8000.0))
507            .pipe(loudnorm(Loudnorm {
508                target: Some(-14.0),
509            }));
510        let bytes = crate::graph::output("out.mp4", v, a).build().unwrap();
511        assert!(!bytes.is_empty());
512    }
513}