1use crate::error::{PulseError, PulseResult};
4use std::collections::BTreeMap;
5use tunes::composition::TrackBuilder;
6use tunes::synthesis::effects::{Chorus, Compressor, Delay, Limiter, Phaser, Reverb, EQ};
7use tunes::synthesis::filter::{Filter, FilterSlope, FilterType};
8use tunes::track::{Mixer, Track};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct EffectInfo {
13 pub name: &'static str,
15 pub category: &'static str,
17 pub scopes: &'static [&'static str],
19 pub parameters: &'static [&'static str],
21 pub presets: &'static [&'static str],
23}
24
25#[derive(Debug, Clone, PartialEq)]
27pub enum EffectOption {
28 Number(f32),
30 Text(String),
32 Integer(i64),
34}
35
36#[derive(Debug, Clone, Default, PartialEq)]
38pub enum EffectOptions {
39 #[default]
41 Default,
42 Preset(String),
44 Params(BTreeMap<String, EffectOption>),
46}
47
48const TRACK_AND_MASTER: &[&str] = &["track", "master"];
49const TRACK_ONLY: &[&str] = &["track"];
50
51const EFFECT_INFOS: &[EffectInfo] = &[
52 EffectInfo {
53 name: "delay",
54 category: "time",
55 scopes: TRACK_AND_MASTER,
56 parameters: &["time", "feedback", "mix"],
57 presets: &[
58 "eighth_note",
59 "quarter_note",
60 "dotted_eighth",
61 "half_note",
62 "slapback",
63 "ping_pong",
64 "doubling",
65 "ambient",
66 ],
67 },
68 EffectInfo {
69 name: "reverb",
70 category: "space",
71 scopes: TRACK_AND_MASTER,
72 parameters: &["room_size", "damping", "mix"],
73 presets: &[
74 "room",
75 "hall",
76 "plate",
77 "chamber",
78 "cathedral",
79 "ambient",
80 "subtle",
81 "spring",
82 ],
83 },
84 EffectInfo {
85 name: "filter",
86 category: "filter",
87 scopes: TRACK_ONLY,
88 parameters: &["type", "cutoff", "resonance", "slope"],
89 presets: &[],
90 },
91 EffectInfo {
92 name: "compressor",
93 category: "dynamics",
94 scopes: TRACK_AND_MASTER,
95 parameters: &["threshold", "ratio", "attack", "release", "makeup_gain"],
96 presets: &["gentle", "drum_bus", "bass", "master", "aggressive"],
97 },
98 EffectInfo {
99 name: "limiter",
100 category: "dynamics",
101 scopes: TRACK_AND_MASTER,
102 parameters: &["threshold", "release"],
103 presets: &[
104 "transparent",
105 "standard",
106 "brick_wall",
107 "mastering",
108 "safety",
109 ],
110 },
111 EffectInfo {
112 name: "chorus",
113 category: "modulation",
114 scopes: TRACK_AND_MASTER,
115 parameters: &["rate", "depth", "mix"],
116 presets: &["subtle", "classic", "wide", "vibrato", "thick"],
117 },
118 EffectInfo {
119 name: "phaser",
120 category: "modulation",
121 scopes: TRACK_AND_MASTER,
122 parameters: &["rate", "depth", "feedback", "stages", "mix"],
123 presets: &["slow", "classic", "fast", "subtle", "deep"],
124 },
125 EffectInfo {
126 name: "eq",
127 category: "filter",
128 scopes: TRACK_AND_MASTER,
129 parameters: &["low_gain", "mid_gain", "high_gain", "low_freq", "high_freq"],
130 presets: &["flat", "bass_boost", "bright", "warm", "phone"],
131 },
132];
133
134#[derive(Debug, Clone, PartialEq)]
136pub struct PulseDelay {
137 pub time: f32,
139 pub feedback: f32,
141 pub mix: f32,
143}
144
145#[derive(Debug, Clone, PartialEq)]
147pub struct PulseReverb {
148 pub room_size: f32,
150 pub damping: f32,
152 pub mix: f32,
154}
155
156#[derive(Debug, Clone, PartialEq)]
158pub struct PulseFilter {
159 pub kind: PulseFilterKind,
161 pub cutoff: f32,
163 pub resonance: f32,
165 pub slope: PulseFilterSlope,
167}
168
169#[derive(Debug, Clone, PartialEq)]
171pub struct PulseCompressor {
172 pub threshold: f32,
174 pub ratio: f32,
176 pub attack: f32,
178 pub release: f32,
180 pub makeup_gain: f32,
182}
183
184#[derive(Debug, Clone, PartialEq)]
186pub struct PulseLimiter {
187 pub threshold: f32,
189 pub release: f32,
191}
192
193#[derive(Debug, Clone, PartialEq)]
195pub struct PulseChorus {
196 pub rate: f32,
198 pub depth: f32,
200 pub mix: f32,
202}
203
204#[derive(Debug, Clone, PartialEq)]
206pub struct PulsePhaser {
207 pub rate: f32,
209 pub depth: f32,
211 pub feedback: f32,
213 pub stages: usize,
215 pub mix: f32,
217}
218
219#[derive(Debug, Clone, PartialEq)]
221pub struct PulseEq {
222 pub low_gain: f32,
224 pub mid_gain: f32,
226 pub high_gain: f32,
228 pub low_freq: f32,
230 pub high_freq: f32,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum PulseFilterKind {
237 LowPass,
239 HighPass,
241 BandPass,
243 Notch,
245 AllPass,
247 Moog,
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum PulseFilterSlope {
254 Pole12,
256 Pole24,
258}
259
260#[derive(Debug, Clone, PartialEq)]
262pub enum PulseEffect {
263 Delay(PulseDelay),
265 Reverb(PulseReverb),
267 Filter(PulseFilter),
269 Compressor(PulseCompressor),
271 Limiter(PulseLimiter),
273 Chorus(PulseChorus),
275 Phaser(PulsePhaser),
277 Eq(PulseEq),
279}
280
281impl Default for PulseDelay {
282 fn default() -> Self {
283 Self {
284 time: 0.25,
285 feedback: 0.4,
286 mix: 0.35,
287 }
288 }
289}
290
291impl Default for PulseReverb {
292 fn default() -> Self {
293 Self {
294 room_size: 0.5,
295 damping: 0.5,
296 mix: 0.25,
297 }
298 }
299}
300
301impl Default for PulseFilter {
302 fn default() -> Self {
303 Self {
304 kind: PulseFilterKind::LowPass,
305 cutoff: 1200.0,
306 resonance: 0.3,
307 slope: PulseFilterSlope::Pole12,
308 }
309 }
310}
311
312impl Default for PulseCompressor {
313 fn default() -> Self {
314 Self {
315 threshold: 0.5,
316 ratio: 2.0,
317 attack: 0.01,
318 release: 0.1,
319 makeup_gain: 1.0,
320 }
321 }
322}
323
324impl Default for PulseLimiter {
325 fn default() -> Self {
326 Self {
327 threshold: -0.3,
328 release: 0.05,
329 }
330 }
331}
332
333impl Default for PulseChorus {
334 fn default() -> Self {
335 Self {
336 rate: 1.5,
337 depth: 5.0,
338 mix: 0.5,
339 }
340 }
341}
342
343impl Default for PulsePhaser {
344 fn default() -> Self {
345 Self {
346 rate: 0.5,
347 depth: 0.8,
348 feedback: 0.6,
349 stages: 4,
350 mix: 0.6,
351 }
352 }
353}
354
355impl Default for PulseEq {
356 fn default() -> Self {
357 Self {
358 low_gain: 1.0,
359 mid_gain: 1.0,
360 high_gain: 1.0,
361 low_freq: 300.0,
362 high_freq: 3000.0,
363 }
364 }
365}
366
367#[must_use]
369pub fn effect_infos() -> &'static [EffectInfo] {
370 EFFECT_INFOS
371}
372
373#[must_use]
375pub fn effect_names() -> Vec<&'static str> {
376 EFFECT_INFOS.iter().map(|info| info.name).collect()
377}
378
379pub fn effect_info(name: &str) -> PulseResult<&'static EffectInfo> {
385 let normalized = normalize_name(name);
386 EFFECT_INFOS
387 .iter()
388 .find(|info| normalize_name(info.name) == normalized)
389 .ok_or_else(|| PulseError::InvalidEffect {
390 name: name.to_string(),
391 })
392}
393
394pub fn effect_from_options(name: &str, options: EffectOptions) -> PulseResult<PulseEffect> {
400 match normalize_name(name).as_str() {
401 "delay" => build_delay(options).map(PulseEffect::Delay),
402 "reverb" => build_reverb(options).map(PulseEffect::Reverb),
403 "filter" => build_filter(options).map(PulseEffect::Filter),
404 "compressor" => build_compressor(options).map(PulseEffect::Compressor),
405 "limiter" => build_limiter(options).map(PulseEffect::Limiter),
406 "chorus" => build_chorus(options).map(PulseEffect::Chorus),
407 "phaser" => build_phaser(options).map(PulseEffect::Phaser),
408 "eq" => build_eq(options).map(PulseEffect::Eq),
409 _ => Err(PulseError::InvalidEffect {
410 name: name.to_string(),
411 }),
412 }
413}
414
415fn normalize_name(value: &str) -> String {
416 value
417 .trim()
418 .to_ascii_lowercase()
419 .replace(['_', '-', ' '], "")
420}
421
422fn option_key_error(effect: &str, option: &str) -> PulseError {
423 PulseError::InvalidEffectOption {
424 effect: effect.to_string(),
425 option: option.to_string(),
426 value: "unsupported".to_string(),
427 }
428}
429
430fn option_value_error(effect: &str, option: &str, value: impl ToString) -> PulseError {
431 PulseError::InvalidEffectOption {
432 effect: effect.to_string(),
433 option: option.to_string(),
434 value: value.to_string(),
435 }
436}
437
438fn invalid_preset(effect: &str, preset: &str) -> PulseError {
439 PulseError::InvalidEffectPreset {
440 effect: effect.to_string(),
441 preset: preset.to_string(),
442 }
443}
444
445fn number_option(effect: &str, option: &str, value: &EffectOption) -> PulseResult<f32> {
446 match value {
447 EffectOption::Number(value) if value.is_finite() => Ok(*value),
448 EffectOption::Integer(value) => Ok(*value as f32),
449 EffectOption::Number(value) => Err(option_value_error(effect, option, value)),
450 EffectOption::Text(value) => Err(option_value_error(effect, option, value)),
451 }
452}
453
454fn integer_option(effect: &str, option: &str, value: &EffectOption) -> PulseResult<i64> {
455 match value {
456 EffectOption::Integer(value) => Ok(*value),
457 EffectOption::Number(value) if value.is_finite() && value.fract() == 0.0 => {
458 Ok(*value as i64)
459 }
460 EffectOption::Number(value) => Err(option_value_error(effect, option, value)),
461 EffectOption::Text(value) => Err(option_value_error(effect, option, value)),
462 }
463}
464
465fn text_option(effect: &str, option: &str, value: &EffectOption) -> PulseResult<String> {
466 match value {
467 EffectOption::Text(value) => Ok(value.clone()),
468 EffectOption::Number(value) => Err(option_value_error(effect, option, value)),
469 EffectOption::Integer(value) => Err(option_value_error(effect, option, value)),
470 }
471}
472
473fn bounded_number(
474 effect: &str,
475 option: &str,
476 value: &EffectOption,
477 min: f32,
478 max: f32,
479) -> PulseResult<f32> {
480 let value = number_option(effect, option, value)?;
481 if (min..=max).contains(&value) {
482 Ok(value)
483 } else {
484 Err(option_value_error(effect, option, value))
485 }
486}
487
488fn unsupported_option(effect: &str, key: &str) -> PulseResult<()> {
489 Err(option_key_error(effect, key))
490}
491
492fn build_delay(options: EffectOptions) -> PulseResult<PulseDelay> {
493 match options {
494 EffectOptions::Default => Ok(PulseDelay::default()),
495 EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
496 "eighthnote" => Ok(PulseDelay {
497 time: 0.125,
498 feedback: 0.35,
499 mix: 0.3,
500 }),
501 "quarternote" => Ok(PulseDelay {
502 time: 0.25,
503 feedback: 0.4,
504 mix: 0.35,
505 }),
506 "dottedeighth" => Ok(PulseDelay {
507 time: 0.1875,
508 feedback: 0.35,
509 mix: 0.3,
510 }),
511 "halfnote" => Ok(PulseDelay {
512 time: 0.5,
513 feedback: 0.45,
514 mix: 0.4,
515 }),
516 "slapback" => Ok(PulseDelay {
517 time: 0.08,
518 feedback: 0.0,
519 mix: 0.3,
520 }),
521 "pingpong" => Ok(PulseDelay {
522 time: 0.375,
523 feedback: 0.5,
524 mix: 0.4,
525 }),
526 "doubling" => Ok(PulseDelay {
527 time: 0.03,
528 feedback: 0.0,
529 mix: 0.2,
530 }),
531 "ambient" => Ok(PulseDelay {
532 time: 1.0,
533 feedback: 0.6,
534 mix: 0.5,
535 }),
536 _ => Err(invalid_preset("delay", &preset)),
537 },
538 EffectOptions::Params(params) => {
539 let mut effect = PulseDelay::default();
540 for (key, value) in params {
541 match key.as_str() {
542 "time" => effect.time = bounded_number("delay", "time", &value, 0.001, 10.0)?,
543 "feedback" => {
544 effect.feedback = bounded_number("delay", "feedback", &value, 0.0, 0.99)?;
545 }
546 "mix" => effect.mix = bounded_number("delay", "mix", &value, 0.0, 1.0)?,
547 _ => unsupported_option("delay", &key)?,
548 }
549 }
550 Ok(effect)
551 }
552 }
553}
554
555fn build_reverb(options: EffectOptions) -> PulseResult<PulseReverb> {
556 match options {
557 EffectOptions::Default => Ok(PulseReverb::default()),
558 EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
559 "room" => Ok(PulseReverb {
560 room_size: 0.3,
561 damping: 0.7,
562 mix: 0.2,
563 }),
564 "hall" => Ok(PulseReverb {
565 room_size: 0.8,
566 damping: 0.5,
567 mix: 0.3,
568 }),
569 "plate" => Ok(PulseReverb {
570 room_size: 0.5,
571 damping: 0.3,
572 mix: 0.25,
573 }),
574 "chamber" => Ok(PulseReverb {
575 room_size: 0.6,
576 damping: 0.6,
577 mix: 0.25,
578 }),
579 "cathedral" => Ok(PulseReverb {
580 room_size: 0.95,
581 damping: 0.4,
582 mix: 0.4,
583 }),
584 "ambient" => Ok(PulseReverb {
585 room_size: 0.9,
586 damping: 0.4,
587 mix: 0.5,
588 }),
589 "subtle" => Ok(PulseReverb {
590 room_size: 0.4,
591 damping: 0.6,
592 mix: 0.15,
593 }),
594 "spring" => Ok(PulseReverb {
595 room_size: 0.4,
596 damping: 0.8,
597 mix: 0.3,
598 }),
599 _ => Err(invalid_preset("reverb", &preset)),
600 },
601 EffectOptions::Params(params) => {
602 let mut effect = PulseReverb::default();
603 for (key, value) in params {
604 match key.as_str() {
605 "room_size" => {
606 effect.room_size = bounded_number("reverb", "room_size", &value, 0.0, 1.0)?;
607 }
608 "damping" => {
609 effect.damping = bounded_number("reverb", "damping", &value, 0.0, 1.0)?;
610 }
611 "mix" => effect.mix = bounded_number("reverb", "mix", &value, 0.0, 1.0)?,
612 _ => unsupported_option("reverb", &key)?,
613 }
614 }
615 Ok(effect)
616 }
617 }
618}
619
620fn build_filter(options: EffectOptions) -> PulseResult<PulseFilter> {
621 match options {
622 EffectOptions::Default => Ok(PulseFilter::default()),
623 EffectOptions::Preset(preset) => Err(invalid_preset("filter", &preset)),
624 EffectOptions::Params(params) => {
625 let mut effect = PulseFilter::default();
626 for (key, value) in params {
627 match key.as_str() {
628 "type" => {
629 let kind = text_option("filter", "type", &value)?;
630 effect.kind = match normalize_name(&kind).as_str() {
631 "lowpass" => PulseFilterKind::LowPass,
632 "highpass" => PulseFilterKind::HighPass,
633 "bandpass" => PulseFilterKind::BandPass,
634 "notch" => PulseFilterKind::Notch,
635 "allpass" => PulseFilterKind::AllPass,
636 "moog" => PulseFilterKind::Moog,
637 _ => return Err(option_value_error("filter", "type", kind)),
638 };
639 }
640 "cutoff" => {
641 effect.cutoff = bounded_number("filter", "cutoff", &value, 20.0, 20000.0)?;
642 }
643 "resonance" => {
644 effect.resonance =
645 bounded_number("filter", "resonance", &value, 0.0, 0.99)?;
646 }
647 "slope" => {
648 let slope = integer_option("filter", "slope", &value)?;
649 effect.slope = match slope {
650 12 => PulseFilterSlope::Pole12,
651 24 => PulseFilterSlope::Pole24,
652 _ => return Err(option_value_error("filter", "slope", slope)),
653 };
654 }
655 _ => unsupported_option("filter", &key)?,
656 }
657 }
658 Ok(effect)
659 }
660 }
661}
662
663fn build_compressor(options: EffectOptions) -> PulseResult<PulseCompressor> {
664 match options {
665 EffectOptions::Default => Ok(PulseCompressor::default()),
666 EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
667 "gentle" => Ok(PulseCompressor {
668 threshold: 0.5,
669 ratio: 2.0,
670 attack: 0.01,
671 release: 0.1,
672 makeup_gain: 1.0,
673 }),
674 "drumbus" => Ok(PulseCompressor {
675 threshold: 0.6,
676 ratio: 4.0,
677 attack: 0.01,
678 release: 0.15,
679 makeup_gain: 1.0,
680 }),
681 "bass" => Ok(PulseCompressor {
682 threshold: 0.5,
683 ratio: 6.0,
684 attack: 0.02,
685 release: 0.2,
686 makeup_gain: 1.0,
687 }),
688 "master" => Ok(PulseCompressor {
689 threshold: 0.6,
690 ratio: 2.5,
691 attack: 0.01,
692 release: 0.1,
693 makeup_gain: 1.0,
694 }),
695 "aggressive" => Ok(PulseCompressor {
696 threshold: 0.3,
697 ratio: 8.0,
698 attack: 0.005,
699 release: 0.08,
700 makeup_gain: 1.0,
701 }),
702 _ => Err(invalid_preset("compressor", &preset)),
703 },
704 EffectOptions::Params(params) => {
705 let mut effect = PulseCompressor::default();
706 for (key, value) in params {
707 match key.as_str() {
708 "threshold" => {
709 effect.threshold =
710 bounded_number("compressor", "threshold", &value, 0.0, 1.0)?;
711 }
712 "ratio" => {
713 effect.ratio = bounded_number("compressor", "ratio", &value, 1.0, 20.0)?;
714 }
715 "attack" => {
716 effect.attack = bounded_number("compressor", "attack", &value, 0.001, 1.0)?;
717 }
718 "release" => {
719 effect.release =
720 bounded_number("compressor", "release", &value, 0.001, 5.0)?;
721 }
722 "makeup_gain" => {
723 effect.makeup_gain =
724 bounded_number("compressor", "makeup_gain", &value, 0.1, 4.0)?;
725 }
726 _ => unsupported_option("compressor", &key)?,
727 }
728 }
729 Ok(effect)
730 }
731 }
732}
733
734fn build_limiter(options: EffectOptions) -> PulseResult<PulseLimiter> {
735 match options {
736 EffectOptions::Default => Ok(PulseLimiter::default()),
737 EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
738 "transparent" => Ok(PulseLimiter {
739 threshold: -0.5,
740 release: 0.1,
741 }),
742 "standard" => Ok(PulseLimiter {
743 threshold: -0.3,
744 release: 0.05,
745 }),
746 "brickwall" => Ok(PulseLimiter {
747 threshold: -0.1,
748 release: 0.005,
749 }),
750 "mastering" => Ok(PulseLimiter {
751 threshold: -0.2,
752 release: 0.08,
753 }),
754 "safety" => Ok(PulseLimiter {
755 threshold: 0.0,
756 release: 0.01,
757 }),
758 _ => Err(invalid_preset("limiter", &preset)),
759 },
760 EffectOptions::Params(params) => {
761 let mut effect = PulseLimiter::default();
762 for (key, value) in params {
763 match key.as_str() {
764 "threshold" => {
765 effect.threshold =
766 bounded_number("limiter", "threshold", &value, -60.0, 0.0)?;
767 }
768 "release" => {
769 effect.release = bounded_number("limiter", "release", &value, 0.001, 5.0)?;
770 }
771 _ => unsupported_option("limiter", &key)?,
772 }
773 }
774 Ok(effect)
775 }
776 }
777}
778
779fn build_chorus(options: EffectOptions) -> PulseResult<PulseChorus> {
780 match options {
781 EffectOptions::Default => Ok(PulseChorus::default()),
782 EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
783 "subtle" => Ok(PulseChorus {
784 rate: 0.5,
785 depth: 3.0,
786 mix: 0.3,
787 }),
788 "classic" => Ok(PulseChorus {
789 rate: 1.5,
790 depth: 5.0,
791 mix: 0.5,
792 }),
793 "wide" => Ok(PulseChorus {
794 rate: 0.8,
795 depth: 8.0,
796 mix: 0.6,
797 }),
798 "vibrato" => Ok(PulseChorus {
799 rate: 5.0,
800 depth: 3.0,
801 mix: 1.0,
802 }),
803 "thick" => Ok(PulseChorus {
804 rate: 2.0,
805 depth: 7.0,
806 mix: 0.7,
807 }),
808 _ => Err(invalid_preset("chorus", &preset)),
809 },
810 EffectOptions::Params(params) => {
811 let mut effect = PulseChorus::default();
812 for (key, value) in params {
813 match key.as_str() {
814 "rate" => effect.rate = bounded_number("chorus", "rate", &value, 0.1, 10.0)?,
815 "depth" => {
816 effect.depth = bounded_number("chorus", "depth", &value, 0.5, 50.0)?;
817 }
818 "mix" => effect.mix = bounded_number("chorus", "mix", &value, 0.0, 1.0)?,
819 _ => unsupported_option("chorus", &key)?,
820 }
821 }
822 Ok(effect)
823 }
824 }
825}
826
827fn build_phaser(options: EffectOptions) -> PulseResult<PulsePhaser> {
828 match options {
829 EffectOptions::Default => Ok(PulsePhaser::default()),
830 EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
831 "slow" => Ok(PulsePhaser {
832 rate: 0.3,
833 depth: 0.7,
834 feedback: 0.5,
835 stages: 4,
836 mix: 0.5,
837 }),
838 "classic" => Ok(PulsePhaser {
839 rate: 0.5,
840 depth: 0.8,
841 feedback: 0.6,
842 stages: 4,
843 mix: 0.6,
844 }),
845 "fast" => Ok(PulsePhaser {
846 rate: 2.0,
847 depth: 0.9,
848 feedback: 0.7,
849 stages: 6,
850 mix: 0.7,
851 }),
852 "subtle" => Ok(PulsePhaser {
853 rate: 0.4,
854 depth: 0.5,
855 feedback: 0.3,
856 stages: 4,
857 mix: 0.4,
858 }),
859 "deep" => Ok(PulsePhaser {
860 rate: 0.6,
861 depth: 1.0,
862 feedback: 0.8,
863 stages: 8,
864 mix: 0.8,
865 }),
866 _ => Err(invalid_preset("phaser", &preset)),
867 },
868 EffectOptions::Params(params) => {
869 let mut effect = PulsePhaser::default();
870 for (key, value) in params {
871 match key.as_str() {
872 "rate" => effect.rate = bounded_number("phaser", "rate", &value, 0.1, 10.0)?,
873 "depth" => {
874 effect.depth = bounded_number("phaser", "depth", &value, 0.0, 1.0)?;
875 }
876 "feedback" => {
877 effect.feedback = bounded_number("phaser", "feedback", &value, 0.0, 0.95)?;
878 }
879 "mix" => effect.mix = bounded_number("phaser", "mix", &value, 0.0, 1.0)?,
880 "stages" => {
881 let stages = integer_option("phaser", "stages", &value)?;
882 effect.stages = match stages {
883 2 | 4 | 6 | 8 => stages as usize,
884 _ => return Err(option_value_error("phaser", "stages", stages)),
885 };
886 }
887 _ => unsupported_option("phaser", &key)?,
888 }
889 }
890 Ok(effect)
891 }
892 }
893}
894
895fn build_eq(options: EffectOptions) -> PulseResult<PulseEq> {
896 match options {
897 EffectOptions::Default => Ok(PulseEq::default()),
898 EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
899 "flat" => Ok(PulseEq::default()),
900 "bassboost" => Ok(PulseEq {
901 low_gain: 1.5,
902 mid_gain: 1.0,
903 high_gain: 1.0,
904 low_freq: 100.0,
905 high_freq: 3000.0,
906 }),
907 "bright" => Ok(PulseEq {
908 low_gain: 0.8,
909 mid_gain: 1.0,
910 high_gain: 1.4,
911 low_freq: 300.0,
912 high_freq: 5000.0,
913 }),
914 "warm" => Ok(PulseEq {
915 low_gain: 1.3,
916 mid_gain: 1.0,
917 high_gain: 0.9,
918 low_freq: 150.0,
919 high_freq: 3000.0,
920 }),
921 "phone" => Ok(PulseEq {
922 low_gain: 0.2,
923 mid_gain: 1.4,
924 high_gain: 0.2,
925 low_freq: 600.0,
926 high_freq: 3000.0,
927 }),
928 _ => Err(invalid_preset("eq", &preset)),
929 },
930 EffectOptions::Params(params) => {
931 let mut effect = PulseEq::default();
932 for (key, value) in params {
933 match key.as_str() {
934 "low_gain" => {
935 effect.low_gain = bounded_number("eq", "low_gain", &value, 0.0, 4.0)?;
936 }
937 "mid_gain" => {
938 effect.mid_gain = bounded_number("eq", "mid_gain", &value, 0.0, 4.0)?;
939 }
940 "high_gain" => {
941 effect.high_gain = bounded_number("eq", "high_gain", &value, 0.0, 4.0)?;
942 }
943 "low_freq" => {
944 effect.low_freq = bounded_number("eq", "low_freq", &value, 20.0, 20000.0)?;
945 }
946 "high_freq" => {
947 effect.high_freq =
948 bounded_number("eq", "high_freq", &value, 20.0, 20000.0)?;
949 }
950 _ => unsupported_option("eq", &key)?,
951 }
952 }
953 if effect.high_freq <= effect.low_freq {
954 return Err(option_value_error("eq", "high_freq", effect.high_freq));
955 }
956 Ok(effect)
957 }
958 }
959}
960
961impl PulseDelay {
962 #[must_use]
964 pub fn to_tunes(&self) -> Delay {
965 Delay::new(self.time, self.feedback, self.mix)
966 }
967}
968
969impl PulseReverb {
970 #[must_use]
972 pub fn to_tunes(&self) -> Reverb {
973 Reverb::new(self.room_size, self.damping, self.mix)
974 }
975}
976
977impl PulseFilter {
978 #[must_use]
980 pub fn to_tunes(&self) -> Filter {
981 let filter_type = match self.kind {
982 PulseFilterKind::LowPass => FilterType::LowPass,
983 PulseFilterKind::HighPass => FilterType::HighPass,
984 PulseFilterKind::BandPass => FilterType::BandPass,
985 PulseFilterKind::Notch => FilterType::Notch,
986 PulseFilterKind::AllPass => FilterType::AllPass,
987 PulseFilterKind::Moog => FilterType::Moog,
988 };
989 let slope = match self.slope {
990 PulseFilterSlope::Pole12 => FilterSlope::Pole12dB,
991 PulseFilterSlope::Pole24 => FilterSlope::Pole24dB,
992 };
993 Filter::with_slope(filter_type, self.cutoff, self.resonance, slope)
994 }
995}
996
997impl PulseCompressor {
998 #[must_use]
1000 pub fn to_tunes(&self) -> Compressor {
1001 Compressor::new(
1002 self.threshold,
1003 self.ratio,
1004 self.attack,
1005 self.release,
1006 self.makeup_gain,
1007 )
1008 }
1009}
1010
1011impl PulseLimiter {
1012 #[must_use]
1014 pub fn to_tunes(&self) -> Limiter {
1015 Limiter::new(self.threshold, self.release)
1016 }
1017}
1018
1019impl PulseChorus {
1020 #[must_use]
1022 pub fn to_tunes(&self) -> Chorus {
1023 Chorus::new(self.rate, self.depth, self.mix)
1024 }
1025}
1026
1027impl PulsePhaser {
1028 #[must_use]
1030 pub fn to_tunes(&self) -> Phaser {
1031 Phaser::new(self.rate, self.depth, self.feedback, self.stages, self.mix)
1032 }
1033}
1034
1035impl PulseEq {
1036 #[must_use]
1038 pub fn to_tunes(&self) -> EQ {
1039 EQ::new(
1040 self.low_gain,
1041 self.mid_gain,
1042 self.high_gain,
1043 self.low_freq,
1044 self.high_freq,
1045 )
1046 }
1047}
1048
1049impl PulseEffect {
1050 #[must_use]
1052 pub fn name(&self) -> &'static str {
1053 match self {
1054 Self::Delay(_) => "delay",
1055 Self::Reverb(_) => "reverb",
1056 Self::Filter(_) => "filter",
1057 Self::Compressor(_) => "compressor",
1058 Self::Limiter(_) => "limiter",
1059 Self::Chorus(_) => "chorus",
1060 Self::Phaser(_) => "phaser",
1061 Self::Eq(_) => "eq",
1062 }
1063 }
1064
1065 #[must_use]
1067 pub fn allowed_on_master(&self) -> bool {
1068 !matches!(self, Self::Filter(_))
1069 }
1070
1071 #[must_use]
1073 pub fn apply_to_track_builder<'a>(&self, builder: TrackBuilder<'a>) -> TrackBuilder<'a> {
1074 match self {
1075 Self::Delay(value) => builder.delay(value.to_tunes()),
1076 Self::Reverb(value) => builder.reverb(value.to_tunes()),
1077 Self::Filter(value) => builder.filter(value.to_tunes()),
1078 Self::Compressor(value) => builder.compressor(value.to_tunes()),
1079 Self::Limiter(value) => builder.limiter(value.to_tunes()),
1080 Self::Chorus(value) => builder.chorus(value.to_tunes()),
1081 Self::Phaser(value) => builder.phaser(value.to_tunes()),
1082 Self::Eq(value) => builder.eq(value.to_tunes()),
1083 }
1084 }
1085
1086 pub(crate) fn apply_to_track(&self, track: &mut Track) {
1088 match self {
1089 Self::Delay(value) => {
1090 track.effects = track.effects.clone().with_delay(value.to_tunes())
1091 }
1092 Self::Reverb(value) => {
1093 track.effects = track.effects.clone().with_reverb(value.to_tunes());
1094 }
1095 Self::Filter(value) => {
1096 track.filter = value.to_tunes();
1097 }
1098 Self::Compressor(value) => {
1099 track.effects = track.effects.clone().with_compressor(value.to_tunes());
1100 }
1101 Self::Limiter(value) => {
1102 track.effects = track.effects.clone().with_limiter(value.to_tunes());
1103 }
1104 Self::Chorus(value) => {
1105 track.effects = track.effects.clone().with_chorus(value.to_tunes());
1106 }
1107 Self::Phaser(value) => {
1108 track.effects = track.effects.clone().with_phaser(value.to_tunes());
1109 }
1110 Self::Eq(value) => {
1111 track.effects = track.effects.clone().with_eq(value.to_tunes());
1112 }
1113 }
1114 }
1115
1116 pub fn apply_to_master(&self, mixer: &mut Mixer) -> PulseResult<()> {
1122 match self {
1123 Self::Delay(value) => mixer.master_delay(value.to_tunes()),
1124 Self::Reverb(value) => mixer.master_reverb(value.to_tunes()),
1125 Self::Filter(_) => {
1126 return Err(PulseError::InvalidEffectScope {
1127 effect: "filter".to_string(),
1128 scope: "master".to_string(),
1129 });
1130 }
1131 Self::Compressor(value) => mixer.master_compressor(value.to_tunes()),
1132 Self::Limiter(value) => mixer.master_limiter(value.to_tunes()),
1133 Self::Chorus(value) => mixer.master_chorus(value.to_tunes()),
1134 Self::Phaser(value) => mixer.master_phaser(value.to_tunes()),
1135 Self::Eq(value) => mixer.master_eq(value.to_tunes()),
1136 }
1137 Ok(())
1138 }
1139}
1140
1141#[cfg(test)]
1142mod tests {
1143 use super::*;
1144 use std::collections::BTreeMap;
1145
1146 fn params(entries: &[(&str, EffectOption)]) -> EffectOptions {
1147 let mut values = BTreeMap::new();
1148 for (key, value) in entries {
1149 values.insert((*key).to_string(), value.clone());
1150 }
1151 EffectOptions::Params(values)
1152 }
1153
1154 #[test]
1155 fn effect_catalog_exposes_v1_effects() {
1156 let names = effect_names();
1157
1158 assert_eq!(names.len(), 8);
1159 assert!(names.contains(&"delay"));
1160 assert!(names.contains(&"reverb"));
1161 assert!(names.contains(&"filter"));
1162 assert!(names.contains(&"compressor"));
1163 assert!(names.contains(&"limiter"));
1164 assert!(names.contains(&"chorus"));
1165 assert!(names.contains(&"phaser"));
1166 assert!(names.contains(&"eq"));
1167
1168 let delay = effect_info("delay").expect("delay info should exist");
1169 assert_eq!(delay.category, "time");
1170 assert!(delay.scopes.contains(&"track"));
1171 assert!(delay.scopes.contains(&"master"));
1172 assert!(delay.parameters.contains(&"time"));
1173 assert!(delay.presets.contains(&"quarter_note"));
1174
1175 let filter = effect_info("filter").expect("filter info should exist");
1176 assert_eq!(filter.scopes, &["track"]);
1177 }
1178
1179 #[test]
1180 fn invalid_effect_name_reports_stable_error() {
1181 let error = effect_from_options("shimmer", EffectOptions::Default)
1182 .expect_err("unknown effect should fail");
1183
1184 assert_eq!(error.to_string(), "invalid effect: shimmer");
1185 }
1186
1187 #[test]
1188 fn delay_validates_params_and_presets() {
1189 let effect = effect_from_options(
1190 "delay",
1191 params(&[
1192 ("time", EffectOption::Number(0.25)),
1193 ("feedback", EffectOption::Number(0.35)),
1194 ("mix", EffectOption::Number(0.3)),
1195 ]),
1196 )
1197 .expect("valid delay should parse");
1198
1199 let PulseEffect::Delay(delay) = effect else {
1200 panic!("expected delay");
1201 };
1202 assert_eq!(delay.time, 0.25);
1203 assert_eq!(delay.feedback, 0.35);
1204 assert_eq!(delay.mix, 0.3);
1205
1206 let preset = effect_from_options("delay", EffectOptions::Preset("slapback".to_string()))
1207 .expect("delay preset should parse");
1208 assert!(matches!(preset, PulseEffect::Delay(_)));
1209
1210 let invalid =
1211 effect_from_options("delay", params(&[("feedback", EffectOption::Number(1.2))]))
1212 .expect_err("feedback outside range should fail");
1213 assert_eq!(
1214 invalid.to_string(),
1215 "invalid effect option delay.feedback: 1.2"
1216 );
1217 }
1218
1219 #[test]
1220 fn filter_validates_type_cutoff_resonance_and_scope() {
1221 let effect = effect_from_options(
1222 "filter",
1223 params(&[
1224 ("type", EffectOption::Text("low_pass".to_string())),
1225 ("cutoff", EffectOption::Number(1800.0)),
1226 ("resonance", EffectOption::Number(0.4)),
1227 ("slope", EffectOption::Integer(24)),
1228 ]),
1229 )
1230 .expect("valid filter should parse");
1231
1232 let PulseEffect::Filter(filter) = effect else {
1233 panic!("expected filter");
1234 };
1235 assert_eq!(filter.kind, PulseFilterKind::LowPass);
1236 assert_eq!(filter.cutoff, 1800.0);
1237 assert_eq!(filter.resonance, 0.4);
1238 assert_eq!(filter.slope, PulseFilterSlope::Pole24);
1239 assert!(!PulseEffect::Filter(filter).allowed_on_master());
1240
1241 let invalid = effect_from_options(
1242 "filter",
1243 params(&[("type", EffectOption::Text("comb".to_string()))]),
1244 )
1245 .expect_err("unknown filter type should fail");
1246 assert_eq!(
1247 invalid.to_string(),
1248 "invalid effect option filter.type: comb"
1249 );
1250 }
1251
1252 #[test]
1253 fn eq_rejects_invalid_frequency_order() {
1254 let invalid = effect_from_options(
1255 "eq",
1256 params(&[
1257 ("low_freq", EffectOption::Number(5000.0)),
1258 ("high_freq", EffectOption::Number(200.0)),
1259 ]),
1260 )
1261 .expect_err("high frequency must be above low frequency");
1262
1263 assert_eq!(
1264 invalid.to_string(),
1265 "invalid effect option eq.high_freq: 200"
1266 );
1267 }
1268
1269 #[test]
1270 fn all_default_effects_convert_to_tunes_types() {
1271 let effects = [
1272 effect_from_options("delay", EffectOptions::Default).unwrap(),
1273 effect_from_options("reverb", EffectOptions::Default).unwrap(),
1274 effect_from_options("filter", EffectOptions::Default).unwrap(),
1275 effect_from_options("compressor", EffectOptions::Default).unwrap(),
1276 effect_from_options("limiter", EffectOptions::Default).unwrap(),
1277 effect_from_options("chorus", EffectOptions::Default).unwrap(),
1278 effect_from_options("phaser", EffectOptions::Default).unwrap(),
1279 effect_from_options("eq", EffectOptions::Default).unwrap(),
1280 ];
1281
1282 for effect in effects {
1283 match effect {
1284 PulseEffect::Delay(value) => {
1285 let _ = value.to_tunes();
1286 }
1287 PulseEffect::Reverb(value) => {
1288 let _ = value.to_tunes();
1289 }
1290 PulseEffect::Filter(value) => {
1291 let _ = value.to_tunes();
1292 }
1293 PulseEffect::Compressor(value) => {
1294 let _ = value.to_tunes();
1295 }
1296 PulseEffect::Limiter(value) => {
1297 let _ = value.to_tunes();
1298 }
1299 PulseEffect::Chorus(value) => {
1300 let _ = value.to_tunes();
1301 }
1302 PulseEffect::Phaser(value) => {
1303 let _ = value.to_tunes();
1304 }
1305 PulseEffect::Eq(value) => {
1306 let _ = value.to_tunes();
1307 }
1308 }
1309 }
1310 }
1311}