feat: implement basic stereo delay plugin
Add time (0-2000ms), feedback (0-1), and mix (0-1 wet/dry) parameters with a stereo delay line DSP. No extra features — just time, feedback and mix.
This commit is contained in:
+161
-34
@@ -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<EguiState>,
|
||||
|
||||
/// 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<f32>,
|
||||
right: Vec<f32>,
|
||||
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<KolmekiviParams>,
|
||||
sample_rate: f32,
|
||||
delay_line: Option<StereoDelayLine>,
|
||||
}
|
||||
|
||||
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<Self>) -> Option<Box<dyn Editor>> {
|
||||
// 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<Self>,
|
||||
) -> 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<Self>,
|
||||
) -> 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];
|
||||
|
||||
Reference in New Issue
Block a user