somewhat working ui
This commit is contained in:
@@ -6,3 +6,6 @@ edition.workspace = true
|
||||
[dependencies]
|
||||
params = { workspace = true }
|
||||
vizia = { workspace = true }
|
||||
engine = { workspace = true}
|
||||
crossbeam-channel = { workspace = true }
|
||||
cpal = { workspace = true }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crossbeam_channel::Sender;
|
||||
use params::{ParamId, ParamStore};
|
||||
use vizia::prelude::*;
|
||||
|
||||
@@ -6,6 +7,13 @@ use crate::widgets::piano::PianoKeyboard;
|
||||
pub const MOD_NSRC: usize = 19;
|
||||
pub const MOD_NDST: usize = 11;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum MidiMsg {
|
||||
NoteOn(u8, u8),
|
||||
NoteOff(u8),
|
||||
AllNotesOff,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Data)]
|
||||
pub enum Panel {
|
||||
Voice,
|
||||
@@ -41,10 +49,12 @@ pub struct AppData {
|
||||
pub active_env: usize,
|
||||
pub active_lfo: usize,
|
||||
pub fx_selected: usize,
|
||||
#[lens(ignore)]
|
||||
pub midi_tx: Sender<MidiMsg>,
|
||||
}
|
||||
|
||||
impl AppData {
|
||||
pub fn new(store: ParamStore) -> Self {
|
||||
pub fn new(store: ParamStore, midi_tx: Sender<MidiMsg>) -> Self {
|
||||
let params = (0..ParamId::COUNT)
|
||||
.map(|i| store.get(unsafe { std::mem::transmute::<usize, ParamId>(i) }))
|
||||
.collect();
|
||||
@@ -63,6 +73,7 @@ impl AppData {
|
||||
active_env: 0,
|
||||
active_lfo: 0,
|
||||
fx_selected: 0,
|
||||
midi_tx,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,34 +107,45 @@ impl Model for AppData {
|
||||
self.params[*id as usize] = *val;
|
||||
self.store.set(*id, *val);
|
||||
}
|
||||
|
||||
AppEvent::SetPanel(p) => self.active_panel = *p,
|
||||
AppEvent::SelectEnv(i) => self.active_env = *i,
|
||||
AppEvent::SelectLfo(i) => self.active_lfo = *i,
|
||||
AppEvent::SelectFx(i) => self.fx_selected = *i,
|
||||
|
||||
AppEvent::UpdateMetrics { voices, cpu } => {
|
||||
self.voice_count = *voices;
|
||||
self.cpu_load = *cpu;
|
||||
}
|
||||
AppEvent::NoteOn(note, _) => {
|
||||
|
||||
AppEvent::NoteOn(note, vel) => {
|
||||
if !self.held_notes.contains(note) {
|
||||
self.held_notes.push(*note);
|
||||
}
|
||||
let _ = self.midi_tx.try_send(MidiMsg::NoteOn(*note, *vel));
|
||||
}
|
||||
AppEvent::NoteOff(note) => self.held_notes.retain(|n| n != note),
|
||||
AppEvent::NoteOff(note) => {
|
||||
self.held_notes.retain(|n| n != note);
|
||||
let _ = self.midi_tx.try_send(MidiMsg::NoteOff(*note));
|
||||
}
|
||||
|
||||
AppEvent::OctaveUp => self.octave = (self.octave + 1).min(8),
|
||||
AppEvent::OctaveDown => self.octave = (self.octave - 1).max(0),
|
||||
|
||||
AppEvent::SetModDepth { src, dst, depth } => {
|
||||
let idx = src * MOD_NDST + dst;
|
||||
if idx < self.mod_depths.len() {
|
||||
self.mod_depths[idx] = depth.clamp(-1.0, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
AppEvent::ToggleFx(s) => {
|
||||
if *s < self.fx_enabled.len() {
|
||||
self.fx_enabled[*s] = !self.fx_enabled[*s];
|
||||
}
|
||||
}
|
||||
AppEvent::SelectEnv(i) => self.active_env = *i,
|
||||
AppEvent::SelectLfo(i) => self.active_lfo = *i,
|
||||
AppEvent::SelectFx(i) => self.fx_selected = *i,
|
||||
});
|
||||
|
||||
event.map(|we: &WindowEvent, _| {
|
||||
crate::widgets::piano::handle_kbd(cx, we, self.octave);
|
||||
});
|
||||
@@ -158,3 +180,14 @@ pub fn build_root(cx: &mut Context) {
|
||||
.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 active = active_lens.get(cx) == p;
|
||||
Label::new(cx, p.label())
|
||||
.class("top-tab")
|
||||
.checked(active)
|
||||
.on_press(move |cx| cx.emit(AppEvent::SetPanel(p)))
|
||||
.height(Stretch(1.0));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,15 +2,28 @@ mod app;
|
||||
mod panels;
|
||||
mod widgets;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use app::{AppData, MidiMsg};
|
||||
use crossbeam_channel::bounded;
|
||||
use engine::oscillator::WavetableBank;
|
||||
use engine::synth::Synth;
|
||||
use params::ParamStore;
|
||||
use vizia::prelude::*;
|
||||
|
||||
fn main() {
|
||||
use app::AppData;
|
||||
use params::ParamStore;
|
||||
use vizia::prelude::*;
|
||||
let store = ParamStore::new();
|
||||
let bank = Arc::new(WavetableBank::new());
|
||||
|
||||
let (midi_tx, midi_rx) = bounded::<MidiMsg>(1024);
|
||||
|
||||
let audio_store = store.clone();
|
||||
let _stream = build_audio_stream(audio_store, bank, midi_rx);
|
||||
|
||||
let _ = Application::new(|cx| {
|
||||
cx.add_stylesheet(include_str!("theme.css"))
|
||||
.expect("theme.css");
|
||||
AppData::new(ParamStore::new()).build(cx);
|
||||
AppData::new(store, midi_tx).build(cx);
|
||||
app::build_root(cx);
|
||||
cx.focus();
|
||||
})
|
||||
@@ -19,3 +32,92 @@ fn main() {
|
||||
.resizable(false)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn build_audio_stream(
|
||||
store: ParamStore,
|
||||
bank: Arc<WavetableBank>,
|
||||
midi_rx: crossbeam_channel::Receiver<MidiMsg>,
|
||||
) -> cpal::Stream {
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
|
||||
let host = cpal::default_host();
|
||||
let device = host
|
||||
.default_output_device()
|
||||
.expect("no audio output device available");
|
||||
|
||||
let config = device
|
||||
.default_output_config()
|
||||
.expect("no default output config");
|
||||
|
||||
let sr = config.sample_rate() as f32;
|
||||
|
||||
let synth = Synth::new(sr, store, bank);
|
||||
|
||||
let host_bpm = 120.0f32;
|
||||
|
||||
let stream = match config.sample_format() {
|
||||
cpal::SampleFormat::F32 => {
|
||||
build_stream_f32(&device, &config.into(), synth, midi_rx, host_bpm)
|
||||
}
|
||||
_fmt => {
|
||||
let cfg: cpal::StreamConfig = config.into();
|
||||
build_stream_f32(&device, &cfg, synth, midi_rx, host_bpm)
|
||||
}
|
||||
};
|
||||
|
||||
stream.play().expect("failed to start audio stream");
|
||||
stream
|
||||
}
|
||||
|
||||
fn build_stream_f32(
|
||||
device: &cpal::Device,
|
||||
config: &cpal::StreamConfig,
|
||||
mut synth: Synth,
|
||||
midi_rx: crossbeam_channel::Receiver<MidiMsg>,
|
||||
host_bpm: f32,
|
||||
) -> cpal::Stream {
|
||||
use cpal::traits::DeviceTrait;
|
||||
|
||||
let channels = config.channels as usize;
|
||||
|
||||
device
|
||||
.build_output_stream(
|
||||
config,
|
||||
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
|
||||
while let Ok(msg) = midi_rx.try_recv() {
|
||||
match msg {
|
||||
MidiMsg::NoteOn(note, vel) => synth.note_on(note, vel),
|
||||
MidiMsg::NoteOff(note) => synth.note_off(note),
|
||||
MidiMsg::AllNotesOff => {
|
||||
for n in 0..128 {
|
||||
synth.note_off(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let frame_count = data.len() / channels;
|
||||
for frame in 0..frame_count {
|
||||
let (l, r) = synth.process(host_bpm);
|
||||
let base = frame * channels;
|
||||
match channels {
|
||||
1 => data[base] = (l + r) * 0.5,
|
||||
2 => {
|
||||
data[base] = l;
|
||||
data[base + 1] = r;
|
||||
}
|
||||
n => {
|
||||
data[base] = l;
|
||||
data[base + 1] = r;
|
||||
for ch in 2..n {
|
||||
data[base + ch] = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|err| eprintln!("audio stream error: {err}"),
|
||||
None,
|
||||
)
|
||||
.expect("failed to build output stream")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user