some sort of ui
This commit is contained in:
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -338,6 +338,12 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "atomic_float"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atspi"
|
name = "atspi"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
@@ -709,6 +715,15 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
@@ -849,6 +864,7 @@ checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
|||||||
name = "engine"
|
name = "engine"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
"params",
|
"params",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2013,6 +2029,9 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "params"
|
name = "params"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"atomic_float",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
|
|||||||
@@ -17,3 +17,5 @@ params = { path = "crates/params" }
|
|||||||
engine = { path = "crates/engine" }
|
engine = { path = "crates/engine" }
|
||||||
lv2 = "0.6.0"
|
lv2 = "0.6.0"
|
||||||
vizia = "0.3.0"
|
vizia = "0.3.0"
|
||||||
|
atomic_float = "1.1.0"
|
||||||
|
crossbeam-channel = "0.5.15"
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ version.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
crossbeam-channel.workspace = true
|
||||||
params = { 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 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 {
|
pub struct Engine {
|
||||||
params: ParamStore,
|
params: ParamStore,
|
||||||
sample_rate: f32,
|
sample_rate: f32,
|
||||||
|
voices: Box<[Voice; MAX_VOICES]>,
|
||||||
|
round_robin: usize,
|
||||||
|
mod_matrix: ModMatrix,
|
||||||
|
pub metrics_tx: Sender<EngineMetrics>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
pub fn new(params: ParamStore, sample_rate: f32) -> Self {
|
pub fn new(params: ParamStore, sample_rate: f32) -> (Self, Receiver<EngineMetrics>) {
|
||||||
Self {
|
let (tx, rx) = bounded(4);
|
||||||
params,
|
let voices = Box::new(std::array::from_fn(|_| Voice::new(sample_rate)));
|
||||||
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) {
|
pub fn set_sample_rate(&mut self, rate: f32) {
|
||||||
self.sample_rate = rate;
|
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]) {
|
pub fn process(&mut self, out_l: &mut [f32], out_r: &mut [f32]) {
|
||||||
debug_assert_eq!(out_l.len(), out_r.len());
|
debug_assert_eq!(out_l.len(), out_r.len());
|
||||||
out_l.fill(0.0);
|
out_l.fill(0.0);
|
||||||
out_r.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,3 +2,6 @@
|
|||||||
name = "params"
|
name = "params"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
atomic_float.workspace = true
|
||||||
|
|||||||
@@ -1,114 +1,120 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicU32, Ordering};
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use atomic_float::AtomicF32;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
#[repr(usize)]
|
#[repr(usize)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum ParamId {
|
pub enum ParamId {
|
||||||
// Master
|
// OSC 1-3
|
||||||
MasterVolume = 0,
|
Osc1Gain = 0,
|
||||||
MasterPan,
|
|
||||||
|
|
||||||
// Oscillator 1
|
|
||||||
Osc1Volume,
|
|
||||||
Osc1Pan,
|
Osc1Pan,
|
||||||
Osc1Pitch,
|
Osc1Semitone,
|
||||||
Osc1Fine,
|
Osc1Fine,
|
||||||
Osc1WavePos,
|
Osc1UnisonCount,
|
||||||
Osc1UnisonVoices,
|
|
||||||
Osc1UnisonDetune,
|
Osc1UnisonDetune,
|
||||||
Osc1UnisonSpread,
|
Osc1UnisonSpread,
|
||||||
|
Osc1WavePos,
|
||||||
|
|
||||||
// Oscillator 2
|
Osc2Gain,
|
||||||
Osc2Volume,
|
|
||||||
Osc2Pan,
|
Osc2Pan,
|
||||||
Osc2Pitch,
|
Osc2Semitone,
|
||||||
Osc2Fine,
|
Osc2Fine,
|
||||||
Osc2WavePos,
|
Osc2UnisonCount,
|
||||||
Osc2UnisonVoices,
|
|
||||||
Osc2UnisonDetune,
|
Osc2UnisonDetune,
|
||||||
Osc2UnisonSpread,
|
Osc2UnisonSpread,
|
||||||
|
Osc2WavePos,
|
||||||
|
|
||||||
// Oscillator 3
|
Osc3Gain,
|
||||||
Osc3Volume,
|
|
||||||
Osc3Pan,
|
Osc3Pan,
|
||||||
Osc3Pitch,
|
Osc3Semitone,
|
||||||
Osc3Fine,
|
Osc3Fine,
|
||||||
Osc3WavePos,
|
Osc3UnisonCount,
|
||||||
Osc3UnisonVoices,
|
|
||||||
Osc3UnisonDetune,
|
Osc3UnisonDetune,
|
||||||
Osc3UnisonSpread,
|
Osc3UnisonSpread,
|
||||||
|
Osc3WavePos,
|
||||||
|
|
||||||
// Envelope 1 (amplitude)
|
// ENV 1-3
|
||||||
Env1Delay,
|
Env1Delay,
|
||||||
Env1Attack,
|
Env1Attack,
|
||||||
Env1Hold,
|
Env1Hold,
|
||||||
Env1Decay,
|
Env1Decay,
|
||||||
Env1Sustain,
|
Env1Sustain,
|
||||||
Env1Release,
|
Env1Release,
|
||||||
|
Env1AttackCurve,
|
||||||
|
Env1DecayCurve,
|
||||||
|
Env1ReleaseCurve,
|
||||||
|
|
||||||
// Envelope 2
|
|
||||||
Env2Delay,
|
Env2Delay,
|
||||||
Env2Attack,
|
Env2Attack,
|
||||||
Env2Hold,
|
Env2Hold,
|
||||||
Env2Decay,
|
Env2Decay,
|
||||||
Env2Sustain,
|
Env2Sustain,
|
||||||
Env2Release,
|
Env2Release,
|
||||||
|
Env2AttackCurve,
|
||||||
|
Env2DecayCurve,
|
||||||
|
Env2ReleaseCurve,
|
||||||
|
|
||||||
// Envelope 3
|
|
||||||
Env3Delay,
|
Env3Delay,
|
||||||
Env3Attack,
|
Env3Attack,
|
||||||
Env3Hold,
|
Env3Hold,
|
||||||
Env3Decay,
|
Env3Decay,
|
||||||
Env3Sustain,
|
Env3Sustain,
|
||||||
Env3Release,
|
Env3Release,
|
||||||
|
Env3AttackCurve,
|
||||||
|
Env3DecayCurve,
|
||||||
|
Env3ReleaseCurve,
|
||||||
|
|
||||||
// LFO 1–4
|
// LFO 1-4
|
||||||
Lfo1Rate,
|
Lfo1Rate,
|
||||||
Lfo1Depth,
|
|
||||||
Lfo1Phase,
|
Lfo1Phase,
|
||||||
|
Lfo1Depth,
|
||||||
|
Lfo1WavePos,
|
||||||
|
Lfo1Sync,
|
||||||
Lfo2Rate,
|
Lfo2Rate,
|
||||||
Lfo2Depth,
|
|
||||||
Lfo2Phase,
|
Lfo2Phase,
|
||||||
|
Lfo2Depth,
|
||||||
|
Lfo2WavePos,
|
||||||
|
Lfo2Sync,
|
||||||
Lfo3Rate,
|
Lfo3Rate,
|
||||||
Lfo3Depth,
|
|
||||||
Lfo3Phase,
|
Lfo3Phase,
|
||||||
|
Lfo3Depth,
|
||||||
|
Lfo3WavePos,
|
||||||
|
Lfo3Sync,
|
||||||
Lfo4Rate,
|
Lfo4Rate,
|
||||||
Lfo4Depth,
|
|
||||||
Lfo4Phase,
|
Lfo4Phase,
|
||||||
|
Lfo4Depth,
|
||||||
|
Lfo4WavePos,
|
||||||
|
Lfo4Sync,
|
||||||
|
|
||||||
// Filter 1
|
// Filter 1-2
|
||||||
Filter1Cutoff,
|
Filter1Cutoff,
|
||||||
Filter1Resonance,
|
Filter1Resonance,
|
||||||
Filter1Drive,
|
Filter1Drive,
|
||||||
Filter1Keytrack,
|
Filter1Keytrack,
|
||||||
|
Filter1Type,
|
||||||
// Filter 2
|
|
||||||
Filter2Cutoff,
|
Filter2Cutoff,
|
||||||
Filter2Resonance,
|
Filter2Resonance,
|
||||||
Filter2Drive,
|
Filter2Drive,
|
||||||
Filter2Keytrack,
|
Filter2Keytrack,
|
||||||
|
Filter2Type,
|
||||||
|
|
||||||
// FX — Chorus
|
// FX
|
||||||
|
DistDrive,
|
||||||
|
DistType,
|
||||||
ChorusRate,
|
ChorusRate,
|
||||||
ChorusDepth,
|
ChorusDepth,
|
||||||
ChorusMix,
|
ChorusMix,
|
||||||
|
PhaserRate,
|
||||||
// FX — Reverb
|
PhaserDepth,
|
||||||
|
PhaserMix,
|
||||||
ReverbSize,
|
ReverbSize,
|
||||||
ReverbDamping,
|
ReverbDamping,
|
||||||
ReverbMix,
|
ReverbMix,
|
||||||
|
|
||||||
// FX — Delay
|
|
||||||
DelayTime,
|
DelayTime,
|
||||||
DelayFeedback,
|
DelayFeedback,
|
||||||
DelayMix,
|
DelayMix,
|
||||||
|
|
||||||
// FX — Distortion
|
|
||||||
DistortionDrive,
|
|
||||||
DistortionMix,
|
|
||||||
|
|
||||||
// FX — EQ
|
|
||||||
EqLowGain,
|
EqLowGain,
|
||||||
EqMidFreq,
|
EqMidFreq,
|
||||||
EqMidGain,
|
EqMidGain,
|
||||||
@@ -124,83 +130,96 @@ pub enum ParamId {
|
|||||||
Macro7,
|
Macro7,
|
||||||
Macro8,
|
Macro8,
|
||||||
|
|
||||||
|
// Master
|
||||||
|
MasterVolume,
|
||||||
|
MasterPan,
|
||||||
|
Polyphony,
|
||||||
|
PortamentoTime,
|
||||||
|
PortamentoMode,
|
||||||
|
|
||||||
Count,
|
Count,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const PARAM_COUNT: usize = ParamId::Count as usize;
|
impl ParamId {
|
||||||
|
pub const COUNT: usize = ParamId::Count as usize;
|
||||||
|
|
||||||
pub fn default_value(id: ParamId) -> f32 {
|
pub fn default_value(self) -> f32 {
|
||||||
use ParamId::*;
|
match self {
|
||||||
match id {
|
Self::Osc1Gain | Self::Osc2Gain | Self::Osc3Gain => 1.0,
|
||||||
MasterVolume => 0.8,
|
Self::MasterVolume => 1.0,
|
||||||
MasterPan => 0.5,
|
Self::Filter1Cutoff | Self::Filter2Cutoff => 1.0,
|
||||||
|
Self::Filter1Resonance | Self::Filter2Resonance => 0.0,
|
||||||
|
Self::Env1Sustain | Self::Env2Sustain | Self::Env3Sustain => 1.0,
|
||||||
|
Self::Env1Attack | Self::Env2Attack | Self::Env3Attack => 0.01,
|
||||||
|
Self::Env1Decay | Self::Env2Decay | Self::Env3Decay => 0.3,
|
||||||
|
Self::Env1Release | Self::Env2Release | Self::Env3Release => 0.3,
|
||||||
|
Self::Polyphony => 1.0, // normalized: 1.0 = 16 voices
|
||||||
|
Self::ReverbMix => 0.15,
|
||||||
|
Self::ChorusMix => 0.0,
|
||||||
|
_ => 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Osc1Volume => 0.8,
|
pub fn label(self) -> &'static str {
|
||||||
Osc2Volume | Osc3Volume => 0.0, // off by default
|
match self {
|
||||||
Osc1Pan | Osc2Pan | Osc3Pan => 0.5,
|
Self::Osc1Gain => "Gain",
|
||||||
Osc1Pitch | Osc2Pitch | Osc3Pitch => 0.5,
|
Self::Osc1Pan => "Pan",
|
||||||
Osc1Fine | Osc2Fine | Osc3Fine => 0.5,
|
Self::Osc1Semitone => "Semi",
|
||||||
|
Self::Osc1Fine => "Fine",
|
||||||
Env1Attack | Env2Attack | Env3Attack => 0.02,
|
Self::Osc1UnisonCount => "Voices",
|
||||||
Env1Decay | Env2Decay | Env3Decay => 0.30,
|
Self::Osc1UnisonDetune => "Detune",
|
||||||
Env1Sustain | Env2Sustain | Env3Sustain => 0.70,
|
Self::Osc1UnisonSpread => "Spread",
|
||||||
Env1Release | Env2Release | Env3Release => 0.35,
|
Self::Osc1WavePos => "Wave",
|
||||||
|
Self::Filter1Cutoff => "Cutoff",
|
||||||
Filter1Cutoff | Filter2Cutoff => 1.0,
|
Self::Filter1Resonance => "Res",
|
||||||
Filter1Keytrack | Filter2Keytrack => 0.0,
|
Self::Filter1Drive => "Drive",
|
||||||
|
Self::Filter1Keytrack => "Key",
|
||||||
Lfo1Rate | Lfo2Rate | Lfo3Rate | Lfo4Rate => 0.3,
|
Self::Env1Attack => "A",
|
||||||
|
Self::Env1Decay => "D",
|
||||||
_ => 0.0,
|
Self::Env1Sustain => "S",
|
||||||
|
Self::Env1Release => "R",
|
||||||
|
Self::Lfo1Rate => "Rate",
|
||||||
|
Self::Lfo1Depth => "Depth",
|
||||||
|
Self::Macro1 => "M1",
|
||||||
|
Self::Macro2 => "M2",
|
||||||
|
Self::Macro3 => "M3",
|
||||||
|
Self::Macro4 => "M4",
|
||||||
|
Self::Macro5 => "M5",
|
||||||
|
Self::Macro6 => "M6",
|
||||||
|
Self::Macro7 => "M7",
|
||||||
|
Self::Macro8 => "M8",
|
||||||
|
Self::MasterVolume => "Vol",
|
||||||
|
_ => "—",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ParamStore {
|
pub struct ParamStore {
|
||||||
data: Arc<[AtomicU32]>,
|
params: Arc<Box<[AtomicF32]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParamStore {
|
impl ParamStore {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let data: Arc<[AtomicU32]> = (0..PARAM_COUNT)
|
let params = (0..ParamId::COUNT)
|
||||||
.map(|i| {
|
.map(|i| {
|
||||||
let id: ParamId = unsafe { std::mem::transmute(i) };
|
let id: ParamId = unsafe { std::mem::transmute(i) };
|
||||||
AtomicU32::new(default_value(id).to_bits())
|
AtomicF32::new(id.default_value())
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.into();
|
.into_boxed_slice();
|
||||||
Self { data }
|
Self {
|
||||||
|
params: Arc::new(params),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline]
|
||||||
pub fn get(&self, id: ParamId) -> f32 {
|
pub fn get(&self, id: ParamId) -> f32 {
|
||||||
f32::from_bits(self.data[id as usize].load(Ordering::Relaxed))
|
self.params[id as usize].load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
#[inline]
|
||||||
#[inline(always)]
|
pub fn set(&self, id: ParamId, v: f32) {
|
||||||
pub fn set(&self, id: ParamId, value: f32) {
|
self.params[id as usize].store(v, Ordering::Relaxed);
|
||||||
debug_assert!((0.0..=1.0).contains(&value), "param out of range: {value}");
|
|
||||||
self.data[id as usize].store(value.to_bits(), Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset_to_defaults(&self) {
|
|
||||||
for i in 0..PARAM_COUNT {
|
|
||||||
let id: ParamId = unsafe { std::mem::transmute(i) };
|
|
||||||
self.data[i].store(default_value(id).to_bits(), Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn snapshot(&self) -> Vec<f32> {
|
|
||||||
(0..PARAM_COUNT)
|
|
||||||
.map(|i| f32::from_bits(self.data[i].load(Ordering::Relaxed)))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn restore(&self, values: &[f32]) {
|
|
||||||
assert_eq!(values.len(), PARAM_COUNT, "snapshot length mismatch");
|
|
||||||
for (i, &v) in values.iter().enumerate() {
|
|
||||||
self.data[i].store(v.to_bits(), Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,70 +228,3 @@ impl Default for ParamStore {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ParamMeta {
|
|
||||||
pub name: &'static str,
|
|
||||||
pub label: &'static str,
|
|
||||||
pub steps: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn param_meta(id: ParamId) -> ParamMeta {
|
|
||||||
use ParamId::*;
|
|
||||||
match id {
|
|
||||||
MasterVolume => ParamMeta {
|
|
||||||
name: "Master Volume",
|
|
||||||
label: "%",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
MasterPan => ParamMeta {
|
|
||||||
name: "Master Pan",
|
|
||||||
label: "",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
Osc1Pitch => ParamMeta {
|
|
||||||
name: "Osc1 Pitch",
|
|
||||||
label: "st",
|
|
||||||
steps: Some(96),
|
|
||||||
},
|
|
||||||
Filter1Cutoff => ParamMeta {
|
|
||||||
name: "Filter1 Cutoff",
|
|
||||||
label: "Hz",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
Filter1Resonance => ParamMeta {
|
|
||||||
name: "Filter1 Res",
|
|
||||||
label: "",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
Env1Attack => ParamMeta {
|
|
||||||
name: "Env1 Attack",
|
|
||||||
label: "s",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
Env1Decay => ParamMeta {
|
|
||||||
name: "Env1 Decay",
|
|
||||||
label: "s",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
Env1Sustain => ParamMeta {
|
|
||||||
name: "Env1 Sustain",
|
|
||||||
label: "%",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
Env1Release => ParamMeta {
|
|
||||||
name: "Env1 Release",
|
|
||||||
label: "s",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
Macro1 => ParamMeta {
|
|
||||||
name: "Macro 1",
|
|
||||||
label: "",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
_ => ParamMeta {
|
|
||||||
name: "?",
|
|
||||||
label: "",
|
|
||||||
steps: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
136
crates/ui/src/app.rs
Normal file
136
crates/ui/src/app.rs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
use params::{ParamId, ParamStore};
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Data)]
|
||||||
|
pub enum Panel {
|
||||||
|
Osc,
|
||||||
|
Env,
|
||||||
|
Lfo,
|
||||||
|
Filter,
|
||||||
|
Fx,
|
||||||
|
ModMatrix,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Panel {
|
||||||
|
pub const ALL: &'static [Self] = &[
|
||||||
|
Self::Osc,
|
||||||
|
Self::Env,
|
||||||
|
Self::Lfo,
|
||||||
|
Self::Filter,
|
||||||
|
Self::Fx,
|
||||||
|
Self::ModMatrix,
|
||||||
|
];
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Osc => "OSC",
|
||||||
|
Self::Env => "ENV",
|
||||||
|
Self::Lfo => "LFO",
|
||||||
|
Self::Filter => "FLT",
|
||||||
|
Self::Fx => "FX",
|
||||||
|
Self::ModMatrix => "MOD",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Lens, Clone)]
|
||||||
|
pub struct AppData {
|
||||||
|
pub params: Vec<f32>,
|
||||||
|
pub active_panel: Panel,
|
||||||
|
pub preset_name: String,
|
||||||
|
pub voice_count: u8,
|
||||||
|
pub cpu_load: f32,
|
||||||
|
pub host_bpm: f32,
|
||||||
|
#[lens(ignore)]
|
||||||
|
pub store: ParamStore,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppData {
|
||||||
|
pub fn new(store: ParamStore) -> Self {
|
||||||
|
let params = (0..ParamId::COUNT)
|
||||||
|
.map(|i| store.get(unsafe { std::mem::transmute::<usize, ParamId>(i) }))
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
params,
|
||||||
|
store,
|
||||||
|
active_panel: Panel::Osc,
|
||||||
|
preset_name: "Init".into(),
|
||||||
|
voice_count: 0,
|
||||||
|
cpu_load: 0.0,
|
||||||
|
host_bpm: 120.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum AppEvent {
|
||||||
|
SetParam(ParamId, f32),
|
||||||
|
SetPanel(Panel),
|
||||||
|
UpdateMetrics { voices: u8, cpu: f32 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Model for AppData {
|
||||||
|
fn event(&mut self, _cx: &mut EventContext, event: &mut Event) {
|
||||||
|
event.map(|e: &AppEvent, _| match e {
|
||||||
|
AppEvent::SetParam(id, val) => {
|
||||||
|
let v = val.clamp(0.0, 1.0);
|
||||||
|
self.params[*id as usize] = v;
|
||||||
|
self.store.set(*id, v);
|
||||||
|
}
|
||||||
|
AppEvent::SetPanel(p) => self.active_panel = *p,
|
||||||
|
AppEvent::UpdateMetrics { voices, cpu } => {
|
||||||
|
self.voice_count = *voices;
|
||||||
|
self.cpu_load = *cpu;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build_root(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
crate::panels::header::build(cx);
|
||||||
|
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
for &p in Panel::ALL {
|
||||||
|
tab_button(cx, p);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.width(Pixels(56.0))
|
||||||
|
.height(Stretch(1.0))
|
||||||
|
.background_color(Color::rgb(14, 14, 26));
|
||||||
|
|
||||||
|
Binding::new(cx, AppData::active_panel, |cx, panel_lens| {
|
||||||
|
VStack::new(cx, |cx| match panel_lens.get(cx) {
|
||||||
|
Panel::Osc => crate::panels::osc::build(cx),
|
||||||
|
Panel::Env => crate::panels::env_panel::build(cx),
|
||||||
|
Panel::Lfo => crate::panels::lfo::build(cx),
|
||||||
|
Panel::Filter => crate::panels::filter::build(cx),
|
||||||
|
Panel::Fx => crate::panels::fx::build(cx),
|
||||||
|
Panel::ModMatrix => crate::panels::mod_matrix::build(cx),
|
||||||
|
})
|
||||||
|
.class("panel")
|
||||||
|
.width(Stretch(1.0))
|
||||||
|
.height(Stretch(1.0));
|
||||||
|
});
|
||||||
|
|
||||||
|
crate::panels::macro_bar::build(cx);
|
||||||
|
})
|
||||||
|
.height(Stretch(1.0));
|
||||||
|
})
|
||||||
|
.background_color(Color::rgb(18, 18, 28))
|
||||||
|
.width(Stretch(1.0))
|
||||||
|
.height(Stretch(1.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_button(cx: &mut Context, p: Panel) {
|
||||||
|
Binding::new(cx, AppData::active_panel, move |cx, active_lens| {
|
||||||
|
let al = active_lens.get(cx) == p;
|
||||||
|
Label::new(cx, p.label())
|
||||||
|
.class("tab")
|
||||||
|
.checked(al)
|
||||||
|
.on_press(move |cx| cx.emit(AppEvent::SetPanel(p)))
|
||||||
|
.width(Stretch(1.0))
|
||||||
|
.height(Pixels(44.0))
|
||||||
|
.text_align(TextAlign::Center);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
pub mod knob {}
|
pub mod app;
|
||||||
pub mod env_disp {}
|
pub mod panels;
|
||||||
pub mod wave_disp {}
|
pub mod widgets;
|
||||||
pub mod mod_matrix {}
|
|
||||||
pub mod spectrum {}
|
|
||||||
|
|
||||||
pub fn run_ui(_params: params::ParamStore) {
|
|
||||||
unimplemented!("stub")
|
|
||||||
}
|
|
||||||
|
|||||||
20
crates/ui/src/main.rs
Normal file
20
crates/ui/src/main.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
mod app;
|
||||||
|
mod panels;
|
||||||
|
mod widgets;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
use app::AppData;
|
||||||
|
use params::ParamStore;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
let _ = Application::new(|cx| {
|
||||||
|
cx.add_stylesheet(include_str!("theme.css"))
|
||||||
|
.expect("theme.css");
|
||||||
|
AppData::new(ParamStore::new()).build(cx);
|
||||||
|
app::build_root(cx);
|
||||||
|
})
|
||||||
|
.title("Tenko")
|
||||||
|
.inner_size((1280, 760))
|
||||||
|
.resizable(false)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
69
crates/ui/src/panels/env_panel.rs
Normal file
69
crates/ui/src/panels/env_panel.rs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
use crate::widgets::{env_display::EnvDisplay, knob::labeled_knob};
|
||||||
|
use params::ParamId;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
const ENV_KNOBS: [[ParamId; 9]; 3] = [
|
||||||
|
[
|
||||||
|
ParamId::Env1Delay,
|
||||||
|
ParamId::Env1Attack,
|
||||||
|
ParamId::Env1Hold,
|
||||||
|
ParamId::Env1Decay,
|
||||||
|
ParamId::Env1Sustain,
|
||||||
|
ParamId::Env1Release,
|
||||||
|
ParamId::Env1AttackCurve,
|
||||||
|
ParamId::Env1DecayCurve,
|
||||||
|
ParamId::Env1ReleaseCurve,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Env2Delay,
|
||||||
|
ParamId::Env2Attack,
|
||||||
|
ParamId::Env2Hold,
|
||||||
|
ParamId::Env2Decay,
|
||||||
|
ParamId::Env2Sustain,
|
||||||
|
ParamId::Env2Release,
|
||||||
|
ParamId::Env2AttackCurve,
|
||||||
|
ParamId::Env2DecayCurve,
|
||||||
|
ParamId::Env2ReleaseCurve,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Env3Delay,
|
||||||
|
ParamId::Env3Attack,
|
||||||
|
ParamId::Env3Hold,
|
||||||
|
ParamId::Env3Decay,
|
||||||
|
ParamId::Env3Sustain,
|
||||||
|
ParamId::Env3Release,
|
||||||
|
ParamId::Env3AttackCurve,
|
||||||
|
ParamId::Env3DecayCurve,
|
||||||
|
ParamId::Env3ReleaseCurve,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, "ENVELOPES")
|
||||||
|
.class("section-title")
|
||||||
|
.bottom(Pixels(6.0));
|
||||||
|
for (i, knobs) in ENV_KNOBS.iter().enumerate() {
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, format!("ENV {}", i + 1)).class("knob-label");
|
||||||
|
EnvDisplay::new(cx, i);
|
||||||
|
})
|
||||||
|
.width(Pixels(218.0));
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for &p in knobs.iter() {
|
||||||
|
labeled_knob(cx, p);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(2.0))
|
||||||
|
.left(Pixels(10.0));
|
||||||
|
})
|
||||||
|
.background_color(Color::from("#1a1a28"))
|
||||||
|
.corner_radius(Pixels(4.0))
|
||||||
|
.padding(Pixels(8.0))
|
||||||
|
.height(Pixels(108.0));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.vertical_gap(Pixels(8.0))
|
||||||
|
.padding(Pixels(12.0));
|
||||||
|
}
|
||||||
71
crates/ui/src/panels/filter.rs
Normal file
71
crates/ui/src/panels/filter.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
use crate::widgets::knob::labeled_knob;
|
||||||
|
use params::ParamId;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
const FILTER_KNOBS: [[ParamId; 4]; 2] = [
|
||||||
|
[
|
||||||
|
ParamId::Filter1Cutoff,
|
||||||
|
ParamId::Filter1Resonance,
|
||||||
|
ParamId::Filter1Drive,
|
||||||
|
ParamId::Filter1Keytrack,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Filter2Cutoff,
|
||||||
|
ParamId::Filter2Resonance,
|
||||||
|
ParamId::Filter2Drive,
|
||||||
|
ParamId::Filter2Keytrack,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
const TYPES: [&str; 4] = ["LADDER", "SVF", "COMB", "FORMANT"];
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, "FILTERS")
|
||||||
|
.class("section-title")
|
||||||
|
.bottom(Pixels(6.0));
|
||||||
|
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for m in ["SERIAL", "PARALLEL"] {
|
||||||
|
Label::new(cx, m).class("tab").padding(Pixels(5.0));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.bottom(Pixels(8.0));
|
||||||
|
|
||||||
|
for (i, knobs) in FILTER_KNOBS.iter().enumerate() {
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, format!("FILTER {}", i + 1)).class("knob-label");
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for t in TYPES {
|
||||||
|
Label::new(cx, t)
|
||||||
|
.class("tab")
|
||||||
|
.padding(Pixels(3.0))
|
||||||
|
.height(Pixels(20.0));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.top(Pixels(4.0));
|
||||||
|
Element::new(cx)
|
||||||
|
.width(Pixels(170.0))
|
||||||
|
.height(Pixels(52.0))
|
||||||
|
.background_color(Color::from("#12121c"))
|
||||||
|
.corner_radius(Pixels(3.0))
|
||||||
|
.top(Pixels(4.0));
|
||||||
|
})
|
||||||
|
.width(Pixels(178.0));
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for &p in knobs.iter() {
|
||||||
|
labeled_knob(cx, p);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(2.0))
|
||||||
|
.left(Pixels(10.0));
|
||||||
|
})
|
||||||
|
.background_color(Color::from("#1a1a28"))
|
||||||
|
.corner_radius(Pixels(4.0))
|
||||||
|
.padding(Pixels(8.0))
|
||||||
|
.height(Pixels(120.0));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.vertical_gap(Pixels(8.0))
|
||||||
|
.padding(Pixels(12.0));
|
||||||
|
}
|
||||||
95
crates/ui/src/panels/fx.rs
Normal file
95
crates/ui/src/panels/fx.rs
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
use crate::widgets::knob::labeled_knob;
|
||||||
|
use params::ParamId;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
struct Slot {
|
||||||
|
name: &'static str,
|
||||||
|
params: &'static [ParamId],
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHAIN: &[Slot] = &[
|
||||||
|
Slot {
|
||||||
|
name: "DIST",
|
||||||
|
params: &[ParamId::DistDrive],
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
name: "CHORUS",
|
||||||
|
params: &[
|
||||||
|
ParamId::ChorusRate,
|
||||||
|
ParamId::ChorusDepth,
|
||||||
|
ParamId::ChorusMix,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
name: "PHASER",
|
||||||
|
params: &[
|
||||||
|
ParamId::PhaserRate,
|
||||||
|
ParamId::PhaserDepth,
|
||||||
|
ParamId::PhaserMix,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
name: "REVERB",
|
||||||
|
params: &[
|
||||||
|
ParamId::ReverbSize,
|
||||||
|
ParamId::ReverbDamping,
|
||||||
|
ParamId::ReverbMix,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
name: "DELAY",
|
||||||
|
params: &[
|
||||||
|
ParamId::DelayTime,
|
||||||
|
ParamId::DelayFeedback,
|
||||||
|
ParamId::DelayMix,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Slot {
|
||||||
|
name: "EQ",
|
||||||
|
params: &[
|
||||||
|
ParamId::EqLowGain,
|
||||||
|
ParamId::EqMidFreq,
|
||||||
|
ParamId::EqMidGain,
|
||||||
|
ParamId::EqHighGain,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, "FX CHAIN")
|
||||||
|
.class("section-title")
|
||||||
|
.bottom(Pixels(6.0));
|
||||||
|
for slot in CHAIN {
|
||||||
|
fx_row(cx, slot);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.vertical_gap(Pixels(5.0))
|
||||||
|
.padding(Pixels(12.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fx_row(cx: &mut Context, slot: &'static Slot) {
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
Element::new(cx)
|
||||||
|
.width(Pixels(10.0))
|
||||||
|
.height(Pixels(10.0))
|
||||||
|
.background_color(Color::from("#ff7830"))
|
||||||
|
.corner_radius(Pixels(5.0));
|
||||||
|
Label::new(cx, slot.name)
|
||||||
|
.color(Color::from("#d8d8e8"))
|
||||||
|
.width(Pixels(55.0))
|
||||||
|
.left(Pixels(8.0));
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for &p in slot.params {
|
||||||
|
labeled_knob(cx, p);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(2.0));
|
||||||
|
})
|
||||||
|
.background_color(Color::from("#1a1a28"))
|
||||||
|
.corner_radius(Pixels(4.0))
|
||||||
|
.padding(Pixels(8.0))
|
||||||
|
.height(Pixels(68.0))
|
||||||
|
.padding_top(Stretch(1.0))
|
||||||
|
.padding_bottom(Stretch(1.0));
|
||||||
|
}
|
||||||
38
crates/ui/src/panels/header.rs
Normal file
38
crates/ui/src/panels/header.rs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
use crate::app::AppData;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, AppData::preset_name)
|
||||||
|
.font_size(13.0)
|
||||||
|
.color(Color::from("#d8d8e8"))
|
||||||
|
.width(Pixels(180.0));
|
||||||
|
|
||||||
|
Element::new(cx).width(Stretch(1.0)).height(Stretch(1.0));
|
||||||
|
|
||||||
|
stat_pair(cx, "BPM", AppData::host_bpm.map(|b| format!("{b:.0}")));
|
||||||
|
stat_pair(cx, "VOICES", AppData::voice_count.map(|v| format!("{v}")));
|
||||||
|
stat_pair(
|
||||||
|
cx,
|
||||||
|
"CPU",
|
||||||
|
AppData::cpu_load.map(|c| format!("{:.0}%", c * 100.0)),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.height(Pixels(34.0))
|
||||||
|
.background_color(Color::from("#0e0e1a"))
|
||||||
|
.padding(Pixels(8.0))
|
||||||
|
.padding_top(Stretch(1.0))
|
||||||
|
.padding_bottom(Stretch(1.0))
|
||||||
|
.horizontal_gap(Pixels(16.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stat_pair(cx: &mut Context, key: &'static str, val_lens: impl Lens<Target = String>) {
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, key)
|
||||||
|
.class("knob-label")
|
||||||
|
.color(Color::from("#888899"));
|
||||||
|
Label::new(cx, val_lens)
|
||||||
|
.color(Color::from("#ff7830"))
|
||||||
|
.left(Pixels(4.0));
|
||||||
|
});
|
||||||
|
}
|
||||||
69
crates/ui/src/panels/lfo.rs
Normal file
69
crates/ui/src/panels/lfo.rs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
use crate::widgets::{knob::labeled_knob, wavetable_display::WavetableDisplay};
|
||||||
|
use params::ParamId;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
const LFO_KNOBS: [[ParamId; 4]; 4] = [
|
||||||
|
[
|
||||||
|
ParamId::Lfo1Rate,
|
||||||
|
ParamId::Lfo1Phase,
|
||||||
|
ParamId::Lfo1Depth,
|
||||||
|
ParamId::Lfo1WavePos,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Lfo2Rate,
|
||||||
|
ParamId::Lfo2Phase,
|
||||||
|
ParamId::Lfo2Depth,
|
||||||
|
ParamId::Lfo2WavePos,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Lfo3Rate,
|
||||||
|
ParamId::Lfo3Phase,
|
||||||
|
ParamId::Lfo3Depth,
|
||||||
|
ParamId::Lfo3WavePos,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Lfo4Rate,
|
||||||
|
ParamId::Lfo4Phase,
|
||||||
|
ParamId::Lfo4Depth,
|
||||||
|
ParamId::Lfo4WavePos,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, "LFOs")
|
||||||
|
.class("section-title")
|
||||||
|
.bottom(Pixels(6.0));
|
||||||
|
for (i, knobs) in LFO_KNOBS.iter().enumerate() {
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, format!("LFO {}", i + 1)).class("knob-label");
|
||||||
|
let sine: Vec<f32> = (0..96)
|
||||||
|
.map(|j| (j as f32 / 96.0 * std::f32::consts::TAU).sin())
|
||||||
|
.collect();
|
||||||
|
WavetableDisplay::new(cx, sine)
|
||||||
|
.width(Pixels(110.0))
|
||||||
|
.height(Pixels(52.0));
|
||||||
|
})
|
||||||
|
.width(Pixels(118.0));
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for &p in knobs.iter() {
|
||||||
|
labeled_knob(cx, p);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(2.0))
|
||||||
|
.left(Pixels(10.0));
|
||||||
|
Label::new(cx, "SYNC").class("knob-label").left(Pixels(8.0));
|
||||||
|
})
|
||||||
|
.background_color(Color::from("#1a1a28"))
|
||||||
|
.corner_radius(Pixels(4.0))
|
||||||
|
.padding(Pixels(8.0))
|
||||||
|
.height(Pixels(84.0));
|
||||||
|
}
|
||||||
|
Button::new(cx, |cx| Label::new(cx, "+ LFO"))
|
||||||
|
.class("add-btn")
|
||||||
|
.top(Pixels(6.0));
|
||||||
|
})
|
||||||
|
.vertical_gap(Pixels(8.0))
|
||||||
|
.padding(Pixels(12.0));
|
||||||
|
}
|
||||||
33
crates/ui/src/panels/macro_bar.rs
Normal file
33
crates/ui/src/panels/macro_bar.rs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
use crate::widgets::knob::labeled_knob;
|
||||||
|
use params::ParamId;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
const MACROS: [ParamId; 8] = [
|
||||||
|
ParamId::Macro1,
|
||||||
|
ParamId::Macro2,
|
||||||
|
ParamId::Macro3,
|
||||||
|
ParamId::Macro4,
|
||||||
|
ParamId::Macro5,
|
||||||
|
ParamId::Macro6,
|
||||||
|
ParamId::Macro7,
|
||||||
|
ParamId::Macro8,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, "MACRO")
|
||||||
|
.class("section-title")
|
||||||
|
.bottom(Pixels(6.0));
|
||||||
|
for &m in &MACROS {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
labeled_knob(cx, m);
|
||||||
|
})
|
||||||
|
.class("macro-knob")
|
||||||
|
.bottom(Pixels(3.0));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.width(Pixels(68.0))
|
||||||
|
.background_color(Color::from("#16162a"))
|
||||||
|
.padding(Pixels(8.0))
|
||||||
|
.vertical_gap(Pixels(2.0));
|
||||||
|
}
|
||||||
8
crates/ui/src/panels/mod.rs
Normal file
8
crates/ui/src/panels/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
pub mod env_panel;
|
||||||
|
pub mod filter;
|
||||||
|
pub mod fx;
|
||||||
|
pub mod header;
|
||||||
|
pub mod lfo;
|
||||||
|
pub mod macro_bar;
|
||||||
|
pub mod mod_matrix;
|
||||||
|
pub mod osc;
|
||||||
61
crates/ui/src/panels/mod_matrix.rs
Normal file
61
crates/ui/src/panels/mod_matrix.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
const SOURCES: &[&str] = &[
|
||||||
|
"ENV1", "ENV2", "ENV3", "LFO1", "LFO2", "LFO3", "LFO4", "VEL", "NOTE", "MOD", "AFT", "M1",
|
||||||
|
"M2", "M3", "M4", "M5", "M6", "M7", "M8",
|
||||||
|
];
|
||||||
|
const DESTS: &[&str] = &[
|
||||||
|
"CUT1", "RES1", "CUT2", "RES2", "P1", "P2", "P3", "AMP", "PAN", "LR1", "LR2",
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, "MOD MATRIX — 64 slots")
|
||||||
|
.class("section-title")
|
||||||
|
.bottom(Pixels(6.0));
|
||||||
|
Label::new(cx, "Click cell → depth · Ctrl+click → clear")
|
||||||
|
.class("knob-label")
|
||||||
|
.bottom(Pixels(8.0));
|
||||||
|
|
||||||
|
ScrollView::new(cx, |cx| {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
// column header
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
Element::new(cx).width(Pixels(48.0));
|
||||||
|
for &d in DESTS {
|
||||||
|
Label::new(cx, d)
|
||||||
|
.class("knob-label")
|
||||||
|
.width(Pixels(38.0))
|
||||||
|
.text_align(TextAlign::Center);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.height(Pixels(20.0));
|
||||||
|
|
||||||
|
for &src in SOURCES {
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, src).class("knob-label").width(Pixels(48.0));
|
||||||
|
for _ in DESTS {
|
||||||
|
mod_cell(cx);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.height(Pixels(34.0))
|
||||||
|
.padding_top(Stretch(1.0))
|
||||||
|
.padding_bottom(Stretch(1.0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.padding(Pixels(12.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mod_cell(cx: &mut Context) {
|
||||||
|
Element::new(cx)
|
||||||
|
.width(Pixels(34.0))
|
||||||
|
.height(Pixels(26.0))
|
||||||
|
.background_color(Color::rgb(30, 30, 48))
|
||||||
|
.border_color(Color::rgb(42, 42, 68))
|
||||||
|
.border_width(Pixels(1.0))
|
||||||
|
.corner_radius(Pixels(3.0))
|
||||||
|
.left(Pixels(2.0))
|
||||||
|
.cursor(CursorIcon::Hand);
|
||||||
|
}
|
||||||
76
crates/ui/src/panels/osc.rs
Normal file
76
crates/ui/src/panels/osc.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use crate::widgets::{knob::labeled_knob, wavetable_display::WavetableDisplay};
|
||||||
|
use params::ParamId;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
|
||||||
|
const OSC_KNOBS: [[ParamId; 8]; 3] = [
|
||||||
|
[
|
||||||
|
ParamId::Osc1Gain,
|
||||||
|
ParamId::Osc1Pan,
|
||||||
|
ParamId::Osc1Semitone,
|
||||||
|
ParamId::Osc1Fine,
|
||||||
|
ParamId::Osc1UnisonCount,
|
||||||
|
ParamId::Osc1UnisonDetune,
|
||||||
|
ParamId::Osc1UnisonSpread,
|
||||||
|
ParamId::Osc1WavePos,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Osc2Gain,
|
||||||
|
ParamId::Osc2Pan,
|
||||||
|
ParamId::Osc2Semitone,
|
||||||
|
ParamId::Osc2Fine,
|
||||||
|
ParamId::Osc2UnisonCount,
|
||||||
|
ParamId::Osc2UnisonDetune,
|
||||||
|
ParamId::Osc2UnisonSpread,
|
||||||
|
ParamId::Osc2WavePos,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
ParamId::Osc3Gain,
|
||||||
|
ParamId::Osc3Pan,
|
||||||
|
ParamId::Osc3Semitone,
|
||||||
|
ParamId::Osc3Fine,
|
||||||
|
ParamId::Osc3UnisonCount,
|
||||||
|
ParamId::Osc3UnisonDetune,
|
||||||
|
ParamId::Osc3UnisonSpread,
|
||||||
|
ParamId::Osc3WavePos,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn build(cx: &mut Context) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, "OSCILLATORS")
|
||||||
|
.class("section-title")
|
||||||
|
.bottom(Pixels(6.0));
|
||||||
|
for (i, knobs) in OSC_KNOBS.iter().enumerate() {
|
||||||
|
osc_row(cx, i + 1, knobs);
|
||||||
|
}
|
||||||
|
Button::new(cx, |cx| Label::new(cx, "+ OSC"))
|
||||||
|
.class("add-btn")
|
||||||
|
.top(Pixels(6.0));
|
||||||
|
})
|
||||||
|
.vertical_gap(Pixels(8.0))
|
||||||
|
.padding(Pixels(12.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn osc_row(cx: &mut Context, idx: usize, knobs: &'static [ParamId; 8]) {
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
Label::new(cx, format!("OSC {idx}")).class("knob-label");
|
||||||
|
let sine: Vec<f32> = (0..128)
|
||||||
|
.map(|i| (i as f32 / 128.0 * std::f32::consts::TAU).sin())
|
||||||
|
.collect();
|
||||||
|
WavetableDisplay::new(cx, sine);
|
||||||
|
})
|
||||||
|
.width(Pixels(158.0));
|
||||||
|
HStack::new(cx, |cx| {
|
||||||
|
for &p in knobs {
|
||||||
|
labeled_knob(cx, p);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.horizontal_gap(Pixels(2.0))
|
||||||
|
.left(Pixels(10.0));
|
||||||
|
})
|
||||||
|
.background_color(Color::from("#1a1a28"))
|
||||||
|
.corner_radius(Pixels(4.0))
|
||||||
|
.padding(Pixels(8.0))
|
||||||
|
.height(Pixels(98.0));
|
||||||
|
}
|
||||||
49
crates/ui/src/theme.css
Normal file
49
crates/ui/src/theme.css
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
font-family: "Inter", "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background-color: #1a1a28;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
background-color: #22223a;
|
||||||
|
color: #888899;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 4px 4px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background-color: #1a1a28;
|
||||||
|
color: #ff7830;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knob-label {
|
||||||
|
color: #888899;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
color: #ff7830;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-knob {
|
||||||
|
background-color: #22223a;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-btn {
|
||||||
|
background-color: #22223a;
|
||||||
|
color: #888899;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 10px;
|
||||||
|
}
|
||||||
111
crates/ui/src/widgets/env_display.rs
Normal file
111
crates/ui/src/widgets/env_display.rs
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
use params::ParamId;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
use vizia::vg;
|
||||||
|
|
||||||
|
pub struct EnvDisplay {
|
||||||
|
pub delay: f32,
|
||||||
|
pub attack: f32,
|
||||||
|
pub hold: f32,
|
||||||
|
pub decay: f32,
|
||||||
|
pub sustain: f32,
|
||||||
|
pub release: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvDisplay {
|
||||||
|
pub fn new(cx: &mut Context, env_idx: usize) -> Handle<Self> {
|
||||||
|
let ids = env_param_ids(env_idx);
|
||||||
|
let g = |id: ParamId| {
|
||||||
|
cx.data::<crate::app::AppData>()
|
||||||
|
.map(|d| d.params[id as usize])
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
};
|
||||||
|
Self {
|
||||||
|
delay: g(ids[0]),
|
||||||
|
attack: g(ids[1]),
|
||||||
|
hold: g(ids[2]),
|
||||||
|
decay: g(ids[3]),
|
||||||
|
sustain: g(ids[4]),
|
||||||
|
release: g(ids[5]),
|
||||||
|
}
|
||||||
|
.build(cx, |_| {})
|
||||||
|
.width(Pixels(210.0))
|
||||||
|
.height(Pixels(72.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_param_ids(idx: usize) -> [ParamId; 6] {
|
||||||
|
match idx {
|
||||||
|
0 => [
|
||||||
|
ParamId::Env1Delay,
|
||||||
|
ParamId::Env1Attack,
|
||||||
|
ParamId::Env1Hold,
|
||||||
|
ParamId::Env1Decay,
|
||||||
|
ParamId::Env1Sustain,
|
||||||
|
ParamId::Env1Release,
|
||||||
|
],
|
||||||
|
1 => [
|
||||||
|
ParamId::Env2Delay,
|
||||||
|
ParamId::Env2Attack,
|
||||||
|
ParamId::Env2Hold,
|
||||||
|
ParamId::Env2Decay,
|
||||||
|
ParamId::Env2Sustain,
|
||||||
|
ParamId::Env2Release,
|
||||||
|
],
|
||||||
|
_ => [
|
||||||
|
ParamId::Env3Delay,
|
||||||
|
ParamId::Env3Attack,
|
||||||
|
ParamId::Env3Hold,
|
||||||
|
ParamId::Env3Decay,
|
||||||
|
ParamId::Env3Sustain,
|
||||||
|
ParamId::Env3Release,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for EnvDisplay {
|
||||||
|
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
||||||
|
let b = cx.bounds();
|
||||||
|
let mut p = vg::Paint::default();
|
||||||
|
p.set_color(vg::Color::from_argb(255, 18, 18, 30));
|
||||||
|
canvas.draw_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), &p);
|
||||||
|
|
||||||
|
let total =
|
||||||
|
(self.delay + self.attack + self.hold + self.decay + 0.25 + self.release).max(0.001);
|
||||||
|
let sx = |t: f32| b.x + (t / total) * b.w;
|
||||||
|
let top = b.y + 6.0;
|
||||||
|
let bot = b.y + b.h - 6.0;
|
||||||
|
let sy = top + (1.0 - self.sustain) * (bot - top);
|
||||||
|
|
||||||
|
let mut t = 0.0f32;
|
||||||
|
let x0 = sx(t);
|
||||||
|
t += self.delay;
|
||||||
|
let x1 = sx(t);
|
||||||
|
t += self.attack;
|
||||||
|
let x2 = sx(t);
|
||||||
|
t += self.hold;
|
||||||
|
let x3 = sx(t);
|
||||||
|
t += self.decay;
|
||||||
|
let x4 = sx(t);
|
||||||
|
t += 0.25;
|
||||||
|
let x5 = sx(t);
|
||||||
|
t += self.release;
|
||||||
|
let x6 = sx(t);
|
||||||
|
|
||||||
|
let mut path = vg::Path::new();
|
||||||
|
path.move_to((x0, bot));
|
||||||
|
path.line_to((x1, bot));
|
||||||
|
path.line_to((x2, top));
|
||||||
|
path.line_to((x3, top));
|
||||||
|
path.line_to((x4, sy));
|
||||||
|
path.line_to((x5, sy));
|
||||||
|
path.line_to((x6, bot));
|
||||||
|
|
||||||
|
p.set_style(vg::PaintStyle::Stroke);
|
||||||
|
p.set_color(vg::Color::from_argb(255, 255, 120, 48));
|
||||||
|
p.set_stroke_width(2.0);
|
||||||
|
p.set_stroke_cap(vg::paint::Cap::Round);
|
||||||
|
p.set_stroke_join(vg::paint::Join::Round);
|
||||||
|
p.set_anti_alias(true);
|
||||||
|
canvas.draw_path(&path, &p);
|
||||||
|
}
|
||||||
|
}
|
||||||
135
crates/ui/src/widgets/knob.rs
Normal file
135
crates/ui/src/widgets/knob.rs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
use crate::app::AppEvent;
|
||||||
|
use params::ParamId;
|
||||||
|
use std::f32::consts::PI;
|
||||||
|
use vizia::prelude::*;
|
||||||
|
use vizia::vg;
|
||||||
|
|
||||||
|
const ARC_START: f32 = PI * 0.75;
|
||||||
|
const ARC_RANGE: f32 = PI * 1.5;
|
||||||
|
|
||||||
|
pub struct TenkoKnob {
|
||||||
|
pub param: ParamId,
|
||||||
|
value: f32,
|
||||||
|
drag_origin_y: Option<f32>,
|
||||||
|
drag_origin_v: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TenkoKnob {
|
||||||
|
pub fn new(cx: &mut Context, param: ParamId) -> Handle<Self> {
|
||||||
|
let init = cx
|
||||||
|
.data::<crate::app::AppData>()
|
||||||
|
.map(|d| d.params[param as usize])
|
||||||
|
.unwrap_or(param.default_value());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
param,
|
||||||
|
value: init,
|
||||||
|
drag_origin_y: None,
|
||||||
|
drag_origin_v: 0.0,
|
||||||
|
}
|
||||||
|
.build(cx, |_| {})
|
||||||
|
.bind(
|
||||||
|
crate::app::AppData::params.idx(param as usize),
|
||||||
|
|h, lens| {
|
||||||
|
let v = lens.get(&h);
|
||||||
|
h.modify(|k: &mut TenkoKnob| k.value = v);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.width(Pixels(46.0))
|
||||||
|
.height(Pixels(46.0))
|
||||||
|
.cursor(CursorIcon::NsResize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for TenkoKnob {
|
||||||
|
fn element(&self) -> Option<&'static str> {
|
||||||
|
Some("tenko-knob")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
|
||||||
|
event.map(|we: &WindowEvent, _| match we {
|
||||||
|
WindowEvent::MouseDown(MouseButton::Left) => {
|
||||||
|
cx.capture();
|
||||||
|
self.drag_origin_y = Some(cx.mouse().cursor_y);
|
||||||
|
self.drag_origin_v = self.value;
|
||||||
|
}
|
||||||
|
WindowEvent::MouseUp(MouseButton::Left) => {
|
||||||
|
cx.release();
|
||||||
|
self.drag_origin_y = None;
|
||||||
|
}
|
||||||
|
WindowEvent::MouseMove(_, y) => {
|
||||||
|
if let Some(oy) = self.drag_origin_y {
|
||||||
|
let sens = if cx.modifiers().shift() {
|
||||||
|
0.0015
|
||||||
|
} else {
|
||||||
|
0.004
|
||||||
|
};
|
||||||
|
let new_v = (self.drag_origin_v + (oy - y) * sens).clamp(0.0, 1.0);
|
||||||
|
cx.emit(AppEvent::SetParam(self.param, new_v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
WindowEvent::MouseDoubleClick(MouseButton::Left) => {
|
||||||
|
cx.emit(AppEvent::SetParam(self.param, self.param.default_value()));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
||||||
|
let b = cx.bounds();
|
||||||
|
let kx = b.x + b.w * 0.5;
|
||||||
|
let ky = b.y + b.h * 0.5;
|
||||||
|
let r = b.w.min(b.h) * 0.5 - 5.0;
|
||||||
|
let val_end = ARC_START + self.value * ARC_RANGE;
|
||||||
|
|
||||||
|
let mut paint = vg::Paint::default();
|
||||||
|
paint.set_anti_alias(true);
|
||||||
|
paint.set_style(vg::PaintStyle::Stroke);
|
||||||
|
paint.set_stroke_width(3.5);
|
||||||
|
paint.set_stroke_cap(vg::paint::Cap::Round);
|
||||||
|
paint.set_color(vg::Color::from_argb(255, 45, 45, 65));
|
||||||
|
|
||||||
|
let track_rect = vg::Rect::from_xywh(kx - r, ky - r, r * 2.0, r * 2.0);
|
||||||
|
let mut path = vg::Path::new();
|
||||||
|
path.add_arc(track_rect, ARC_START.to_degrees(), ARC_RANGE.to_degrees());
|
||||||
|
canvas.draw_path(&path, &paint);
|
||||||
|
|
||||||
|
if self.value > 0.001 {
|
||||||
|
paint.set_color(vg::Color::from_argb(255, 255, 120, 48));
|
||||||
|
let mut vpath = vg::Path::new();
|
||||||
|
vpath.add_arc(
|
||||||
|
track_rect,
|
||||||
|
ARC_START.to_degrees(),
|
||||||
|
(self.value * ARC_RANGE).to_degrees(),
|
||||||
|
);
|
||||||
|
canvas.draw_path(&vpath, &paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
paint.set_style(vg::PaintStyle::Fill);
|
||||||
|
paint.set_color(vg::Color::from_argb(255, 30, 30, 48));
|
||||||
|
canvas.draw_circle((kx, ky), r - 6.0, &paint);
|
||||||
|
|
||||||
|
let px = kx + val_end.cos() * (r - 9.0);
|
||||||
|
let py = ky + val_end.sin() * (r - 9.0);
|
||||||
|
paint.set_style(vg::PaintStyle::Stroke);
|
||||||
|
paint.set_stroke_width(2.0);
|
||||||
|
paint.set_color(vg::Color::from_argb(255, 220, 220, 235));
|
||||||
|
let mut ptr = vg::Path::new();
|
||||||
|
ptr.move_to((kx, ky));
|
||||||
|
ptr.line_to((px, py));
|
||||||
|
canvas.draw_path(&ptr, &paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn labeled_knob(cx: &mut Context, param: ParamId) {
|
||||||
|
VStack::new(cx, |cx| {
|
||||||
|
TenkoKnob::new(cx, param);
|
||||||
|
Label::new(cx, param.label())
|
||||||
|
.class("knob-label")
|
||||||
|
.text_align(TextAlign::Center)
|
||||||
|
.width(Stretch(1.0));
|
||||||
|
})
|
||||||
|
.width(Pixels(50.0))
|
||||||
|
.height(Pixels(62.0))
|
||||||
|
.padding(Pixels(2.0));
|
||||||
|
}
|
||||||
3
crates/ui/src/widgets/mod.rs
Normal file
3
crates/ui/src/widgets/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
pub mod env_display;
|
||||||
|
pub mod knob;
|
||||||
|
pub mod wavetable_display;
|
||||||
54
crates/ui/src/widgets/wavetable_display.rs
Normal file
54
crates/ui/src/widgets/wavetable_display.rs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
use vizia::prelude::*;
|
||||||
|
use vizia::vg;
|
||||||
|
|
||||||
|
pub struct WavetableDisplay {
|
||||||
|
pub samples: Vec<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WavetableDisplay {
|
||||||
|
pub fn new(cx: &mut Context, samples: Vec<f32>) -> Handle<Self> {
|
||||||
|
Self { samples }
|
||||||
|
.build(cx, |_| {})
|
||||||
|
.width(Pixels(150.0))
|
||||||
|
.height(Pixels(68.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for WavetableDisplay {
|
||||||
|
fn draw(&self, cx: &mut DrawContext, canvas: &Canvas) {
|
||||||
|
let b = cx.bounds();
|
||||||
|
|
||||||
|
let mut p = vg::Paint::default();
|
||||||
|
p.set_color(vg::Color::from_argb(255, 18, 18, 30));
|
||||||
|
canvas.draw_rect(vg::Rect::from_xywh(b.x, b.y, b.w, b.h), &p);
|
||||||
|
|
||||||
|
p.set_color(vg::Color::from_argb(40, 255, 255, 255));
|
||||||
|
p.set_style(vg::PaintStyle::Stroke);
|
||||||
|
p.set_stroke_width(1.0);
|
||||||
|
let mid_y = b.y + b.h * 0.5;
|
||||||
|
let mut zl = vg::Path::new();
|
||||||
|
zl.move_to((b.x, mid_y));
|
||||||
|
zl.line_to((b.x + b.w, mid_y));
|
||||||
|
canvas.draw_path(&zl, &p);
|
||||||
|
|
||||||
|
if self.samples.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let n = self.samples.len() as f32;
|
||||||
|
|
||||||
|
let mut wave = vg::Path::new();
|
||||||
|
for (i, &s) in self.samples.iter().enumerate() {
|
||||||
|
let x = b.x + (i as f32 / n) * b.w;
|
||||||
|
let y = mid_y - s * b.h * 0.44;
|
||||||
|
if i == 0 {
|
||||||
|
wave.move_to((x, y));
|
||||||
|
} else {
|
||||||
|
wave.line_to((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.set_color(vg::Color::from_argb(255, 255, 120, 48));
|
||||||
|
p.set_stroke_width(1.5);
|
||||||
|
p.set_anti_alias(true);
|
||||||
|
canvas.draw_path(&wave, &p);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
println!("Hello, world!");
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user