diff --git a/README.md b/README.md index 243a22c..435e2c5 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ the following is the current list of plugins - `double_reverse_delta_inverter`: idk, a weird distortion - `transmute_pitch`: pitch to midi converter - `reverter`: play sound backwards +- `panera`: pan individual notes differently there's a bit of an explanation of each of the plugins below, but it's not a thorough documentation or a manual, it's just a bunch of notes i've written and a short description of the parameters @@ -264,6 +265,28 @@ this plugin will introduce a delay of `length` samples, since it has to record t in my experience values between 5000 and 8000 tend to work well, but you might want to experiment a bit to see what works for you +### panera + +ever wanted to have each pluck of the guitar have a different pan? do i have something for you then! + +params: +- `gain`: pregain added to the detector. doesn't affect output sound +- `attack`: attack of the envelope follower +- `release`: release of the envelope follower +- `gate`: gate level at which a peak is detected + +- `panning mode`: one of {alternating [0, 0.33), sine [0.33, 0.66), random [0.66, 1]} +- `lfo freq`: frequency for the sine lfo + +panera consists of a peak detector with a sample and hold lfo tied to the pan. this lfo is sampled each time a new peak comes in, and it's value will be held until the next peak + +there's three panning modes: +- alternating: each peak will be hard panned to a different side, alternating between left and right +- sine: samples a sine lfo, and uses that as the pan for the rest of the note +- random: each note will have a random pan + +the peak detector is still a bit fiddly, so you'll have to tweak the params a bit until it works for the kind of sound you're giving it. the defaults have worked great for me, so i recommend you start from there and change as you see fit + ## contributing issues and prs are welcome, but please open an issue before making any big pr, i don't wanna have to reject a pr where you have put a lot of effort on. if you are fine with that, ig go ahead i'm not your mum diff --git a/crates/panera/Cargo.toml b/crates/panera/Cargo.toml new file mode 100644 index 0000000..accedc8 --- /dev/null +++ b/crates/panera/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "panera" +version = "0.1.0" +edition = "2018" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +baseplug = { git = "https://github.com/wrl/baseplug.git", rev = "9cec68f31cca9c0c7a1448379f75d92bbbc782a8" } +serde = "1.0.126" + +utils = { path = "../utils" } diff --git a/crates/panera/src/lib.rs b/crates/panera/src/lib.rs new file mode 100644 index 0000000..932aa78 --- /dev/null +++ b/crates/panera/src/lib.rs @@ -0,0 +1,167 @@ +#![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"); diff --git a/crates/robotuna/src/lib.rs b/crates/robotuna/src/lib.rs index 97efecc..d2dab4b 100644 --- a/crates/robotuna/src/lib.rs +++ b/crates/robotuna/src/lib.rs @@ -170,17 +170,13 @@ impl RoboTuna { self.delay_idx_r += adv_r; self.true_idx += 1; - // get how close we are to the input idx, so we know if we have to interpolate/jump - let l_diff = self.true_idx as f32 - self.delay_idx_l; - let r_diff = self.true_idx as f32 - self.delay_idx_r; - // get the current value let mut l = self.delays.l.floating_index(self.delay_idx_l); let mut r = self.delays.r.floating_index(self.delay_idx_r); - // Interpolation - // if we are close to having to jump, we start interpolating with the jump destination - // interpolate when we're one third of the period away from jumping + // get how close we are to the input idx, so we know if we have to interpolate/jump + let l_diff = self.true_idx as f32 - self.delay_idx_l; + let r_diff = self.true_idx as f32 - self.delay_idx_r; // TODO change to a non-linear interpolation @@ -189,11 +185,22 @@ impl RoboTuna { let a = (l_diff - period_l) / (period_l / DIV); l *= a; l += (1.0 - a) * self.delays.l.floating_index(self.delay_idx_l - period_l); + // crossfade + // if we are close to having to jump, we start crossfading with the jump destination + // crossfade when we're one third of the period away from jumping + // when we get close to jumping back + if l_diff - period_l < cf_len_l { + // cross goes from 1 (when l_diff is at the max) to 0 (when l_diff == period_l) + let cross = (l_diff - period_l) / cf_len_l; + let (fade_in, fade_out) = ep_crossfade(1.0 - cross); + l = fade_out * l + fade_in * self.delays.l.floating_index(self.delay_idx_l - period_l); } - if 3.0 * period_l - l_diff < (period_l / DIV) { - let a = (3.0 * period_l - l_diff) / (period_l / DIV); - l *= a; - l += (1.0 - a) * self.delays.l.floating_index(self.delay_idx_l - period_l); + // when we get close to jumping foward + if MAX_PERIOD * period_l - l_diff < cf_len_l { + // cross goes from 1 (when l_diff is at the min) to 0 (when l_diff == 3.0 * period_l) + let cross = (MAX_PERIOD * period_l - l_diff) / cf_len_l; + let (fade_in, fade_out) = ep_crossfade(1.0 - cross); + l = fade_out * l + fade_in * self.delays.l.floating_index(self.delay_idx_l + period_l); } if r_diff - period_r < (period_r / DIV) { let a = (r_diff - period_r) / (period_r / DIV); diff --git a/crates/utils/src/delay.rs b/crates/utils/src/delay.rs index 862a214..3e4ffe6 100644 --- a/crates/utils/src/delay.rs +++ b/crates/utils/src/delay.rs @@ -10,13 +10,7 @@ impl DelayLine { } } - pub fn read_slice(&self, slice: &mut [f32]) { - // Copy values in order - for i in 0..LEN { - slice[i] = self.wrapped_index(self.index + i); - } - } - + /// write to delay line and advance index pub fn write_and_advance(&mut self, value: f32) { self.buffer[self.index] = value; @@ -27,6 +21,13 @@ impl DelayLine { } } + /// write to delay line, advance index, and return the oldest sample + pub fn write_and_advance_get_last(&mut self, value: f32) -> f32 { + self.write_and_advance(value); + + self.wrapped_index(self.index + 1) + } + /// Returns the sample at idx after taking modulo LEN pub fn wrapped_index(&self, idx: usize) -> f32 { self.buffer[idx % LEN] @@ -37,9 +38,8 @@ impl DelayLine { let idx = val.trunc() as usize; let frac = val.fract(); - // TODO uhm idk what this should be, but we don't want an underflow so yeah, let xm1 = if idx == 0 { - 0.0 + self.wrapped_index(LEN - 1) } else { self.wrapped_index(idx - 1) }; @@ -62,6 +62,13 @@ impl DelayLine { pub fn buffer(&self) -> &[f32; LEN] { &self.buffer } + + pub fn read_slice(&self, slice: &mut [f32]) { + // Copy values in order + for i in 0..LEN { + slice[i] = self.wrapped_index(self.index + i); + } + } } pub struct DelayLines { diff --git a/crates/utils/src/envelope.rs b/crates/utils/src/envelope.rs new file mode 100644 index 0000000..9145f38 --- /dev/null +++ b/crates/utils/src/envelope.rs @@ -0,0 +1,41 @@ +// from https://www.musicdsp.org/en/latest/Analysis/97-envelope-detector.html +// with some modifications +pub struct EnvelopeFollower { + x1: f32, + x2: f32, + + ga: f32, + gr: f32, +} + +impl EnvelopeFollower { + pub fn new() -> Self { + Self { + x1: 0.0, + x2: 0.0, + ga: 0.0, + gr: 0.0, + } + } + + /// attack and release are in seconds + pub fn set_attack_release(&mut self, sample_rate: f32, attack: f32, release: f32) { + self.ga = (-1.0 / (sample_rate * attack)).exp(); + self.gr = (-1.0 / (sample_rate * release)).exp(); + } + + pub fn process(&mut self, input: f32) -> f32 { + let in_abs = input.abs(); + + // 2nd order lowpass + if self.x1 < in_abs { + self.x1 = in_abs + self.ga * (self.x1 - in_abs); + self.x2 = self.x1 + self.ga * (self.x2 - self.x1); + } else { + self.x1 = in_abs + self.gr * (self.x1 - in_abs); + self.x2 = self.x1 + self.gr * (self.x2 - self.x1); + } + + self.x2 + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index c6f51cf..ca2b3c2 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,5 +1,6 @@ pub mod buffers; pub mod delay; +pub mod envelope; pub mod logs; pub mod pitch; pub mod threeband;