#![allow(incomplete_features)] #![feature(generic_associated_types)] use baseplug::{Plugin, ProcessContext}; use serde::{Deserialize, Serialize}; use utils::delay::*; use utils::envelope::*; const DELAY_LEN: usize = 200; baseplug::model! { #[derive(Debug, Serialize, Deserialize)] struct PaneraModel { // peak detection options #[model(min = 0.0, max = 3.0)] #[parameter(name = "gain")] gain: f32, #[model(min = 0.0, max = 1.0)] #[parameter(name = "attack")] attack: f32, #[model(min = 0.0, max = 1.0)] #[parameter(name = "release")] release: f32, #[model(min = 0.0, max = 1.0)] #[parameter(name = "gate")] gate: f32, // options to control the pan oscillation #[model(min = 0.0, max = 1.0)] #[parameter(name = "panning mode")] panning_mode: f32, #[model(min = 0.0, max = 10.0)] #[parameter(name = "lfo freq")] lfo_freq: f32, } } impl Default for PaneraModel { fn default() -> Self { Self { gain: 1.8, attack: 0.0, release: 0.27, gate: 0.22, panning_mode: 0.0, lfo_freq: 1.0, } } } struct Panera { /// envelope follower so we can detect peaks envelope_follower: EnvelopeFollower, /// whether we are currently on a peak on: bool, /// the current pan position /// goes from 0 (left) to 1 (right) pan: f32, /// delay line is used so we don't change the pan after the peak has started /// since to detect a peak we have to be halfway through it, if we change the pan /// when we detect a peak, this means that it gets changed halfway, which results in a click /// the delay makes it so we can detect a peak before actually playing it, therefore /// allowing us to change the pan before the peak actually starts, and thus no clicks :) delay: DelayLine, lfo_idx: usize, } impl Plugin for Panera { const NAME: &'static str = "panera"; const PRODUCT: &'static str = "panera"; const VENDOR: &'static str = "unnieversal"; const INPUT_CHANNELS: usize = 1; const OUTPUT_CHANNELS: usize = 2; type Model = PaneraModel; #[inline] fn new(_sample_rate: f32, _model: &PaneraModel) -> Self { Self { envelope_follower: EnvelopeFollower::new(), on: true, pan: 0.0, delay: DelayLine::::new(), lfo_idx: 0, } } #[inline] fn process(&mut self, model: &PaneraModelProcess, ctx: &mut ProcessContext) { let input = &ctx.inputs[0].buffers; let output = &mut ctx.outputs[0].buffers; for i in 0..ctx.nframes { // set values for env detector self.envelope_follower.set_attack_release( ctx.sample_rate, model.attack[i], model.release[i], ); // process envelope detector with the non-delayed sample let env = self.envelope_follower.process(input[0][i] * model.gain[i]); // detect peak if env > model.gate[i] && !self.on { self.update_pan( model.panning_mode[i].into(), model.lfo_freq[i], ctx.sample_rate, ); self.on = true; } if env < model.gate[i] && self.on { self.on = false; } // increment lfo counter self.lfo_idx += 1; // update delay and get value let s = self.delay.write_and_advance_get_last(input[0][i]); // square pan law // we play the delayed value output[0][i] = self.pan.sqrt() * s; output[1][i] = (1.0 - self.pan).sqrt() * s; } } } enum PanningMode { Alternating, Sine, Random, } impl From for PanningMode { fn from(a: f32) -> Self { if a < 0.33 { Self::Alternating } else if a < 0.66 { Self::Sine } else { Self::Random } } } impl Panera { fn update_pan(&mut self, lfo_type: PanningMode, lfo_freq: f32, sample_rate: f32) { self.pan = match lfo_type { PanningMode::Alternating => 1.0 - self.pan, PanningMode::Sine => ((self.lfo_idx as f32 * lfo_freq / sample_rate).sin() + 1.0) / 2.0, PanningMode::Random => { // idk tbh, just something kinda random ig // i just want it to not be predictable (((self.lfo_idx * (self.lfo_idx ^ 1234)) as f32 * lfo_freq).sin() + 1.0) / 2.0 } } } } baseplug::vst2!(Panera, b"pann");