[panera] make and implement crate

main
annieversary 2021-08-13 21:42:24 +02:00
parent 59b3b9c822
commit 2aaad403d1
7 changed files with 279 additions and 20 deletions

View File

@ -64,6 +64,7 @@ the following is the current list of plugins
- `double_reverse_delta_inverter`: idk, a weird distortion - `double_reverse_delta_inverter`: idk, a weird distortion
- `transmute_pitch`: pitch to midi converter - `transmute_pitch`: pitch to midi converter
- `reverter`: play sound backwards - `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 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 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 ## 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 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

13
crates/panera/Cargo.toml Normal file
View File

@ -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" }

167
crates/panera/src/lib.rs Normal file
View File

@ -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<DELAY_LEN>,
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::<DELAY_LEN>::new(),
lfo_idx: 0,
}
}
#[inline]
fn process(&mut self, model: &PaneraModelProcess, ctx: &mut ProcessContext<Self>) {
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<f32> 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");

View File

@ -170,17 +170,13 @@ impl RoboTuna {
self.delay_idx_r += adv_r; self.delay_idx_r += adv_r;
self.true_idx += 1; 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 // get the current value
let mut l = self.delays.l.floating_index(self.delay_idx_l); let mut l = self.delays.l.floating_index(self.delay_idx_l);
let mut r = self.delays.r.floating_index(self.delay_idx_r); let mut r = self.delays.r.floating_index(self.delay_idx_r);
// Interpolation // get how close we are to the input idx, so we know if we have to interpolate/jump
// if we are close to having to jump, we start interpolating with the jump destination let l_diff = self.true_idx as f32 - self.delay_idx_l;
// interpolate when we're one third of the period away from jumping let r_diff = self.true_idx as f32 - self.delay_idx_r;
// TODO change to a non-linear interpolation // TODO change to a non-linear interpolation
@ -189,11 +185,22 @@ impl RoboTuna {
let a = (l_diff - period_l) / (period_l / DIV); let a = (l_diff - period_l) / (period_l / DIV);
l *= a; l *= a;
l += (1.0 - a) * self.delays.l.floating_index(self.delay_idx_l - period_l); 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) { // when we get close to jumping foward
let a = (3.0 * period_l - l_diff) / (period_l / DIV); if MAX_PERIOD * period_l - l_diff < cf_len_l {
l *= a; // cross goes from 1 (when l_diff is at the min) to 0 (when l_diff == 3.0 * period_l)
l += (1.0 - a) * self.delays.l.floating_index(self.delay_idx_l - 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) { if r_diff - period_r < (period_r / DIV) {
let a = (r_diff - period_r) / (period_r / DIV); let a = (r_diff - period_r) / (period_r / DIV);

View File

@ -10,13 +10,7 @@ impl<const LEN: usize> DelayLine<LEN> {
} }
} }
pub fn read_slice(&self, slice: &mut [f32]) { /// write to delay line and advance index
// Copy values in order
for i in 0..LEN {
slice[i] = self.wrapped_index(self.index + i);
}
}
pub fn write_and_advance(&mut self, value: f32) { pub fn write_and_advance(&mut self, value: f32) {
self.buffer[self.index] = value; self.buffer[self.index] = value;
@ -27,6 +21,13 @@ impl<const LEN: usize> DelayLine<LEN> {
} }
} }
/// 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 /// Returns the sample at idx after taking modulo LEN
pub fn wrapped_index(&self, idx: usize) -> f32 { pub fn wrapped_index(&self, idx: usize) -> f32 {
self.buffer[idx % LEN] self.buffer[idx % LEN]
@ -37,9 +38,8 @@ impl<const LEN: usize> DelayLine<LEN> {
let idx = val.trunc() as usize; let idx = val.trunc() as usize;
let frac = val.fract(); 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 { let xm1 = if idx == 0 {
0.0 self.wrapped_index(LEN - 1)
} else { } else {
self.wrapped_index(idx - 1) self.wrapped_index(idx - 1)
}; };
@ -62,6 +62,13 @@ impl<const LEN: usize> DelayLine<LEN> {
pub fn buffer(&self) -> &[f32; LEN] { pub fn buffer(&self) -> &[f32; LEN] {
&self.buffer &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<const LEN: usize> { pub struct DelayLines<const LEN: usize> {

View File

@ -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
}
}

View File

@ -1,5 +1,6 @@
pub mod buffers; pub mod buffers;
pub mod delay; pub mod delay;
pub mod envelope;
pub mod logs; pub mod logs;
pub mod pitch; pub mod pitch;
pub mod threeband; pub mod threeband;