use crate::error::{PulseError, PulseResult};
use std::collections::BTreeMap;
use tunes::composition::TrackBuilder;
use tunes::synthesis::effects::{Chorus, Compressor, Delay, Limiter, Phaser, Reverb, EQ};
use tunes::synthesis::filter::{Filter, FilterSlope, FilterType};
use tunes::track::{Mixer, Track};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EffectInfo {
pub name: &'static str,
pub category: &'static str,
pub scopes: &'static [&'static str],
pub parameters: &'static [&'static str],
pub presets: &'static [&'static str],
}
#[derive(Debug, Clone, PartialEq)]
pub enum EffectOption {
Number(f32),
Text(String),
Integer(i64),
}
#[derive(Debug, Clone, Default, PartialEq)]
pub enum EffectOptions {
#[default]
Default,
Preset(String),
Params(BTreeMap<String, EffectOption>),
}
const TRACK_AND_MASTER: &[&str] = &["track", "master"];
const TRACK_ONLY: &[&str] = &["track"];
const EFFECT_INFOS: &[EffectInfo] = &[
EffectInfo {
name: "delay",
category: "time",
scopes: TRACK_AND_MASTER,
parameters: &["time", "feedback", "mix"],
presets: &[
"eighth_note",
"quarter_note",
"dotted_eighth",
"half_note",
"slapback",
"ping_pong",
"doubling",
"ambient",
],
},
EffectInfo {
name: "reverb",
category: "space",
scopes: TRACK_AND_MASTER,
parameters: &["room_size", "damping", "mix"],
presets: &[
"room",
"hall",
"plate",
"chamber",
"cathedral",
"ambient",
"subtle",
"spring",
],
},
EffectInfo {
name: "filter",
category: "filter",
scopes: TRACK_ONLY,
parameters: &["type", "cutoff", "resonance", "slope"],
presets: &[],
},
EffectInfo {
name: "compressor",
category: "dynamics",
scopes: TRACK_AND_MASTER,
parameters: &["threshold", "ratio", "attack", "release", "makeup_gain"],
presets: &["gentle", "drum_bus", "bass", "master", "aggressive"],
},
EffectInfo {
name: "limiter",
category: "dynamics",
scopes: TRACK_AND_MASTER,
parameters: &["threshold", "release"],
presets: &[
"transparent",
"standard",
"brick_wall",
"mastering",
"safety",
],
},
EffectInfo {
name: "chorus",
category: "modulation",
scopes: TRACK_AND_MASTER,
parameters: &["rate", "depth", "mix"],
presets: &["subtle", "classic", "wide", "vibrato", "thick"],
},
EffectInfo {
name: "phaser",
category: "modulation",
scopes: TRACK_AND_MASTER,
parameters: &["rate", "depth", "feedback", "stages", "mix"],
presets: &["slow", "classic", "fast", "subtle", "deep"],
},
EffectInfo {
name: "eq",
category: "filter",
scopes: TRACK_AND_MASTER,
parameters: &["low_gain", "mid_gain", "high_gain", "low_freq", "high_freq"],
presets: &["flat", "bass_boost", "bright", "warm", "phone"],
},
];
#[derive(Debug, Clone, PartialEq)]
pub struct PulseDelay {
pub time: f32,
pub feedback: f32,
pub mix: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PulseReverb {
pub room_size: f32,
pub damping: f32,
pub mix: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PulseFilter {
pub kind: PulseFilterKind,
pub cutoff: f32,
pub resonance: f32,
pub slope: PulseFilterSlope,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PulseCompressor {
pub threshold: f32,
pub ratio: f32,
pub attack: f32,
pub release: f32,
pub makeup_gain: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PulseLimiter {
pub threshold: f32,
pub release: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PulseChorus {
pub rate: f32,
pub depth: f32,
pub mix: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PulsePhaser {
pub rate: f32,
pub depth: f32,
pub feedback: f32,
pub stages: usize,
pub mix: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PulseEq {
pub low_gain: f32,
pub mid_gain: f32,
pub high_gain: f32,
pub low_freq: f32,
pub high_freq: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PulseFilterKind {
LowPass,
HighPass,
BandPass,
Notch,
AllPass,
Moog,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PulseFilterSlope {
Pole12,
Pole24,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PulseEffect {
Delay(PulseDelay),
Reverb(PulseReverb),
Filter(PulseFilter),
Compressor(PulseCompressor),
Limiter(PulseLimiter),
Chorus(PulseChorus),
Phaser(PulsePhaser),
Eq(PulseEq),
}
impl Default for PulseDelay {
fn default() -> Self {
Self {
time: 0.25,
feedback: 0.4,
mix: 0.35,
}
}
}
impl Default for PulseReverb {
fn default() -> Self {
Self {
room_size: 0.5,
damping: 0.5,
mix: 0.25,
}
}
}
impl Default for PulseFilter {
fn default() -> Self {
Self {
kind: PulseFilterKind::LowPass,
cutoff: 1200.0,
resonance: 0.3,
slope: PulseFilterSlope::Pole12,
}
}
}
impl Default for PulseCompressor {
fn default() -> Self {
Self {
threshold: 0.5,
ratio: 2.0,
attack: 0.01,
release: 0.1,
makeup_gain: 1.0,
}
}
}
impl Default for PulseLimiter {
fn default() -> Self {
Self {
threshold: -0.3,
release: 0.05,
}
}
}
impl Default for PulseChorus {
fn default() -> Self {
Self {
rate: 1.5,
depth: 5.0,
mix: 0.5,
}
}
}
impl Default for PulsePhaser {
fn default() -> Self {
Self {
rate: 0.5,
depth: 0.8,
feedback: 0.6,
stages: 4,
mix: 0.6,
}
}
}
impl Default for PulseEq {
fn default() -> Self {
Self {
low_gain: 1.0,
mid_gain: 1.0,
high_gain: 1.0,
low_freq: 300.0,
high_freq: 3000.0,
}
}
}
#[must_use]
pub fn effect_infos() -> &'static [EffectInfo] {
EFFECT_INFOS
}
#[must_use]
pub fn effect_names() -> Vec<&'static str> {
EFFECT_INFOS.iter().map(|info| info.name).collect()
}
pub fn effect_info(name: &str) -> PulseResult<&'static EffectInfo> {
let normalized = normalize_name(name);
EFFECT_INFOS
.iter()
.find(|info| normalize_name(info.name) == normalized)
.ok_or_else(|| PulseError::InvalidEffect {
name: name.to_string(),
})
}
pub fn effect_from_options(name: &str, options: EffectOptions) -> PulseResult<PulseEffect> {
match normalize_name(name).as_str() {
"delay" => build_delay(options).map(PulseEffect::Delay),
"reverb" => build_reverb(options).map(PulseEffect::Reverb),
"filter" => build_filter(options).map(PulseEffect::Filter),
"compressor" => build_compressor(options).map(PulseEffect::Compressor),
"limiter" => build_limiter(options).map(PulseEffect::Limiter),
"chorus" => build_chorus(options).map(PulseEffect::Chorus),
"phaser" => build_phaser(options).map(PulseEffect::Phaser),
"eq" => build_eq(options).map(PulseEffect::Eq),
_ => Err(PulseError::InvalidEffect {
name: name.to_string(),
}),
}
}
fn normalize_name(value: &str) -> String {
value
.trim()
.to_ascii_lowercase()
.replace(['_', '-', ' '], "")
}
fn option_key_error(effect: &str, option: &str) -> PulseError {
PulseError::InvalidEffectOption {
effect: effect.to_string(),
option: option.to_string(),
value: "unsupported".to_string(),
}
}
fn option_value_error(effect: &str, option: &str, value: impl ToString) -> PulseError {
PulseError::InvalidEffectOption {
effect: effect.to_string(),
option: option.to_string(),
value: value.to_string(),
}
}
fn invalid_preset(effect: &str, preset: &str) -> PulseError {
PulseError::InvalidEffectPreset {
effect: effect.to_string(),
preset: preset.to_string(),
}
}
fn number_option(effect: &str, option: &str, value: &EffectOption) -> PulseResult<f32> {
match value {
EffectOption::Number(value) if value.is_finite() => Ok(*value),
EffectOption::Integer(value) => Ok(*value as f32),
EffectOption::Number(value) => Err(option_value_error(effect, option, value)),
EffectOption::Text(value) => Err(option_value_error(effect, option, value)),
}
}
fn integer_option(effect: &str, option: &str, value: &EffectOption) -> PulseResult<i64> {
match value {
EffectOption::Integer(value) => Ok(*value),
EffectOption::Number(value) if value.is_finite() && value.fract() == 0.0 => {
Ok(*value as i64)
}
EffectOption::Number(value) => Err(option_value_error(effect, option, value)),
EffectOption::Text(value) => Err(option_value_error(effect, option, value)),
}
}
fn text_option(effect: &str, option: &str, value: &EffectOption) -> PulseResult<String> {
match value {
EffectOption::Text(value) => Ok(value.clone()),
EffectOption::Number(value) => Err(option_value_error(effect, option, value)),
EffectOption::Integer(value) => Err(option_value_error(effect, option, value)),
}
}
fn bounded_number(
effect: &str,
option: &str,
value: &EffectOption,
min: f32,
max: f32,
) -> PulseResult<f32> {
let value = number_option(effect, option, value)?;
if (min..=max).contains(&value) {
Ok(value)
} else {
Err(option_value_error(effect, option, value))
}
}
fn unsupported_option(effect: &str, key: &str) -> PulseResult<()> {
Err(option_key_error(effect, key))
}
fn build_delay(options: EffectOptions) -> PulseResult<PulseDelay> {
match options {
EffectOptions::Default => Ok(PulseDelay::default()),
EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
"eighthnote" => Ok(PulseDelay {
time: 0.125,
feedback: 0.35,
mix: 0.3,
}),
"quarternote" => Ok(PulseDelay {
time: 0.25,
feedback: 0.4,
mix: 0.35,
}),
"dottedeighth" => Ok(PulseDelay {
time: 0.1875,
feedback: 0.35,
mix: 0.3,
}),
"halfnote" => Ok(PulseDelay {
time: 0.5,
feedback: 0.45,
mix: 0.4,
}),
"slapback" => Ok(PulseDelay {
time: 0.08,
feedback: 0.0,
mix: 0.3,
}),
"pingpong" => Ok(PulseDelay {
time: 0.375,
feedback: 0.5,
mix: 0.4,
}),
"doubling" => Ok(PulseDelay {
time: 0.03,
feedback: 0.0,
mix: 0.2,
}),
"ambient" => Ok(PulseDelay {
time: 1.0,
feedback: 0.6,
mix: 0.5,
}),
_ => Err(invalid_preset("delay", &preset)),
},
EffectOptions::Params(params) => {
let mut effect = PulseDelay::default();
for (key, value) in params {
match key.as_str() {
"time" => effect.time = bounded_number("delay", "time", &value, 0.001, 10.0)?,
"feedback" => {
effect.feedback = bounded_number("delay", "feedback", &value, 0.0, 0.99)?;
}
"mix" => effect.mix = bounded_number("delay", "mix", &value, 0.0, 1.0)?,
_ => unsupported_option("delay", &key)?,
}
}
Ok(effect)
}
}
}
fn build_reverb(options: EffectOptions) -> PulseResult<PulseReverb> {
match options {
EffectOptions::Default => Ok(PulseReverb::default()),
EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
"room" => Ok(PulseReverb {
room_size: 0.3,
damping: 0.7,
mix: 0.2,
}),
"hall" => Ok(PulseReverb {
room_size: 0.8,
damping: 0.5,
mix: 0.3,
}),
"plate" => Ok(PulseReverb {
room_size: 0.5,
damping: 0.3,
mix: 0.25,
}),
"chamber" => Ok(PulseReverb {
room_size: 0.6,
damping: 0.6,
mix: 0.25,
}),
"cathedral" => Ok(PulseReverb {
room_size: 0.95,
damping: 0.4,
mix: 0.4,
}),
"ambient" => Ok(PulseReverb {
room_size: 0.9,
damping: 0.4,
mix: 0.5,
}),
"subtle" => Ok(PulseReverb {
room_size: 0.4,
damping: 0.6,
mix: 0.15,
}),
"spring" => Ok(PulseReverb {
room_size: 0.4,
damping: 0.8,
mix: 0.3,
}),
_ => Err(invalid_preset("reverb", &preset)),
},
EffectOptions::Params(params) => {
let mut effect = PulseReverb::default();
for (key, value) in params {
match key.as_str() {
"room_size" => {
effect.room_size = bounded_number("reverb", "room_size", &value, 0.0, 1.0)?;
}
"damping" => {
effect.damping = bounded_number("reverb", "damping", &value, 0.0, 1.0)?;
}
"mix" => effect.mix = bounded_number("reverb", "mix", &value, 0.0, 1.0)?,
_ => unsupported_option("reverb", &key)?,
}
}
Ok(effect)
}
}
}
fn build_filter(options: EffectOptions) -> PulseResult<PulseFilter> {
match options {
EffectOptions::Default => Ok(PulseFilter::default()),
EffectOptions::Preset(preset) => Err(invalid_preset("filter", &preset)),
EffectOptions::Params(params) => {
let mut effect = PulseFilter::default();
for (key, value) in params {
match key.as_str() {
"type" => {
let kind = text_option("filter", "type", &value)?;
effect.kind = match normalize_name(&kind).as_str() {
"lowpass" => PulseFilterKind::LowPass,
"highpass" => PulseFilterKind::HighPass,
"bandpass" => PulseFilterKind::BandPass,
"notch" => PulseFilterKind::Notch,
"allpass" => PulseFilterKind::AllPass,
"moog" => PulseFilterKind::Moog,
_ => return Err(option_value_error("filter", "type", kind)),
};
}
"cutoff" => {
effect.cutoff = bounded_number("filter", "cutoff", &value, 20.0, 20000.0)?;
}
"resonance" => {
effect.resonance =
bounded_number("filter", "resonance", &value, 0.0, 0.99)?;
}
"slope" => {
let slope = integer_option("filter", "slope", &value)?;
effect.slope = match slope {
12 => PulseFilterSlope::Pole12,
24 => PulseFilterSlope::Pole24,
_ => return Err(option_value_error("filter", "slope", slope)),
};
}
_ => unsupported_option("filter", &key)?,
}
}
Ok(effect)
}
}
}
fn build_compressor(options: EffectOptions) -> PulseResult<PulseCompressor> {
match options {
EffectOptions::Default => Ok(PulseCompressor::default()),
EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
"gentle" => Ok(PulseCompressor {
threshold: 0.5,
ratio: 2.0,
attack: 0.01,
release: 0.1,
makeup_gain: 1.0,
}),
"drumbus" => Ok(PulseCompressor {
threshold: 0.6,
ratio: 4.0,
attack: 0.01,
release: 0.15,
makeup_gain: 1.0,
}),
"bass" => Ok(PulseCompressor {
threshold: 0.5,
ratio: 6.0,
attack: 0.02,
release: 0.2,
makeup_gain: 1.0,
}),
"master" => Ok(PulseCompressor {
threshold: 0.6,
ratio: 2.5,
attack: 0.01,
release: 0.1,
makeup_gain: 1.0,
}),
"aggressive" => Ok(PulseCompressor {
threshold: 0.3,
ratio: 8.0,
attack: 0.005,
release: 0.08,
makeup_gain: 1.0,
}),
_ => Err(invalid_preset("compressor", &preset)),
},
EffectOptions::Params(params) => {
let mut effect = PulseCompressor::default();
for (key, value) in params {
match key.as_str() {
"threshold" => {
effect.threshold =
bounded_number("compressor", "threshold", &value, 0.0, 1.0)?;
}
"ratio" => {
effect.ratio = bounded_number("compressor", "ratio", &value, 1.0, 20.0)?;
}
"attack" => {
effect.attack = bounded_number("compressor", "attack", &value, 0.001, 1.0)?;
}
"release" => {
effect.release =
bounded_number("compressor", "release", &value, 0.001, 5.0)?;
}
"makeup_gain" => {
effect.makeup_gain =
bounded_number("compressor", "makeup_gain", &value, 0.1, 4.0)?;
}
_ => unsupported_option("compressor", &key)?,
}
}
Ok(effect)
}
}
}
fn build_limiter(options: EffectOptions) -> PulseResult<PulseLimiter> {
match options {
EffectOptions::Default => Ok(PulseLimiter::default()),
EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
"transparent" => Ok(PulseLimiter {
threshold: -0.5,
release: 0.1,
}),
"standard" => Ok(PulseLimiter {
threshold: -0.3,
release: 0.05,
}),
"brickwall" => Ok(PulseLimiter {
threshold: -0.1,
release: 0.005,
}),
"mastering" => Ok(PulseLimiter {
threshold: -0.2,
release: 0.08,
}),
"safety" => Ok(PulseLimiter {
threshold: 0.0,
release: 0.01,
}),
_ => Err(invalid_preset("limiter", &preset)),
},
EffectOptions::Params(params) => {
let mut effect = PulseLimiter::default();
for (key, value) in params {
match key.as_str() {
"threshold" => {
effect.threshold =
bounded_number("limiter", "threshold", &value, -60.0, 0.0)?;
}
"release" => {
effect.release = bounded_number("limiter", "release", &value, 0.001, 5.0)?;
}
_ => unsupported_option("limiter", &key)?,
}
}
Ok(effect)
}
}
}
fn build_chorus(options: EffectOptions) -> PulseResult<PulseChorus> {
match options {
EffectOptions::Default => Ok(PulseChorus::default()),
EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
"subtle" => Ok(PulseChorus {
rate: 0.5,
depth: 3.0,
mix: 0.3,
}),
"classic" => Ok(PulseChorus {
rate: 1.5,
depth: 5.0,
mix: 0.5,
}),
"wide" => Ok(PulseChorus {
rate: 0.8,
depth: 8.0,
mix: 0.6,
}),
"vibrato" => Ok(PulseChorus {
rate: 5.0,
depth: 3.0,
mix: 1.0,
}),
"thick" => Ok(PulseChorus {
rate: 2.0,
depth: 7.0,
mix: 0.7,
}),
_ => Err(invalid_preset("chorus", &preset)),
},
EffectOptions::Params(params) => {
let mut effect = PulseChorus::default();
for (key, value) in params {
match key.as_str() {
"rate" => effect.rate = bounded_number("chorus", "rate", &value, 0.1, 10.0)?,
"depth" => {
effect.depth = bounded_number("chorus", "depth", &value, 0.5, 50.0)?;
}
"mix" => effect.mix = bounded_number("chorus", "mix", &value, 0.0, 1.0)?,
_ => unsupported_option("chorus", &key)?,
}
}
Ok(effect)
}
}
}
fn build_phaser(options: EffectOptions) -> PulseResult<PulsePhaser> {
match options {
EffectOptions::Default => Ok(PulsePhaser::default()),
EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
"slow" => Ok(PulsePhaser {
rate: 0.3,
depth: 0.7,
feedback: 0.5,
stages: 4,
mix: 0.5,
}),
"classic" => Ok(PulsePhaser {
rate: 0.5,
depth: 0.8,
feedback: 0.6,
stages: 4,
mix: 0.6,
}),
"fast" => Ok(PulsePhaser {
rate: 2.0,
depth: 0.9,
feedback: 0.7,
stages: 6,
mix: 0.7,
}),
"subtle" => Ok(PulsePhaser {
rate: 0.4,
depth: 0.5,
feedback: 0.3,
stages: 4,
mix: 0.4,
}),
"deep" => Ok(PulsePhaser {
rate: 0.6,
depth: 1.0,
feedback: 0.8,
stages: 8,
mix: 0.8,
}),
_ => Err(invalid_preset("phaser", &preset)),
},
EffectOptions::Params(params) => {
let mut effect = PulsePhaser::default();
for (key, value) in params {
match key.as_str() {
"rate" => effect.rate = bounded_number("phaser", "rate", &value, 0.1, 10.0)?,
"depth" => {
effect.depth = bounded_number("phaser", "depth", &value, 0.0, 1.0)?;
}
"feedback" => {
effect.feedback = bounded_number("phaser", "feedback", &value, 0.0, 0.95)?;
}
"mix" => effect.mix = bounded_number("phaser", "mix", &value, 0.0, 1.0)?,
"stages" => {
let stages = integer_option("phaser", "stages", &value)?;
effect.stages = match stages {
2 | 4 | 6 | 8 => stages as usize,
_ => return Err(option_value_error("phaser", "stages", stages)),
};
}
_ => unsupported_option("phaser", &key)?,
}
}
Ok(effect)
}
}
}
fn build_eq(options: EffectOptions) -> PulseResult<PulseEq> {
match options {
EffectOptions::Default => Ok(PulseEq::default()),
EffectOptions::Preset(preset) => match normalize_name(&preset).as_str() {
"flat" => Ok(PulseEq::default()),
"bassboost" => Ok(PulseEq {
low_gain: 1.5,
mid_gain: 1.0,
high_gain: 1.0,
low_freq: 100.0,
high_freq: 3000.0,
}),
"bright" => Ok(PulseEq {
low_gain: 0.8,
mid_gain: 1.0,
high_gain: 1.4,
low_freq: 300.0,
high_freq: 5000.0,
}),
"warm" => Ok(PulseEq {
low_gain: 1.3,
mid_gain: 1.0,
high_gain: 0.9,
low_freq: 150.0,
high_freq: 3000.0,
}),
"phone" => Ok(PulseEq {
low_gain: 0.2,
mid_gain: 1.4,
high_gain: 0.2,
low_freq: 600.0,
high_freq: 3000.0,
}),
_ => Err(invalid_preset("eq", &preset)),
},
EffectOptions::Params(params) => {
let mut effect = PulseEq::default();
for (key, value) in params {
match key.as_str() {
"low_gain" => {
effect.low_gain = bounded_number("eq", "low_gain", &value, 0.0, 4.0)?;
}
"mid_gain" => {
effect.mid_gain = bounded_number("eq", "mid_gain", &value, 0.0, 4.0)?;
}
"high_gain" => {
effect.high_gain = bounded_number("eq", "high_gain", &value, 0.0, 4.0)?;
}
"low_freq" => {
effect.low_freq = bounded_number("eq", "low_freq", &value, 20.0, 20000.0)?;
}
"high_freq" => {
effect.high_freq =
bounded_number("eq", "high_freq", &value, 20.0, 20000.0)?;
}
_ => unsupported_option("eq", &key)?,
}
}
if effect.high_freq <= effect.low_freq {
return Err(option_value_error("eq", "high_freq", effect.high_freq));
}
Ok(effect)
}
}
}
impl PulseDelay {
#[must_use]
pub fn to_tunes(&self) -> Delay {
Delay::new(self.time, self.feedback, self.mix)
}
}
impl PulseReverb {
#[must_use]
pub fn to_tunes(&self) -> Reverb {
Reverb::new(self.room_size, self.damping, self.mix)
}
}
impl PulseFilter {
#[must_use]
pub fn to_tunes(&self) -> Filter {
let filter_type = match self.kind {
PulseFilterKind::LowPass => FilterType::LowPass,
PulseFilterKind::HighPass => FilterType::HighPass,
PulseFilterKind::BandPass => FilterType::BandPass,
PulseFilterKind::Notch => FilterType::Notch,
PulseFilterKind::AllPass => FilterType::AllPass,
PulseFilterKind::Moog => FilterType::Moog,
};
let slope = match self.slope {
PulseFilterSlope::Pole12 => FilterSlope::Pole12dB,
PulseFilterSlope::Pole24 => FilterSlope::Pole24dB,
};
Filter::with_slope(filter_type, self.cutoff, self.resonance, slope)
}
}
impl PulseCompressor {
#[must_use]
pub fn to_tunes(&self) -> Compressor {
Compressor::new(
self.threshold,
self.ratio,
self.attack,
self.release,
self.makeup_gain,
)
}
}
impl PulseLimiter {
#[must_use]
pub fn to_tunes(&self) -> Limiter {
Limiter::new(self.threshold, self.release)
}
}
impl PulseChorus {
#[must_use]
pub fn to_tunes(&self) -> Chorus {
Chorus::new(self.rate, self.depth, self.mix)
}
}
impl PulsePhaser {
#[must_use]
pub fn to_tunes(&self) -> Phaser {
Phaser::new(self.rate, self.depth, self.feedback, self.stages, self.mix)
}
}
impl PulseEq {
#[must_use]
pub fn to_tunes(&self) -> EQ {
EQ::new(
self.low_gain,
self.mid_gain,
self.high_gain,
self.low_freq,
self.high_freq,
)
}
}
impl PulseEffect {
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Delay(_) => "delay",
Self::Reverb(_) => "reverb",
Self::Filter(_) => "filter",
Self::Compressor(_) => "compressor",
Self::Limiter(_) => "limiter",
Self::Chorus(_) => "chorus",
Self::Phaser(_) => "phaser",
Self::Eq(_) => "eq",
}
}
#[must_use]
pub fn allowed_on_master(&self) -> bool {
!matches!(self, Self::Filter(_))
}
#[must_use]
pub fn apply_to_track_builder<'a>(&self, builder: TrackBuilder<'a>) -> TrackBuilder<'a> {
match self {
Self::Delay(value) => builder.delay(value.to_tunes()),
Self::Reverb(value) => builder.reverb(value.to_tunes()),
Self::Filter(value) => builder.filter(value.to_tunes()),
Self::Compressor(value) => builder.compressor(value.to_tunes()),
Self::Limiter(value) => builder.limiter(value.to_tunes()),
Self::Chorus(value) => builder.chorus(value.to_tunes()),
Self::Phaser(value) => builder.phaser(value.to_tunes()),
Self::Eq(value) => builder.eq(value.to_tunes()),
}
}
pub(crate) fn apply_to_track(&self, track: &mut Track) {
match self {
Self::Delay(value) => {
track.effects = track.effects.clone().with_delay(value.to_tunes())
}
Self::Reverb(value) => {
track.effects = track.effects.clone().with_reverb(value.to_tunes());
}
Self::Filter(value) => {
track.filter = value.to_tunes();
}
Self::Compressor(value) => {
track.effects = track.effects.clone().with_compressor(value.to_tunes());
}
Self::Limiter(value) => {
track.effects = track.effects.clone().with_limiter(value.to_tunes());
}
Self::Chorus(value) => {
track.effects = track.effects.clone().with_chorus(value.to_tunes());
}
Self::Phaser(value) => {
track.effects = track.effects.clone().with_phaser(value.to_tunes());
}
Self::Eq(value) => {
track.effects = track.effects.clone().with_eq(value.to_tunes());
}
}
}
pub fn apply_to_master(&self, mixer: &mut Mixer) -> PulseResult<()> {
match self {
Self::Delay(value) => mixer.master_delay(value.to_tunes()),
Self::Reverb(value) => mixer.master_reverb(value.to_tunes()),
Self::Filter(_) => {
return Err(PulseError::InvalidEffectScope {
effect: "filter".to_string(),
scope: "master".to_string(),
});
}
Self::Compressor(value) => mixer.master_compressor(value.to_tunes()),
Self::Limiter(value) => mixer.master_limiter(value.to_tunes()),
Self::Chorus(value) => mixer.master_chorus(value.to_tunes()),
Self::Phaser(value) => mixer.master_phaser(value.to_tunes()),
Self::Eq(value) => mixer.master_eq(value.to_tunes()),
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
fn params(entries: &[(&str, EffectOption)]) -> EffectOptions {
let mut values = BTreeMap::new();
for (key, value) in entries {
values.insert((*key).to_string(), value.clone());
}
EffectOptions::Params(values)
}
#[test]
fn effect_catalog_exposes_v1_effects() {
let names = effect_names();
assert_eq!(names.len(), 8);
assert!(names.contains(&"delay"));
assert!(names.contains(&"reverb"));
assert!(names.contains(&"filter"));
assert!(names.contains(&"compressor"));
assert!(names.contains(&"limiter"));
assert!(names.contains(&"chorus"));
assert!(names.contains(&"phaser"));
assert!(names.contains(&"eq"));
let delay = effect_info("delay").expect("delay info should exist");
assert_eq!(delay.category, "time");
assert!(delay.scopes.contains(&"track"));
assert!(delay.scopes.contains(&"master"));
assert!(delay.parameters.contains(&"time"));
assert!(delay.presets.contains(&"quarter_note"));
let filter = effect_info("filter").expect("filter info should exist");
assert_eq!(filter.scopes, &["track"]);
}
#[test]
fn invalid_effect_name_reports_stable_error() {
let error = effect_from_options("shimmer", EffectOptions::Default)
.expect_err("unknown effect should fail");
assert_eq!(error.to_string(), "invalid effect: shimmer");
}
#[test]
fn delay_validates_params_and_presets() {
let effect = effect_from_options(
"delay",
params(&[
("time", EffectOption::Number(0.25)),
("feedback", EffectOption::Number(0.35)),
("mix", EffectOption::Number(0.3)),
]),
)
.expect("valid delay should parse");
let PulseEffect::Delay(delay) = effect else {
panic!("expected delay");
};
assert_eq!(delay.time, 0.25);
assert_eq!(delay.feedback, 0.35);
assert_eq!(delay.mix, 0.3);
let preset = effect_from_options("delay", EffectOptions::Preset("slapback".to_string()))
.expect("delay preset should parse");
assert!(matches!(preset, PulseEffect::Delay(_)));
let invalid =
effect_from_options("delay", params(&[("feedback", EffectOption::Number(1.2))]))
.expect_err("feedback outside range should fail");
assert_eq!(
invalid.to_string(),
"invalid effect option delay.feedback: 1.2"
);
}
#[test]
fn filter_validates_type_cutoff_resonance_and_scope() {
let effect = effect_from_options(
"filter",
params(&[
("type", EffectOption::Text("low_pass".to_string())),
("cutoff", EffectOption::Number(1800.0)),
("resonance", EffectOption::Number(0.4)),
("slope", EffectOption::Integer(24)),
]),
)
.expect("valid filter should parse");
let PulseEffect::Filter(filter) = effect else {
panic!("expected filter");
};
assert_eq!(filter.kind, PulseFilterKind::LowPass);
assert_eq!(filter.cutoff, 1800.0);
assert_eq!(filter.resonance, 0.4);
assert_eq!(filter.slope, PulseFilterSlope::Pole24);
assert!(!PulseEffect::Filter(filter).allowed_on_master());
let invalid = effect_from_options(
"filter",
params(&[("type", EffectOption::Text("comb".to_string()))]),
)
.expect_err("unknown filter type should fail");
assert_eq!(
invalid.to_string(),
"invalid effect option filter.type: comb"
);
}
#[test]
fn eq_rejects_invalid_frequency_order() {
let invalid = effect_from_options(
"eq",
params(&[
("low_freq", EffectOption::Number(5000.0)),
("high_freq", EffectOption::Number(200.0)),
]),
)
.expect_err("high frequency must be above low frequency");
assert_eq!(
invalid.to_string(),
"invalid effect option eq.high_freq: 200"
);
}
#[test]
fn all_default_effects_convert_to_tunes_types() {
let effects = [
effect_from_options("delay", EffectOptions::Default).unwrap(),
effect_from_options("reverb", EffectOptions::Default).unwrap(),
effect_from_options("filter", EffectOptions::Default).unwrap(),
effect_from_options("compressor", EffectOptions::Default).unwrap(),
effect_from_options("limiter", EffectOptions::Default).unwrap(),
effect_from_options("chorus", EffectOptions::Default).unwrap(),
effect_from_options("phaser", EffectOptions::Default).unwrap(),
effect_from_options("eq", EffectOptions::Default).unwrap(),
];
for effect in effects {
match effect {
PulseEffect::Delay(value) => {
let _ = value.to_tunes();
}
PulseEffect::Reverb(value) => {
let _ = value.to_tunes();
}
PulseEffect::Filter(value) => {
let _ = value.to_tunes();
}
PulseEffect::Compressor(value) => {
let _ = value.to_tunes();
}
PulseEffect::Limiter(value) => {
let _ = value.to_tunes();
}
PulseEffect::Chorus(value) => {
let _ = value.to_tunes();
}
PulseEffect::Phaser(value) => {
let _ = value.to_tunes();
}
PulseEffect::Eq(value) => {
let _ = value.to_tunes();
}
}
}
}
}