155 lines
4.3 KiB
Rust
155 lines
4.3 KiB
Rust
#![allow(incomplete_features)]
|
|
#![feature(generic_associated_types)]
|
|
#![feature(drain_filter)]
|
|
|
|
use baseplug::{event::Data, Event, Plugin, ProcessContext};
|
|
use rand::{thread_rng, Rng};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use utils::logs::*;
|
|
|
|
mod maths;
|
|
use maths::*;
|
|
|
|
baseplug::model! {
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
struct MultidimModel {
|
|
/// Speed at which the ball moves
|
|
/// Around 1 should cause aprox. one bounce per beat
|
|
#[model(min = 0.0, max = 10.0)]
|
|
#[parameter(name = "speed")]
|
|
speed: f32,
|
|
/// Root note
|
|
/// Notes will be generated from `root` to `root + DIM`
|
|
#[model(min = 0.0, max = 111.0)]
|
|
#[parameter(name = "root note")]
|
|
root: f32,
|
|
/// How many beats each note lasts
|
|
#[model(min = 0.0, max = 4.0)]
|
|
#[parameter(name = "note length")]
|
|
note_length: f32,
|
|
/// Enable if over 0.5
|
|
#[model(min = 0.0, max = 1.0)]
|
|
#[parameter(name = "enable")]
|
|
enable: f32,
|
|
}
|
|
}
|
|
|
|
impl Default for MultidimModel {
|
|
fn default() -> Self {
|
|
Self {
|
|
speed: 1.0,
|
|
root: 64.0,
|
|
note_length: 0.25,
|
|
enable: 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
const DIM: usize = 16;
|
|
|
|
struct Multidim {
|
|
position: [f32; DIM],
|
|
velocity: [f32; DIM],
|
|
|
|
frame: usize,
|
|
pending_note_offs: Vec<(u8, usize)>,
|
|
}
|
|
|
|
impl Plugin for Multidim {
|
|
const NAME: &'static str = "multidim";
|
|
const PRODUCT: &'static str = "multidim";
|
|
const VENDOR: &'static str = "unnieversal";
|
|
|
|
const INPUT_CHANNELS: usize = 2;
|
|
const OUTPUT_CHANNELS: usize = 2;
|
|
|
|
type Model = MultidimModel;
|
|
|
|
#[inline]
|
|
fn new(_sample_rate: f32, _model: &MultidimModel) -> Self {
|
|
setup_logging("multidim.log");
|
|
|
|
// Randomize direction, normalize
|
|
let mut velocity = [0.0; DIM];
|
|
let mut rng = thread_rng();
|
|
for v in &mut velocity {
|
|
*v = rng.gen_range(-1.0..1.0);
|
|
}
|
|
velocity = normalize(velocity);
|
|
|
|
log::info!("finished init");
|
|
|
|
Self {
|
|
position: [0.0; DIM],
|
|
velocity,
|
|
frame: 0,
|
|
pending_note_offs: Vec::with_capacity(100),
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn process(&mut self, model: &MultidimModelProcess, ctx: &mut ProcessContext<Self>) {
|
|
let output = &mut ctx.outputs[0].buffers;
|
|
let enqueue_midi = &mut ctx.enqueue_event;
|
|
|
|
let curr_bpm = ctx.musical_time.bpm as f32;
|
|
let beat_in_seconds = 60.0 / curr_bpm;
|
|
let samples_per_beat = beat_in_seconds * ctx.sample_rate as f32;
|
|
|
|
// Check for pending note offs
|
|
let n = ctx.nframes;
|
|
let f = self.frame;
|
|
for (note, end) in self.pending_note_offs.drain_filter(|(_, end)| *end < f + n) {
|
|
// Send note off
|
|
let note_on = Event::<Multidim> {
|
|
frame: end - self.frame,
|
|
data: Data::Midi([0x80, note, 120]),
|
|
};
|
|
enqueue_midi(note_on);
|
|
}
|
|
self.frame += ctx.nframes;
|
|
|
|
for i in 0..ctx.nframes {
|
|
// write silence
|
|
output[0][i] = 0.0;
|
|
output[1][i] = 0.0;
|
|
|
|
// If we're not playing, we skip advancing the ball
|
|
// We don't want to 1) waste cpu 2) send note ons
|
|
if model.enable[i] < 0.5 {
|
|
continue;
|
|
}
|
|
|
|
// Advance ball and get list of collisions
|
|
let collisions = advance(
|
|
model.speed[i] / samples_per_beat,
|
|
&mut self.position,
|
|
&mut self.velocity,
|
|
);
|
|
|
|
let root = model.root[i].floor() as u8;
|
|
// Go through the list of possible collisions, and if any happened, send midi
|
|
for (idx, collision) in collisions.iter().enumerate() {
|
|
if !collision {
|
|
continue;
|
|
}
|
|
|
|
let note = idx as u8 + root;
|
|
|
|
// Send note on
|
|
let note_on = Event::<Multidim> {
|
|
frame: i,
|
|
data: Data::Midi([0x90, note, 120]),
|
|
};
|
|
enqueue_midi(note_on);
|
|
|
|
// Add note off
|
|
let end = self.frame + (samples_per_beat * model.note_length[i]) as usize;
|
|
self.pending_note_offs.push((note, end))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
baseplug::vst2!(Multidim, b"mdim");
|