[panera] make and implement crate
This commit is contained in:
parent
59b3b9c822
commit
2aaad403d1
7 changed files with 279 additions and 20 deletions
23
README.md
23
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
|
||||
|
|
13
crates/panera/Cargo.toml
Normal file
13
crates/panera/Cargo.toml
Normal 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
167
crates/panera/src/lib.rs
Normal 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");
|
|
@ -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);
|
||||
|
|
|
@ -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> {
|
||||
|
|
41
crates/utils/src/envelope.rs
Normal file
41
crates/utils/src/envelope.rs
Normal 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
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
pub mod buffers;
|
||||
pub mod delay;
|
||||
pub mod envelope;
|
||||
pub mod logs;
|
||||
pub mod pitch;
|
||||
pub mod threeband;
|
||||
|
|
Loading…
Reference in a new issue