diff --git a/Cargo.lock b/Cargo.lock index a2676fe..d1a4b6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2563,7 +2563,7 @@ dependencies = [ [[package]] name = "maolan-plugins" -version = "0.1.3" +version = "0.1.4" dependencies = [ "base64", "dirs", @@ -2578,8 +2578,6 @@ dependencies = [ "oxideav-basic", "oxideav-core", "oxideav-flac", - "oxideav-mp3", - "oxideav-ogg", "parking_lot 0.13.0", "pitch-detection", "portable-atomic", @@ -3400,24 +3398,6 @@ dependencies = [ "oxideav-core", ] -[[package]] -name = "oxideav-mp3" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8583bbbe119a19c0b88ee5b0f76c90493395350878e7c770ce6402287b164477" -dependencies = [ - "oxideav-core", -] - -[[package]] -name = "oxideav-ogg" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f64e5e8b7ca99ce6e7ad4cab064cd554d1c4c1161eea015cbcb12b9c5ced0d" -dependencies = [ - "oxideav-core", -] - [[package]] name = "parking" version = "2.2.1" diff --git a/Cargo.toml b/Cargo.toml index cc46304..3f921ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "maolan-plugins" -version = "0.1.3" +version = "0.1.4" edition = "2024" description = "Audio plugins for the Maolan ecosystem" license = "BSD-2-Clause" @@ -22,8 +22,6 @@ dirs = "7.0.0" oxideav-core = "0.1.42" oxideav-basic = "0.0.11" oxideav-flac = "0.0.12" -oxideav-mp3 = "0.1.3" -oxideav-ogg = "0.1.9" pitch-detection = "0.3.0" symphonia = { version = "0.6.1", features = ["aac", "alac", "flac", "isomp4", "mp3", "ogg", "vorbis", "wav"] } maolan-baseview = { version = "0.1.1", features = ["canvas"] } diff --git a/src/chorus/dsp.rs b/src/chorus/dsp.rs index d9740f1..b799483 100644 --- a/src/chorus/dsp.rs +++ b/src/chorus/dsp.rs @@ -40,8 +40,8 @@ impl Chorus { self.buf_r.fill(0.0); self.write_pos = 0; self.phases = [0.0; MAX_VOICES]; - for i in 0..MAX_VOICES { - self.phases[i] = TWO_PI * i as f32 / MAX_VOICES as f32; + for (i, phase) in self.phases.iter_mut().enumerate() { + *phase = TWO_PI * i as f32 / MAX_VOICES as f32; } } diff --git a/src/common/bus.rs b/src/common/bus.rs index 3e93a4e..58dfbe5 100644 --- a/src/common/bus.rs +++ b/src/common/bus.rs @@ -556,8 +556,8 @@ mod tests { valid_bins: 512, ..Default::default() }; - for i in 0..512 { - fft.bins[i] = i as f32; + for (i, bin) in fft.bins.iter_mut().enumerate().take(512) { + *bin = i as f32; } let data = PluginSharedData::new(PluginType::Eq).with_fft(fft); diff --git a/src/common/fft.rs b/src/common/fft.rs index 43db2f9..b7db636 100644 --- a/src/common/fft.rs +++ b/src/common/fft.rs @@ -28,9 +28,9 @@ impl SpectrumAnalyzer { self.complex_buf[i].re = s; self.complex_buf[i].im = 0.0; } - for i in input.len()..n { - self.complex_buf[i].re = 0.0; - self.complex_buf[i].im = 0.0; + for complex in self.complex_buf.iter_mut().take(n).skip(input.len()) { + complex.re = 0.0; + complex.im = 0.0; } let scratch_len = fft.get_inplace_scratch_len(); diff --git a/src/common/modulated_oscillator.rs b/src/common/modulated_oscillator.rs index b1017a4..949009d 100644 --- a/src/common/modulated_oscillator.rs +++ b/src/common/modulated_oscillator.rs @@ -18,46 +18,6 @@ impl FreqEnvMode { } } -#[cfg(test)] -mod tests { - use super::*; - use crate::common::envelope::EnvPoint; - - #[test] - fn changing_base_frequency_preserves_linear_freq_env_hz() { - let mut oscillator = ModulatedOscillator::new(48_000.0); - oscillator.set_base_freq_hz(1000.0); - oscillator.set_freq_env(Some(Envelope::new(vec![ - EnvPoint::new(0.0, 0.5), - EnvPoint::new(1.0, 1.0), - ]))); - - oscillator.set_base_freq_hz_preserving_freq_env(2000.0); - let points = oscillator.freq_env().expect("frequency envelope").points(); - - assert_eq!(points[0].v, 0.25); - assert_eq!(points[1].v, 0.5); - } - - #[test] - fn centered_stereo_oscillator_sums_to_mono_without_level_loss() { - let mut oscillator = ModulatedOscillator::new(48_000.0); - oscillator.set_base_freq_hz(100.0); - oscillator.set_amplitude(1.0); - let mut out = vec![0.0; 480]; - - oscillator.render(&mut out, 480, None); - - let peak = out - .iter() - .fold(0.0f32, |peak, sample| peak.max(sample.abs())); - assert!( - peak > 0.9, - "mono summed oscillator should be near full scale, got {peak}" - ); - } -} - #[derive(Debug, Clone)] pub struct ModulatedOscillator { oscillator: Oscillator, @@ -497,3 +457,43 @@ impl ModulatedOscillator { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::envelope::EnvPoint; + + #[test] + fn changing_base_frequency_preserves_linear_freq_env_hz() { + let mut oscillator = ModulatedOscillator::new(48_000.0); + oscillator.set_base_freq_hz(1000.0); + oscillator.set_freq_env(Some(Envelope::new(vec![ + EnvPoint::new(0.0, 0.5), + EnvPoint::new(1.0, 1.0), + ]))); + + oscillator.set_base_freq_hz_preserving_freq_env(2000.0); + let points = oscillator.freq_env().expect("frequency envelope").points(); + + assert_eq!(points[0].v, 0.25); + assert_eq!(points[1].v, 0.5); + } + + #[test] + fn centered_stereo_oscillator_sums_to_mono_without_level_loss() { + let mut oscillator = ModulatedOscillator::new(48_000.0); + oscillator.set_base_freq_hz(100.0); + oscillator.set_amplitude(1.0); + let mut out = vec![0.0; 480]; + + oscillator.render(&mut out, 480, None); + + let peak = out + .iter() + .fold(0.0f32, |peak, sample| peak.max(sample.abs())); + assert!( + peak > 0.9, + "mono summed oscillator should be near full scale, got {peak}" + ); + } +} diff --git a/src/common/oscillator.rs b/src/common/oscillator.rs index a4e4011..fde9e68 100644 --- a/src/common/oscillator.rs +++ b/src/common/oscillator.rs @@ -1280,147 +1280,6 @@ impl SineOsc { } } -#[cfg(test)] -mod tests { - use super::{ClassicOsc, ClassicWaveform, OscType, Oscillator, SineOsc}; - use crate::common::wavetable::Wavetable; - use std::sync::Arc; - - fn sine_wavetable() -> Wavetable { - let size = 2048; - let table: Vec = (0..size) - .map(|i| (2.0 * std::f32::consts::PI * i as f32 / size as f32).sin()) - .collect(); - let mut wt = Wavetable { - size, - n_tables: 1, - size_po2: size.trailing_zeros() as usize, - frames: vec![table], - ..Default::default() - }; - wt.build_mipmaps(); - wt - } - - #[test] - fn sine_default_filters_do_not_shift_initial_cycles() { - let sample_rate = 4096.0f32; - let freq_hz = 100.0f32; - let period = (sample_rate / freq_hz).round() as usize; - let mut osc = SineOsc::new(sample_rate); - osc.set_freq_hz(freq_hz); - - let mut samples = Vec::with_capacity(period * 3); - for _ in 0..samples.capacity() { - samples.push(osc.next(0.0).0); - } - - for cycle in 0..3 { - let start = cycle * period; - let end = start + period; - let mean = samples[start..end].iter().sum::() / period as f32; - assert!( - mean.abs() < 0.02, - "cycle {cycle} has unexpected DC shift: {mean}" - ); - } - } - - #[test] - fn classic_sine_waveform_tracks_frequency() { - let sample_rate = 48_000.0f32; - let freq_hz = 100.0f32; - let period = (sample_rate / freq_hz).round() as usize; - let mut osc = ClassicOsc::new(sample_rate); - osc.set_waveform(ClassicWaveform::Sine); - osc.set_freq_hz(freq_hz); - osc.reset_to_zero(); - - for i in 0..period * 2 { - let expected = (2.0 * std::f32::consts::PI * i as f32 / period as f32).sin(); - let actual = osc.next_mono(0.0); - assert!( - (actual - expected).abs() < 2e-4, - "sample {i}: expected {expected}, got {actual}" - ); - } - } - - #[test] - fn stereo_filters_persist_across_sample_rate_change() { - for ty in [OscType::Sine, OscType::Window] { - let mut osc = Oscillator::new(ty, 48000.0); - osc.set_freq_hz(440.0); - osc.set_unison(3, 0.5); - match &mut osc { - Oscillator::Sine(o) => { - o.set_lowcut(150.0); - o.set_highcut(6000.0); - } - Oscillator::Window(o) => { - o.set_lowcut(150.0); - o.set_highcut(6000.0); - } - _ => unreachable!(), - } - if ty == OscType::Window { - osc.set_wavetable(Some(Arc::new(sine_wavetable()))); - } - - osc.set_sample_rate(96000.0); - - let mut channels_differ = false; - for _ in 0..1024 { - let (l, r) = osc.next(0.0, 0.0, 0.0); - assert!(l.is_finite() && r.is_finite(), "{ty:?} non-finite output"); - if (l - r).abs() > 1e-6 { - channels_differ = true; - } - } - assert!(channels_differ, "{ty:?} L/R channels should differ"); - } - } - - #[test] - fn set_sample_rate_keeps_every_variant_finite() { - for ty in [ - OscType::Off, - OscType::Classic, - OscType::Sine, - OscType::Fm2, - OscType::Fm3, - OscType::Wavetable, - OscType::Window, - OscType::ShNoise, - OscType::String, - OscType::Alias, - OscType::Twist, - OscType::AudioInput, - OscType::Sample, - ] { - let mut osc = Oscillator::new(ty, 48000.0); - osc.set_freq_hz(220.0); - osc.set_sample_rate(44100.0); - for _ in 0..256 { - let (l, r) = osc.next(0.0, 0.0, 0.0); - assert!( - l.is_finite() && r.is_finite(), - "{ty:?} non-finite output after set_sample_rate" - ); - } - } - } - - #[test] - fn off_oscillator_is_silent() { - let mut osc = Oscillator::new(OscType::Off, 48_000.0); - assert_eq!(osc.osc_type(), OscType::Off); - for _ in 0..256 { - assert_eq!(osc.next(1.0, 1.0, 1.0), (0.0, 0.0)); - } - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Fm2FeedbackMode { Classic = 0, @@ -3999,3 +3858,144 @@ impl Oscillator { } } } + +#[cfg(test)] +mod tests { + use super::{ClassicOsc, ClassicWaveform, OscType, Oscillator, SineOsc}; + use crate::common::wavetable::Wavetable; + use std::sync::Arc; + + fn sine_wavetable() -> Wavetable { + let size = 2048; + let table: Vec = (0..size) + .map(|i| (2.0 * std::f32::consts::PI * i as f32 / size as f32).sin()) + .collect(); + let mut wt = Wavetable { + size, + n_tables: 1, + size_po2: size.trailing_zeros() as usize, + frames: vec![table], + ..Default::default() + }; + wt.build_mipmaps(); + wt + } + + #[test] + fn sine_default_filters_do_not_shift_initial_cycles() { + let sample_rate = 4096.0f32; + let freq_hz = 100.0f32; + let period = (sample_rate / freq_hz).round() as usize; + let mut osc = SineOsc::new(sample_rate); + osc.set_freq_hz(freq_hz); + + let mut samples = Vec::with_capacity(period * 3); + for _ in 0..samples.capacity() { + samples.push(osc.next(0.0).0); + } + + for cycle in 0..3 { + let start = cycle * period; + let end = start + period; + let mean = samples[start..end].iter().sum::() / period as f32; + assert!( + mean.abs() < 0.02, + "cycle {cycle} has unexpected DC shift: {mean}" + ); + } + } + + #[test] + fn classic_sine_waveform_tracks_frequency() { + let sample_rate = 48_000.0f32; + let freq_hz = 100.0f32; + let period = (sample_rate / freq_hz).round() as usize; + let mut osc = ClassicOsc::new(sample_rate); + osc.set_waveform(ClassicWaveform::Sine); + osc.set_freq_hz(freq_hz); + osc.reset_to_zero(); + + for i in 0..period * 2 { + let expected = (2.0 * std::f32::consts::PI * i as f32 / period as f32).sin(); + let actual = osc.next_mono(0.0); + assert!( + (actual - expected).abs() < 2e-4, + "sample {i}: expected {expected}, got {actual}" + ); + } + } + + #[test] + fn stereo_filters_persist_across_sample_rate_change() { + for ty in [OscType::Sine, OscType::Window] { + let mut osc = Oscillator::new(ty, 48000.0); + osc.set_freq_hz(440.0); + osc.set_unison(3, 0.5); + match &mut osc { + Oscillator::Sine(o) => { + o.set_lowcut(150.0); + o.set_highcut(6000.0); + } + Oscillator::Window(o) => { + o.set_lowcut(150.0); + o.set_highcut(6000.0); + } + _ => unreachable!(), + } + if ty == OscType::Window { + osc.set_wavetable(Some(Arc::new(sine_wavetable()))); + } + + osc.set_sample_rate(96000.0); + + let mut channels_differ = false; + for _ in 0..1024 { + let (l, r) = osc.next(0.0, 0.0, 0.0); + assert!(l.is_finite() && r.is_finite(), "{ty:?} non-finite output"); + if (l - r).abs() > 1e-6 { + channels_differ = true; + } + } + assert!(channels_differ, "{ty:?} L/R channels should differ"); + } + } + + #[test] + fn set_sample_rate_keeps_every_variant_finite() { + for ty in [ + OscType::Off, + OscType::Classic, + OscType::Sine, + OscType::Fm2, + OscType::Fm3, + OscType::Wavetable, + OscType::Window, + OscType::ShNoise, + OscType::String, + OscType::Alias, + OscType::Twist, + OscType::AudioInput, + OscType::Sample, + ] { + let mut osc = Oscillator::new(ty, 48000.0); + osc.set_freq_hz(220.0); + osc.set_sample_rate(44100.0); + for _ in 0..256 { + let (l, r) = osc.next(0.0, 0.0, 0.0); + assert!( + l.is_finite() && r.is_finite(), + "{ty:?} non-finite output after set_sample_rate" + ); + } + } + } + + #[test] + fn off_oscillator_is_silent() { + let mut osc = Oscillator::new(OscType::Off, 48_000.0); + assert_eq!(osc.osc_type(), OscType::Off); + for _ in 0..256 { + assert_eq!(osc.next(1.0, 1.0, 1.0), (0.0, 0.0)); + } + } +} diff --git a/src/common/spectrum.rs b/src/common/spectrum.rs index cf5c906..9d70ff6 100644 --- a/src/common/spectrum.rs +++ b/src/common/spectrum.rs @@ -1013,8 +1013,8 @@ impl LogSpectrumAnalyzer { k_hi = k_hi.clamp(k_lo + 1, fft_bins); let mut power_sum = 0.0; - for k in k_lo..k_hi { - power_sum += self.powers[k]; + for &power in self.powers.iter().take(k_hi).skip(k_lo) { + power_sum += power; } *display_power = power_sum; } @@ -1023,10 +1023,16 @@ impl LogSpectrumAnalyzer { let start = i.saturating_sub(2); let end = (i + 2).min(self.bins - 1); let mut weighted_power = 0.0; - for j in start..=end { + for (j, &power) in self + .display_power + .iter() + .enumerate() + .take(end + 1) + .skip(start) + { let distance = i.abs_diff(j) as f32; let weight = 3.0 - distance; - weighted_power += self.display_power[j] * weight; + weighted_power += power * weight; } let power = weighted_power / 3.0; let db = if power > 1.0e-14 { diff --git a/src/common/twist.rs b/src/common/twist.rs index f3d6c53..70dd4a8 100644 --- a/src/common/twist.rs +++ b/src/common/twist.rs @@ -460,15 +460,15 @@ impl TwistOsc { let fm = fm_input * 0.02; let mut out = 0.0f32; - for i in 0..num_harm { + for (i, phase) in self.har_phases.iter_mut().enumerate().take(num_harm) { let n = (i + 1) as f32; let harm_inc = base_inc * n * (1.0 + inharm * n * n); - self.har_phases[i] += harm_inc + fm * harm_inc; - if self.har_phases[i] >= 1.0 { - self.har_phases[i] -= 1.0; + *phase += harm_inc + fm * harm_inc; + if *phase >= 1.0 { + *phase -= 1.0; } let amp = (1.0 - tilt).powf(n - 1.0) * (1.0 / n).sqrt(); - out += (self.har_phases[i] * 2.0 * PI).sin() * amp; + out += (*phase * 2.0 * PI).sin() * amp; } let norm = 1.0 / (num_harm as f32).sqrt(); @@ -752,8 +752,8 @@ impl TwistOsc { self.inh_string_exciter = 1.0; let burst_len = (self.inh_string_buffer.len() as f32 * 0.3) as usize; - for i in 0..burst_len { - self.inh_string_buffer[i] = fast_rand() * 2.0 - 1.0; + for sample in self.inh_string_buffer.iter_mut().take(burst_len) { + *sample = fast_rand() * 2.0 - 1.0; } } self.inh_string_exciter *= 0.999; diff --git a/src/common/wavetable.rs b/src/common/wavetable.rs index 4c8c811..d10ed96 100644 --- a/src/common/wavetable.rs +++ b/src/common/wavetable.rs @@ -543,11 +543,13 @@ mod tests { .collect(); let mut wt = table_wavetable(size, table); wt.build_mipmaps(); - for level in 0..=wt.mipmaps.len().saturating_sub(4) { - let peak = wt.mipmaps[level][0] - .iter() - .map(|s| s.abs()) - .fold(0.0f32, f32::max); + for (level, mipmap) in wt + .mipmaps + .iter() + .enumerate() + .take(wt.mipmaps.len().saturating_sub(4) + 1) + { + let peak = mipmap[0].iter().map(|s| s.abs()).fold(0.0f32, f32::max); assert!((peak - 1.0).abs() < 1.0e-3, "level {level} peak {peak}"); } } diff --git a/src/compressor/gui.rs b/src/compressor/gui.rs index 65adc62..2f4a57e 100644 --- a/src/compressor/gui.rs +++ b/src/compressor/gui.rs @@ -452,9 +452,9 @@ fn update(state: &mut State, message: Message) -> Task { if let Some(slot) = peer.bands_slot() && slot.read(&mut bands) { - for i in 0..bands.len.min(bands.bands.len()) { - if bands.bands[i].on { - freqs.push(bands.bands[i].freq); + for band in bands.bands.iter().take(bands.len.min(bands.bands.len())) { + if band.on { + freqs.push(band.freq); } } } @@ -469,9 +469,12 @@ fn update(state: &mut State, message: Message) -> Task { let mut gr = bus::CompressorGrData::default(); if slot.read(&mut gr) { state.gain_reduction_db.clear(); - for i in 0..gr.valid_bands.min(gr.gr_db.len()) { - state.gain_reduction_db.push(gr.gr_db[i]); - } + state.gain_reduction_db.extend( + gr.gr_db + .iter() + .take(gr.valid_bands.min(gr.gr_db.len())) + .copied(), + ); } } diff --git a/src/delay/dsp.rs b/src/delay/dsp.rs index 5c0fdd7..eee382f 100644 --- a/src/delay/dsp.rs +++ b/src/delay/dsp.rs @@ -18,6 +18,7 @@ pub struct Delay { } pub struct DelayParams { + pub delay_mode: f64, pub time_mode: f64, pub time_ms: f64, pub time_note: f64, @@ -108,6 +109,7 @@ impl Delay { let fb = params.feedback.clamp(0.0, 1.0) as f32; let wet = params.dry_wet.clamp(0.0, 1.0) as f32; let dry = 1.0 - wet; + let ping_pong = params.delay_mode >= 0.5; let max_len = self.buf_l.len(); let frames = left.len().min(right.len()); @@ -129,8 +131,13 @@ impl Delay { self.temp_dry_r[i] = r; self.temp_wet_l[i] = delayed_l; self.temp_wet_r[i] = delayed_r; - self.buf_l[wp] = l + delayed_l * fb; - self.buf_r[wp] = r + delayed_r * fb; + let (feedback_l, feedback_r) = if ping_pong { + (delayed_r, delayed_l) + } else { + (delayed_l, delayed_r) + }; + self.buf_l[wp] = l + feedback_l * fb; + self.buf_r[wp] = r + feedback_r * fb; self.write_pos = if wp + 1 >= max_len { 0 } else { wp + 1 }; self.chase += (self.current_delay - target).abs(); if self.chase > CHASE_THRESHOLD { diff --git a/src/delay/gui.rs b/src/delay/gui.rs index 519910c..1dff376 100644 --- a/src/delay/gui.rs +++ b/src/delay/gui.rs @@ -263,9 +263,16 @@ fn view(state: &State) -> Element<'_, Message> { }) }); + let delay_mode_value = state.shared.params.get(ParamId::DelayMode); + let delay_mode_selected = usize::from(delay_mode_value >= 0.5); + let delay_mode_toggle = + horizontal_multi_toggler(["Normal", "Ping Pong"], delay_mode_selected, |index| { + Message::SetBoolParam(ParamId::DelayMode, index >= 1) + }); + let time_mode_value = state.shared.params.get(ParamId::TimeMode) as f32; let time_mode_switch = row![ - text("Mode").size(14), + text("Time").size(14), text("ms").size(13), toggler(time_mode_value >= 0.5).on_toggle(|v| Message::SetBoolParam(ParamId::TimeMode, v)), text("Note").size(13), @@ -275,9 +282,10 @@ fn view(state: &State) -> Element<'_, Message> { let content = column![ preset::bar(&state.preset_ui), + row![channels_toggle, delay_mode_toggle, time_mode_switch,] + .spacing(8) + .align_y(Alignment::Center), row![ - channels_toggle, - time_mode_switch, time_knob(state), knob(ParamId::Feedback, "Feedback", state), knob(ParamId::DryWet, "Dry/Wet", state), diff --git a/src/delay/params.rs b/src/delay/params.rs index ba0a3a7..4e6a0e9 100644 --- a/src/delay/params.rs +++ b/src/delay/params.rs @@ -10,6 +10,7 @@ crate::define_params! { Feedback = 3, DryWet = 4, Channels = 5, + DelayMode = 6, } pub const PARAMS: [ParamDef] = [ { @@ -72,6 +73,16 @@ crate::define_params! { step: 1.0, flags: STEPPED_BOOL, } + { + id: ParamId::DelayMode, + name: "Mode", + module: "Delay", + min: 0.0, + max: 1.0, + default: 0.0, + step: 1.0, + flags: STEPPED_BOOL, + } ]; } diff --git a/src/delay/plugin.rs b/src/delay/plugin.rs index c4b5a64..f6220f2 100644 --- a/src/delay/plugin.rs +++ b/src/delay/plugin.rs @@ -214,6 +214,7 @@ impl AudioProcessor { &mut self.temp_left[..frames], &mut self.temp_right[..frames], &crate::delay::dsp::DelayParams { + delay_mode: shared.params.get(ParamId::DelayMode), time_mode: shared.params.get(ParamId::TimeMode), time_ms: shared.params.get(ParamId::TimeMs), time_note: shared.params.get(ParamId::TimeNote), @@ -240,6 +241,7 @@ impl AudioProcessor { &mut self.temp_left[..frames], &mut self.temp_right[..frames], &crate::delay::dsp::DelayParams { + delay_mode: shared.params.get(ParamId::DelayMode), time_mode: shared.params.get(ParamId::TimeMode), time_ms: shared.params.get(ParamId::TimeMs), time_note: shared.params.get(ParamId::TimeNote), @@ -278,6 +280,13 @@ impl AudioProcessor { fn param_text(id: ParamId, value: f64) -> String { use crate::delay::params::NOTE_DIVISIONS; match id { + ParamId::DelayMode => { + if value >= 0.5 { + "Ping Pong".to_string() + } else { + "Normal".to_string() + } + } ParamId::TimeMode => { if value >= 0.5 { "Note".to_string() @@ -305,6 +314,11 @@ fn param_text(id: ParamId, value: f64) -> String { fn parse_param_text(id: ParamId, text: &str) -> Option { match id { + ParamId::DelayMode => match text.trim().to_ascii_lowercase().as_str() { + "normal" => Some(0.0), + "ping pong" | "ping-pong" | "pingpong" => Some(1.0), + _ => text.parse().ok(), + }, ParamId::TimeMode => match text.trim() { "ms" | "MS" | "Ms" => Some(0.0), "note" | "Note" | "NOTE" => Some(1.0), diff --git a/src/eq/dsp.rs b/src/eq/dsp.rs index 4fd93a2..1a4ea76 100644 --- a/src/eq/dsp.rs +++ b/src/eq/dsp.rs @@ -781,12 +781,12 @@ impl ParametricEqualizer { } crate::simd::mul_inplace(buffer, self.input_gain_lin); let ctx = self.ctx(); - for i in 0..MAX_BANDS { - if !self.bands[i].on { + for band in self.bands.iter_mut().take(MAX_BANDS) { + if !band.on { continue; } let sc_pair = sidechain.map(|s| (s, s)); - self.bands[i].tick_dynamics_only(&ctx, buffer, buffer, sc_pair); + band.tick_dynamics_only(&ctx, buffer, buffer, sc_pair); } } @@ -1022,11 +1022,11 @@ impl ParametricEqualizer { 0.0 }; let ctx = self.ctx(); - for i in 0..MAX_BANDS { - if !self.bands[i].on || Some(i) == skip { + for (i, band) in self.bands.iter_mut().enumerate().take(MAX_BANDS) { + if !band.on || Some(i) == skip { continue; } - self.bands[i].process_mono(&ctx, buffer, sidechain); + band.process_mono(&ctx, buffer, sidechain); } let comp = self.auto_gain_comp(in_rms, block_rms_mono(buffer), buffer.len()); let polarity = if self.phase_invert { -1.0 } else { 1.0 }; diff --git a/src/eq/spectral.rs b/src/eq/spectral.rs index c734fed..da517e0 100644 --- a/src/eq/spectral.rs +++ b/src/eq/spectral.rs @@ -444,15 +444,20 @@ impl SpectralDynamics { + 0.25 * total_gain_db[bin + 1]; } - for ch in 0..channels { - for (bin, spec) in self.specs[ch].iter_mut().enumerate().take(NUM_BINS) { + for channel in self.specs.iter_mut().take(channels) { + for (bin, spec) in channel.iter_mut().enumerate().take(NUM_BINS) { let g = dsp::db_to_gain(smoothed[bin].clamp(-60.0, 36.0)); *spec *= g; } - for bin in NUM_BINS..SPECTRAL_FFT_SIZE { + for (bin, spec) in channel + .iter_mut() + .enumerate() + .take(SPECTRAL_FFT_SIZE) + .skip(NUM_BINS) + { let mirrored = SPECTRAL_FFT_SIZE - bin; let g = dsp::db_to_gain(smoothed[mirrored].clamp(-60.0, 36.0)); - self.specs[ch][bin] *= g; + *spec *= g; } } diff --git a/src/kick/gui/mod.rs b/src/kick/gui/mod.rs index dc74ffa..dac2a36 100644 --- a/src/kick/gui/mod.rs +++ b/src/kick/gui/mod.rs @@ -1583,21 +1583,6 @@ fn render_source_column( .into() } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn frequency_knob_log_scale_roundtrips_and_favors_low_range() { - let min = 1000.0; - let max = 20000.0; - let midpoint_freq = log_knob_to_freq(0.5, min, max); - - assert!(midpoint_freq < (min + max) * 0.5); - assert!((freq_to_log_knob(midpoint_freq, min, max) - 0.5).abs() < 1.0e-6); - } -} - struct PreviewLayerParams { render: [[bool; 3]; 3], enabled: [bool; 3], @@ -1806,3 +1791,18 @@ impl GuiBridge { (EDITOR_WIDTH, EDITOR_HEIGHT) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frequency_knob_log_scale_roundtrips_and_favors_low_range() { + let min = 1000.0; + let max = 20000.0; + let midpoint_freq = log_knob_to_freq(0.5, min, max); + + assert!(midpoint_freq < (min + max) * 0.5); + assert!((freq_to_log_knob(midpoint_freq, min, max) - 0.5).abs() < 1.0e-6); + } +} diff --git a/src/lib.rs b/src/lib.rs index 36f464e..4818c87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![deny(dead_code)] +#![recursion_limit = "256"] use std::{ ffi::{CStr, c_char, c_void}, diff --git a/src/sampler/dsp/sfz.rs b/src/sampler/dsp/sfz.rs index 370911c..45ecc91 100644 --- a/src/sampler/dsp/sfz.rs +++ b/src/sampler/dsp/sfz.rs @@ -177,7 +177,7 @@ fn parse_note_name(value: &str) -> Option { return None; } let letter = letter.to_ascii_lowercase(); - if !matches!(letter, 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g') { + if !matches!(letter, 'a'..='g') { return None; } diff --git a/src/synth/dsp/voice.rs b/src/synth/dsp/voice.rs index fb4055b..65b0e88 100644 --- a/src/synth/dsp/voice.rs +++ b/src/synth/dsp/voice.rs @@ -3935,8 +3935,8 @@ fn freq_to_scale_degree(freq: f32, tuning: &Tuning) -> i32 { let cents_in_octave = cents - octave as f32 * octave_cents; let mut best_idx = 0usize; let mut best_diff = cents_in_octave.abs(); - for i in 1..tuning.degrees.len() { - let diff = (cents_in_octave - tuning.degrees[i]).abs(); + for (i, °ree) in tuning.degrees.iter().enumerate().skip(1) { + let diff = (cents_in_octave - degree).abs(); if diff < best_diff { best_diff = diff; best_idx = i; diff --git a/src/synth/plugin.rs b/src/synth/plugin.rs index 323c01d..a5010f3 100644 --- a/src/synth/plugin.rs +++ b/src/synth/plugin.rs @@ -2861,8 +2861,8 @@ impl AudioProcessor { } // Pick up custom wavetables loaded by the GUI (off the audio thread). - for osc_index in 0..3 { - let wavetable = shared.custom_wavetables[osc_index].lock().take(); + for (osc_index, custom_wavetable) in shared.custom_wavetables.iter().enumerate() { + let wavetable = custom_wavetable.lock().take(); if let Some(wavetable) = wavetable { self.engine.set_wavetable(osc_index, Some(wavetable)); } diff --git a/tests/fx_dsp_smoke.rs b/tests/fx_dsp_smoke.rs index 495963b..b62fb83 100644 --- a/tests/fx_dsp_smoke.rs +++ b/tests/fx_dsp_smoke.rs @@ -149,6 +149,7 @@ fn delay_smoke() { let mut dsp = Delay::default(); dsp.set_sample_rate(SR); let p = DelayParams { + delay_mode: 0.0, time_mode: 0.0, time_ms: 250.0, time_note: 0.0, @@ -159,6 +160,40 @@ fn delay_smoke() { smoke(|l, r| dsp.process_stereo(l, r, &p)); } +#[test] +fn delay_ping_pong_alternates_feedback_between_channels() { + let params = |delay_mode| DelayParams { + delay_mode, + time_mode: 0.0, + time_ms: 1.0, + time_note: 0.0, + feedback: 0.5, + dry_wet: 1.0, + tempo: None, + }; + let mut normal = Delay::default(); + let mut ping_pong = Delay::default(); + normal.set_sample_rate(1000.0); + ping_pong.set_sample_rate(1000.0); + + let mut silence_l = [0.0; 10_000]; + let mut silence_r = [0.0; 10_000]; + normal.process_stereo(&mut silence_l, &mut silence_r, ¶ms(0.0)); + ping_pong.process_stereo(&mut silence_l, &mut silence_r, ¶ms(1.0)); + + let mut normal_l = [1.0, 0.0, 0.0, 0.0, 0.0]; + let mut normal_r = [0.0; 5]; + normal.process_stereo(&mut normal_l, &mut normal_r, ¶ms(0.0)); + assert_eq!(normal_l, [0.0, 1.0, 0.5, 0.25, 0.125]); + assert_eq!(normal_r, [0.0; 5]); + + let mut ping_pong_l = [1.0, 0.0, 0.0, 0.0, 0.0]; + let mut ping_pong_r = [0.0; 5]; + ping_pong.process_stereo(&mut ping_pong_l, &mut ping_pong_r, ¶ms(1.0)); + assert_eq!(ping_pong_l, [0.0, 1.0, 0.0, 0.25, 0.0]); + assert_eq!(ping_pong_r, [0.0, 0.0, 0.5, 0.0, 0.125]); +} + #[test] fn reverb_smoke() { let mut dsp = Reverb::default(); @@ -205,6 +240,7 @@ fn denormal_input_stays_finite_and_bounded() { let mut delay = Delay::default(); delay.set_sample_rate(SR); let dp = DelayParams { + delay_mode: 0.0, time_mode: 0.0, time_ms: 250.0, time_note: 0.0,