unnieversal/crates/panera/src/lib.rs

168 lines
4.7 KiB
Rust

#![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");