1use std::collections::BTreeMap;
2
3use crate::ops::{add_audio_filter, params, OptParam};
4use crate::sorts::Audio;
5use crate::graph::Stream;
6
7pub 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
28pub 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#[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#[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#[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#[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}