[panera] make and implement crate

This commit is contained in:
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
- `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

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.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);

View file

@ -10,13 +10,7 @@ impl<const LEN: usize> DelayLine<LEN> {
}
}
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<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
pub fn wrapped_index(&self, idx: usize) -> f32 {
self.buffer[idx % LEN]
@ -37,9 +38,8 @@ impl<const LEN: usize> DelayLine<LEN> {
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<const LEN: usize> DelayLine<LEN> {
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<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 delay;
pub mod envelope;
pub mod logs;
pub mod pitch;
pub mod threeband;