some sort of ui
This commit is contained in:
@@ -4,4 +4,5 @@ version.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
crossbeam-channel.workspace = true
|
||||
params = { workspace = true }
|
||||
|
||||
61
crates/engine/src/envelope.rs
Normal file
61
crates/engine/src/envelope.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum Stage {
|
||||
Idle,
|
||||
Delay,
|
||||
Attack,
|
||||
Hold,
|
||||
Decay,
|
||||
Sustain,
|
||||
Release,
|
||||
}
|
||||
|
||||
pub struct Dahdsr {
|
||||
pub stage: Stage,
|
||||
pub level: f32,
|
||||
pub delay: f32,
|
||||
pub attack: f32,
|
||||
pub hold: f32,
|
||||
pub decay: f32,
|
||||
pub sustain: f32,
|
||||
pub release: f32,
|
||||
pub attack_curve: f32,
|
||||
pub decay_curve: f32,
|
||||
pub release_curve: f32,
|
||||
pub sample_rate: f32,
|
||||
stage_samples: u32,
|
||||
}
|
||||
|
||||
impl Dahdsr {
|
||||
pub fn new(sr: f32) -> Self {
|
||||
Self {
|
||||
stage: Stage::Idle,
|
||||
level: 0.0,
|
||||
delay: 0.0,
|
||||
attack: 0.01,
|
||||
hold: 0.0,
|
||||
decay: 0.3,
|
||||
sustain: 1.0,
|
||||
release: 0.3,
|
||||
attack_curve: 0.0,
|
||||
decay_curve: 0.0,
|
||||
release_curve: 0.0,
|
||||
sample_rate: sr,
|
||||
stage_samples: 0,
|
||||
}
|
||||
}
|
||||
pub fn note_on(&mut self) {
|
||||
self.stage = Stage::Delay;
|
||||
self.stage_samples = 0;
|
||||
}
|
||||
pub fn note_off(&mut self) {
|
||||
self.stage = Stage::Release;
|
||||
self.stage_samples = 0;
|
||||
}
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.stage != Stage::Idle
|
||||
}
|
||||
#[inline]
|
||||
pub fn tick(&mut self) -> f32 {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
38
crates/engine/src/filter.rs
Normal file
38
crates/engine/src/filter.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum FilterKind {
|
||||
Ladder,
|
||||
Svf,
|
||||
Comb,
|
||||
Formant,
|
||||
}
|
||||
|
||||
pub struct Filter {
|
||||
pub kind: FilterKind,
|
||||
pub cutoff: f32,
|
||||
pub resonance: f32,
|
||||
pub drive: f32,
|
||||
pub keytrack: f32,
|
||||
state: [f32; 4],
|
||||
sample_rate: f32,
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub fn new(sr: f32) -> Self {
|
||||
Self {
|
||||
kind: FilterKind::Svf,
|
||||
cutoff: 1.0,
|
||||
resonance: 0.0,
|
||||
drive: 0.0,
|
||||
keytrack: 0.0,
|
||||
state: [0.0; 4],
|
||||
sample_rate: sr,
|
||||
}
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
self.state = [0.0; 4];
|
||||
}
|
||||
#[inline]
|
||||
pub fn process(&mut self, x: f32, _note_freq: f32) -> f32 {
|
||||
x
|
||||
}
|
||||
}
|
||||
38
crates/engine/src/lfo.rs
Normal file
38
crates/engine/src/lfo.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum LfoMode {
|
||||
FreeRun,
|
||||
BpmSync,
|
||||
OneShot,
|
||||
Envelope,
|
||||
}
|
||||
|
||||
pub struct Lfo {
|
||||
pub phase: f32,
|
||||
pub rate: f32,
|
||||
pub depth: f32,
|
||||
pub mode: LfoMode,
|
||||
pub wave_pos: f32,
|
||||
pub sync: bool,
|
||||
sample_rate: f32,
|
||||
}
|
||||
|
||||
impl Lfo {
|
||||
pub fn new(sr: f32) -> Self {
|
||||
Self {
|
||||
phase: 0.0,
|
||||
rate: 1.0,
|
||||
depth: 1.0,
|
||||
mode: LfoMode::FreeRun,
|
||||
wave_pos: 0.0,
|
||||
sync: false,
|
||||
sample_rate: sr,
|
||||
}
|
||||
}
|
||||
pub fn retrigger(&mut self) {
|
||||
self.phase = 0.0;
|
||||
}
|
||||
#[inline]
|
||||
pub fn tick(&mut self, _host_bpm: f32) -> f32 {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,105 @@
|
||||
pub mod envelope;
|
||||
pub mod filter;
|
||||
pub mod lfo;
|
||||
pub mod mod_matrix;
|
||||
pub mod oscillator;
|
||||
pub mod voice;
|
||||
|
||||
use crossbeam_channel::{Receiver, Sender, bounded};
|
||||
use mod_matrix::ModMatrix;
|
||||
use params::{ParamId, ParamStore};
|
||||
use voice::Voice;
|
||||
|
||||
pub const MAX_VOICES: usize = 16;
|
||||
|
||||
pub struct EngineMetrics {
|
||||
pub voice_count: u8,
|
||||
pub cpu_load: f32,
|
||||
pub waveform: [f32; 256],
|
||||
}
|
||||
|
||||
pub struct Engine {
|
||||
params: ParamStore,
|
||||
sample_rate: f32,
|
||||
voices: Box<[Voice; MAX_VOICES]>,
|
||||
round_robin: usize,
|
||||
mod_matrix: ModMatrix,
|
||||
pub metrics_tx: Sender<EngineMetrics>,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub fn new(params: ParamStore, sample_rate: f32) -> Self {
|
||||
Self {
|
||||
params,
|
||||
sample_rate,
|
||||
}
|
||||
pub fn new(params: ParamStore, sample_rate: f32) -> (Self, Receiver<EngineMetrics>) {
|
||||
let (tx, rx) = bounded(4);
|
||||
let voices = Box::new(std::array::from_fn(|_| Voice::new(sample_rate)));
|
||||
(
|
||||
Self {
|
||||
params,
|
||||
sample_rate,
|
||||
voices,
|
||||
round_robin: 0,
|
||||
mod_matrix: ModMatrix::new(),
|
||||
metrics_tx: tx,
|
||||
},
|
||||
rx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_simple(params: ParamStore, sample_rate: f32) -> Self {
|
||||
let (engine, _rx) = Self::new(params, sample_rate);
|
||||
engine
|
||||
}
|
||||
|
||||
pub fn set_sample_rate(&mut self, rate: f32) {
|
||||
self.sample_rate = rate;
|
||||
}
|
||||
|
||||
pub fn note_on(&mut self, note: u8, vel: u8) {
|
||||
let idx = self.round_robin % MAX_VOICES;
|
||||
let v = &mut self.voices[idx];
|
||||
v.active = true;
|
||||
v.note = note;
|
||||
v.velocity = vel as f32 / 127.0;
|
||||
for e in &mut v.envs {
|
||||
e.note_on();
|
||||
}
|
||||
self.round_robin += 1;
|
||||
}
|
||||
|
||||
pub fn note_off(&mut self, note: u8) {
|
||||
for v in self
|
||||
.voices
|
||||
.iter_mut()
|
||||
.filter(|v| v.active && v.note == note)
|
||||
{
|
||||
for e in &mut v.envs {
|
||||
e.note_off();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// No alloc, no lock. Must stay that way.
|
||||
#[inline]
|
||||
pub fn process(&mut self, out_l: &mut [f32], out_r: &mut [f32]) {
|
||||
debug_assert_eq!(out_l.len(), out_r.len());
|
||||
out_l.fill(0.0);
|
||||
out_r.fill(0.0);
|
||||
|
||||
let _vol = self.params.get(ParamId::MasterVolume);
|
||||
let vol = self.params.get(ParamId::MasterVolume);
|
||||
for v in self.voices.iter_mut().filter(|v| v.active) {
|
||||
let (vl, vr) = v.process();
|
||||
for (o, s) in out_l.iter_mut().zip(std::iter::repeat(vl)) {
|
||||
*o += s * vol;
|
||||
}
|
||||
for (o, s) in out_r.iter_mut().zip(std::iter::repeat(vr)) {
|
||||
*o += s * vol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn midi_event(&mut self, _data: [u8; 3]) {}
|
||||
pub fn midi_event(&mut self, data: [u8; 3]) {
|
||||
match data[0] & 0xF0 {
|
||||
0x90 if data[2] > 0 => self.note_on(data[1], data[2]),
|
||||
0x80 | 0x90 => self.note_off(data[1]),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
crates/engine/src/mod_matrix.rs
Normal file
46
crates/engine/src/mod_matrix.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use params::ParamId;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ModSource {
|
||||
Env(u8),
|
||||
Lfo(u8),
|
||||
Velocity,
|
||||
Note,
|
||||
Modwheel,
|
||||
Aftertouch,
|
||||
Macro(u8),
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ModSlot {
|
||||
pub active: bool,
|
||||
pub source: ModSource,
|
||||
pub dest: ParamId,
|
||||
pub depth: f32,
|
||||
pub per_voice: bool,
|
||||
}
|
||||
|
||||
impl Default for ModSlot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active: false,
|
||||
source: ModSource::Env(0),
|
||||
dest: ParamId::Filter1Cutoff,
|
||||
depth: 0.0,
|
||||
per_voice: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ModMatrix {
|
||||
pub slots: [ModSlot; 64],
|
||||
}
|
||||
|
||||
impl ModMatrix {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
slots: [ModSlot::default(); 64],
|
||||
}
|
||||
}
|
||||
pub fn apply(&self, _params: ¶ms::ParamStore) {}
|
||||
}
|
||||
34
crates/engine/src/oscillator.rs
Normal file
34
crates/engine/src/oscillator.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
pub const TABLE_SIZE: usize = 2048;
|
||||
pub type Wavetable = Box<[f32; TABLE_SIZE]>;
|
||||
|
||||
pub struct WavetableOsc {
|
||||
pub phase: f32,
|
||||
pub gain: f32,
|
||||
pub pan: f32,
|
||||
pub unison_count: u8,
|
||||
pub unison_detune: f32,
|
||||
pub unison_spread: f32,
|
||||
pub wave_pos: f32,
|
||||
wavetable: Wavetable,
|
||||
}
|
||||
|
||||
impl WavetableOsc {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
phase: 0.0,
|
||||
gain: 1.0,
|
||||
pan: 0.0,
|
||||
unison_count: 1,
|
||||
unison_detune: 0.1,
|
||||
unison_spread: 0.5,
|
||||
wave_pos: 0.0,
|
||||
wavetable: Box::new([0.0f32; TABLE_SIZE]),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn tick(&mut self, phase_inc: f32) -> (f32, f32) {
|
||||
self.phase = (self.phase + phase_inc).fract();
|
||||
(0.0, 0.0)
|
||||
}
|
||||
}
|
||||
31
crates/engine/src/voice.rs
Normal file
31
crates/engine/src/voice.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use crate::{envelope::Dahdsr, filter::Filter, lfo::Lfo, oscillator::WavetableOsc};
|
||||
|
||||
pub struct Voice {
|
||||
pub active: bool,
|
||||
pub note: u8,
|
||||
pub velocity: f32,
|
||||
pub oscs: [WavetableOsc; 3],
|
||||
pub envs: [Dahdsr; 3],
|
||||
pub lfos: [Lfo; 4],
|
||||
pub filter1: Filter,
|
||||
pub filter2: Filter,
|
||||
}
|
||||
|
||||
impl Voice {
|
||||
pub fn new(sr: f32) -> Self {
|
||||
Self {
|
||||
active: false,
|
||||
note: 0,
|
||||
velocity: 0.0,
|
||||
oscs: std::array::from_fn(|_| WavetableOsc::new()),
|
||||
envs: std::array::from_fn(|_| Dahdsr::new(sr)),
|
||||
lfos: std::array::from_fn(|_| Lfo::new(sr)),
|
||||
filter1: Filter::new(sr),
|
||||
filter2: Filter::new(sr),
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
pub fn process(&mut self) -> (f32, f32) {
|
||||
(0.0, 0.0)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user