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;
|
use std::sync::Arc;
|
||||||
|
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
// Params — add your plugin parameters here.
|
// Params — time, feedback, mix
|
||||||
// See musiikki/codename-206/src/params.rs for a real example.
|
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
const MAX_DELAY_MS: f32 = 2_000.0;
|
||||||
|
|
||||||
#[derive(Params)]
|
#[derive(Params)]
|
||||||
struct KolmekiviParams {
|
struct KolmekiviParams {
|
||||||
/// Editor window size, persisted with the plugin state.
|
/// Editor window size, persisted with the plugin state.
|
||||||
#[persist = "editor-state"]
|
#[persist = "editor-state"]
|
||||||
editor_state: Arc<EguiState>,
|
editor_state: Arc<EguiState>,
|
||||||
|
|
||||||
/// Example parameter — replace with your own. The `id` is the stable key the host
|
/// Delay time in milliseconds.
|
||||||
/// stores automation against, so never change it once you've shipped.
|
#[id = "time"]
|
||||||
#[id = "gain"]
|
time: FloatParam,
|
||||||
gain: 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 {
|
impl Default for KolmekiviParams {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
editor_state: EguiState::from_size(320, 160),
|
editor_state: EguiState::from_size(320, 160),
|
||||||
gain: FloatParam::new(
|
time: FloatParam::new(
|
||||||
"Gain",
|
"Time",
|
||||||
util::db_to_gain(0.0),
|
500.0,
|
||||||
FloatRange::Skewed {
|
FloatRange::Linear { min: 0.0, max: MAX_DELAY_MS },
|
||||||
min: util::db_to_gain(-30.0),
|
|
||||||
max: util::db_to_gain(30.0),
|
|
||||||
factor: FloatRange::gain_skew_factor(-30.0, 30.0),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.with_smoother(SmoothingStyle::Logarithmic(50.0))
|
.with_unit(" ms")
|
||||||
.with_unit(" dB")
|
.with_value_to_string(formatters::v2s_f32_rounded(0)),
|
||||||
.with_value_to_string(formatters::v2s_f32_gain_to_db(2))
|
|
||||||
.with_string_to_value(formatters::s2v_f32_gain_to_db()),
|
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
|
// Plugin
|
||||||
// ──────────────────────────────────────────────
|
// ──────────────────────────────────────────────
|
||||||
@@ -49,10 +131,11 @@ impl Default for KolmekiviParams {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct Kolmekivi {
|
struct Kolmekivi {
|
||||||
params: Arc<KolmekiviParams>,
|
params: Arc<KolmekiviParams>,
|
||||||
|
sample_rate: f32,
|
||||||
|
delay_line: Option<StereoDelayLine>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Plugin for Kolmekivi {
|
impl Plugin for Kolmekivi {
|
||||||
// TODO: Replace these placeholder values with your plugin's metadata.
|
|
||||||
const NAME: &'static str = "Kolmekivi";
|
const NAME: &'static str = "Kolmekivi";
|
||||||
const VENDOR: &'static str = "Novoyuuparosk";
|
const VENDOR: &'static str = "Novoyuuparosk";
|
||||||
const URL: &'static str = env!("CARGO_PKG_HOMEPAGE");
|
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>> {
|
fn editor(&mut self, _async_executor: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||||
// TODO: Build your own egui layout here.
|
|
||||||
let params = self.params.clone();
|
let params = self.params.clone();
|
||||||
create_egui_editor(
|
create_egui_editor(
|
||||||
self.params.editor_state.clone(),
|
self.params.editor_state.clone(),
|
||||||
@@ -87,8 +169,18 @@ impl Plugin for Kolmekivi {
|
|||||||
|_, _| {},
|
|_, _| {},
|
||||||
move |egui_ctx, setter, _state| {
|
move |egui_ctx, setter, _state| {
|
||||||
egui::CentralPanel::default().show(egui_ctx, |ui| {
|
egui::CentralPanel::default().show(egui_ctx, |ui| {
|
||||||
ui.label("Gain");
|
ui.horizontal(|ui| {
|
||||||
ui.add(widgets::ParamSlider::for_param(¶ms.gain, setter));
|
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(
|
fn initialize(
|
||||||
&mut self,
|
&mut self,
|
||||||
_audio_io_layout: &AudioIOLayout,
|
_audio_io_layout: &AudioIOLayout,
|
||||||
_buffer_config: &BufferConfig,
|
buffer_config: &BufferConfig,
|
||||||
_context: &mut impl InitContext<Self>,
|
_context: &mut impl InitContext<Self>,
|
||||||
) -> bool {
|
) -> 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
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset(&mut self) {}
|
fn reset(&mut self) {
|
||||||
|
if let Some(ref mut delay_line) = self.delay_line {
|
||||||
|
delay_line.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn process(
|
fn process(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -111,12 +213,35 @@ impl Plugin for Kolmekivi {
|
|||||||
_aux: &mut AuxiliaryBuffers,
|
_aux: &mut AuxiliaryBuffers,
|
||||||
_context: &mut impl ProcessContext<Self>,
|
_context: &mut impl ProcessContext<Self>,
|
||||||
) -> ProcessStatus {
|
) -> ProcessStatus {
|
||||||
// TODO: Implement your DSP here. This applies the example gain parameter; note that
|
let delay_line = match &mut self.delay_line {
|
||||||
// the smoother is advanced once per frame so automation doesn't zipper.
|
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() {
|
for mut frame in buffer.iter_samples() {
|
||||||
let gain = self.params.gain.smoothed.next();
|
let n = frame.len().min(2);
|
||||||
for sample in frame.iter_mut() {
|
// Read input samples
|
||||||
*sample *= gain;
|
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 {
|
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_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_MANUAL_URL: Option<&'static str> = Some(Self::URL);
|
||||||
const CLAP_SUPPORT_URL: Option<&'static str> = None;
|
const CLAP_SUPPORT_URL: Option<&'static str> = None;
|
||||||
const CLAP_FEATURES: &'static [ClapFeature] =
|
const CLAP_FEATURES: &'static [ClapFeature] = &[
|
||||||
&[ClapFeature::AudioEffect, ClapFeature::Delay, ClapFeature::Stereo];
|
ClapFeature::AudioEffect,
|
||||||
|
ClapFeature::Delay,
|
||||||
|
ClapFeature::Stereo,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "vst3")]
|
#[cfg(feature = "vst3")]
|
||||||
impl Vst3Plugin for Kolmekivi {
|
impl Vst3Plugin for Kolmekivi {
|
||||||
// Frozen identity, exactly 16 bytes. Never change it once distributed.
|
|
||||||
const VST3_CLASS_ID: [u8; 16] = *b"KolmekiviDelay!!";
|
const VST3_CLASS_ID: [u8; 16] = *b"KolmekiviDelay!!";
|
||||||
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
|
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
|
||||||
&[Vst3SubCategory::Fx, Vst3SubCategory::Delay];
|
&[Vst3SubCategory::Fx, Vst3SubCategory::Delay];
|
||||||
|
|||||||
Reference in New Issue
Block a user