some sort of ui

This commit is contained in:
2026-04-19 16:20:33 +03:00
parent 4f14980610
commit cdff703f7e
30 changed files with 1504 additions and 184 deletions

View File

@@ -4,4 +4,5 @@ version.workspace = true
edition.workspace = true
[dependencies]
crossbeam-channel.workspace = true
params = { workspace = true }

View 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
}
}

View 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
View 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
}
}

View File

@@ -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]),
_ => {}
}
}
}

View 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: &params::ParamStore) {}
}

View 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)
}
}

View 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)
}
}