diff --git a/src/lib.rs b/src/lib.rs index 23c6e59..fb6472e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,43 +5,125 @@ use nih_plug_egui::{create_egui_editor, egui, widgets, EguiState}; use std::sync::Arc; // ────────────────────────────────────────────── -// Params — add your plugin parameters here. -// See musiikki/codename-206/src/params.rs for a real example. +// Params — time, feedback, mix // ────────────────────────────────────────────── +const MAX_DELAY_MS: f32 = 2_000.0; + #[derive(Params)] struct KolmekiviParams { /// Editor window size, persisted with the plugin state. #[persist = "editor-state"] editor_state: Arc, - /// Example parameter — replace with your own. The `id` is the stable key the host - /// stores automation against, so never change it once you've shipped. - #[id = "gain"] - gain: FloatParam, + /// Delay time in milliseconds. + #[id = "time"] + time: FloatParam, + + /// Feedback amount (0 = no feedback, 1 = oscillation). + #[id = "feedback"] + feedback: FloatParam, + + /// Wet/dry mix (0 = dry only, 1 = wet only). + #[id = "mix"] + mix: FloatParam, } impl Default for KolmekiviParams { fn default() -> Self { Self { editor_state: EguiState::from_size(320, 160), - gain: FloatParam::new( - "Gain", - util::db_to_gain(0.0), - FloatRange::Skewed { - min: util::db_to_gain(-30.0), - max: util::db_to_gain(30.0), - factor: FloatRange::gain_skew_factor(-30.0, 30.0), - }, + time: FloatParam::new( + "Time", + 500.0, + FloatRange::Linear { min: 0.0, max: MAX_DELAY_MS }, ) - .with_smoother(SmoothingStyle::Logarithmic(50.0)) - .with_unit(" dB") - .with_value_to_string(formatters::v2s_f32_gain_to_db(2)) - .with_string_to_value(formatters::s2v_f32_gain_to_db()), + .with_unit(" ms") + .with_value_to_string(formatters::v2s_f32_rounded(0)), + + feedback: FloatParam::new( + "Feedback", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ) + .with_smoother(SmoothingStyle::Logarithmic(32.0)), + + mix: FloatParam::new( + "Mix", + 0.5, + FloatRange::Linear { min: 0.0, max: 1.0 }, + ) + .with_smoother(SmoothingStyle::Logarithmic(32.0)), } } } +// ────────────────────────────────────────────── +// Stereo delay line +// ────────────────────────────────────────────── + +struct StereoDelayLine { + left: Vec, + right: Vec, + write_pos: usize, +} + +impl StereoDelayLine { + fn new(max_samples: usize) -> Self { + Self { + left: vec![0.0; max_samples], + right: vec![0.0; max_samples], + write_pos: 0, + } + } + + fn reset(&mut self) { + for sample in &mut self.left { + *sample = 0.0; + } + for sample in &mut self.right { + *sample = 0.0; + } + self.write_pos = 0; + } + + /// Process one sample frame for both channels. + /// Returns (output_left, output_right) as dry + wet mix. + fn process_frame( + &mut self, + input_left: f32, + input_right: f32, + delay_samples: usize, + feedback: f32, + mix: f32, + ) -> (f32, f32) { + let read_pos = if delay_samples >= self.left.len() { + 0 + } else { + self.write_pos.saturating_sub(delay_samples) + }; + + let delayed_left = self.left[read_pos]; + let delayed_right = self.right[read_pos]; + + // Feedback-mixed input goes into the delay line + let fb_input_left = (input_left + delayed_left * feedback).clamp(-1.0, 1.0); + let fb_input_right = (input_right + delayed_right * feedback).clamp(-1.0, 1.0); + + // Store into delay line + self.left[self.write_pos] = fb_input_left; + self.right[self.write_pos] = fb_input_right; + + // Output is dry signal blended with delayed (wet) signal + let out_left = input_left * (1.0 - mix) + delayed_left * mix; + let out_right = input_right * (1.0 - mix) + delayed_right * mix; + + self.write_pos = (self.write_pos + 1) % self.left.len(); + + (out_left, out_right) + } +} + // ────────────────────────────────────────────── // Plugin // ────────────────────────────────────────────── @@ -49,10 +131,11 @@ impl Default for KolmekiviParams { #[derive(Default)] struct Kolmekivi { params: Arc, + sample_rate: f32, + delay_line: Option, } impl Plugin for Kolmekivi { - // TODO: Replace these placeholder values with your plugin's metadata. const NAME: &'static str = "Kolmekivi"; const VENDOR: &'static str = "Novoyuuparosk"; const URL: &'static str = env!("CARGO_PKG_HOMEPAGE"); @@ -79,7 +162,6 @@ impl Plugin for Kolmekivi { } fn editor(&mut self, _async_executor: AsyncExecutor) -> Option> { - // TODO: Build your own egui layout here. let params = self.params.clone(); create_egui_editor( self.params.editor_state.clone(), @@ -87,8 +169,18 @@ impl Plugin for Kolmekivi { |_, _| {}, move |egui_ctx, setter, _state| { egui::CentralPanel::default().show(egui_ctx, |ui| { - ui.label("Gain"); - ui.add(widgets::ParamSlider::for_param(¶ms.gain, setter)); + ui.horizontal(|ui| { + ui.label("Time"); + ui.add(widgets::ParamSlider::for_param(¶ms.time, setter)); + }); + ui.horizontal(|ui| { + ui.label("Feedback"); + ui.add(widgets::ParamSlider::for_param(¶ms.feedback, setter)); + }); + ui.horizontal(|ui| { + ui.label("Mix"); + ui.add(widgets::ParamSlider::for_param(¶ms.mix, setter)); + }); }); }, ) @@ -97,13 +189,23 @@ impl Plugin for Kolmekivi { fn initialize( &mut self, _audio_io_layout: &AudioIOLayout, - _buffer_config: &BufferConfig, + buffer_config: &BufferConfig, _context: &mut impl InitContext, ) -> bool { + let sample_rate = buffer_config.sample_rate; + self.sample_rate = sample_rate; + // Allocate for max delay (2 seconds at max sample rate) + let max_samples = (MAX_DELAY_MS * sample_rate / 1000.0) as usize + 1; + + self.delay_line = Some(StereoDelayLine::new(max_samples)); true } - fn reset(&mut self) {} + fn reset(&mut self) { + if let Some(ref mut delay_line) = self.delay_line { + delay_line.reset(); + } + } fn process( &mut self, @@ -111,12 +213,35 @@ impl Plugin for Kolmekivi { _aux: &mut AuxiliaryBuffers, _context: &mut impl ProcessContext, ) -> ProcessStatus { - // TODO: Implement your DSP here. This applies the example gain parameter; note that - // the smoother is advanced once per frame so automation doesn't zipper. + let delay_line = match &mut self.delay_line { + Some(dl) => dl, + None => return ProcessStatus::Normal, + }; + + let sample_rate = self.sample_rate; + let time_val = self.params.time.smoothed.next(); + let feedback = self.params.feedback.smoothed.next(); + let mix = self.params.mix.smoothed.next(); + let delay_samples = (time_val * sample_rate / 1000.0) as usize; + + // Process each sample in the buffer for mut frame in buffer.iter_samples() { - let gain = self.params.gain.smoothed.next(); - for sample in frame.iter_mut() { - *sample *= gain; + let n = frame.len().min(2); + // Read input samples + let input_left = *frame.get_mut(0).unwrap(); + let input_right = if n > 1 { + *frame.get_mut(1).unwrap() + } else { + 0.0 + }; + + let (output_left, output_right) = + delay_line.process_frame(input_left, input_right, delay_samples, feedback, mix); + + // Write output back — left channel always exists, right may not + *frame.get_mut(0).unwrap() = output_left; + if n > 1 { + *frame.get_mut(1).unwrap() = output_right; } } @@ -125,18 +250,20 @@ impl Plugin for Kolmekivi { } impl ClapPlugin for Kolmekivi { - // Frozen identity: hosts key saved projects and presets off this. Never change it. const CLAP_ID: &'static str = "org.novoyuuparosk.kolmekivi"; - const CLAP_DESCRIPTION: Option<&'static str> = Some("Stereo delay with feedback and mix control"); + const CLAP_DESCRIPTION: Option<&'static str> = + Some("Stereo delay with feedback and mix control"); const CLAP_MANUAL_URL: Option<&'static str> = Some(Self::URL); const CLAP_SUPPORT_URL: Option<&'static str> = None; - const CLAP_FEATURES: &'static [ClapFeature] = - &[ClapFeature::AudioEffect, ClapFeature::Delay, ClapFeature::Stereo]; + const CLAP_FEATURES: &'static [ClapFeature] = &[ + ClapFeature::AudioEffect, + ClapFeature::Delay, + ClapFeature::Stereo, + ]; } #[cfg(feature = "vst3")] impl Vst3Plugin for Kolmekivi { - // Frozen identity, exactly 16 bytes. Never change it once distributed. const VST3_CLASS_ID: [u8; 16] = *b"KolmekiviDelay!!"; const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] = &[Vst3SubCategory::Fx, Vst3SubCategory::Delay];