diff --git a/crates/tek/Cargo.toml b/crates/tek/Cargo.toml index 6345dc77..9189e4ef 100644 --- a/crates/tek/Cargo.toml +++ b/crates/tek/Cargo.toml @@ -4,6 +4,10 @@ edition = "2021" version = "0.1.0" [dependencies] +clap = { version = "4.5.4", features = [ "derive" ] } +clojure-reader = "0.1.0" +microxdg = "0.1.2" + tek_core = { path = "../tek_core" } tek_jack = { path = "../tek_jack" } tek_plugin = { path = "../tek_plugin" } @@ -13,11 +17,9 @@ tek_timer = { path = "../tek_timer" } tek_chain = { path = "../tek_chain" } tek_mixer = { path = "../tek_mixer" } #jack = "0.10" -#clap = { version = "4.5.4", features = [ "derive" ] } #crossterm = "0.27" #ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] } #backtrace = "0.3.72" -#microxdg = "0.1.2" #toml = "0.8.12" #better-panic = "0.3.0" #midly = "0.5" @@ -31,7 +33,6 @@ tek_mixer = { path = "../tek_mixer" } #fraction = "0.15.3" #rlsf = "0.2.1" #r8brain-rs = "0.3.5" -#clojure-reader = "0.1.0" #once_cell = "1.19.0" #symphonia = { version = "0.5.4", features = [ "all" ] } diff --git a/crates/tek/src/app.rs b/crates/tek/src/app.rs index 724923ff..d3c4a541 100644 --- a/crates/tek/src/app.rs +++ b/crates/tek/src/app.rs @@ -1,4 +1,6 @@ use crate::*; +use tek_timer::TransportToolbar; +use tek_sequencer::Arranger; /// Root of application state. pub struct App { @@ -29,7 +31,7 @@ pub struct App { impl App { pub fn new () -> Usually { - let xdg = Arc::new(microxdg::XdgApp::new("tek")?); + let xdg = Arc::new(XdgApp::new("tek")?); let first_run = crate::config::AppPaths::new(&xdg)?.should_create(); let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0); *MODAL.lock().unwrap() = first_run.then(||{ @@ -49,31 +51,6 @@ impl App { _xdg: Some(xdg), }) } -} - -process!(App |self, _client, scope| { - let ( - reset, current_frames, chunk_size, current_usecs, next_usecs, period_usecs - ) = self.transport.update(&scope); - self.chunk_size = chunk_size; - for track in self.arranger.tracks.iter_mut() { - track.process( - self.midi_in.as_ref().map(|p|p.iter(&scope)), - &self.transport.timebase, - self.transport.playing, - self.transport.started, - self.transport.quant as usize, - reset, - &scope, - (current_frames as usize, self.chunk_size), - (current_usecs as usize, next_usecs.saturating_sub(current_usecs) as usize), - period_usecs as f64 - ); - } - Control::Continue -}); - -impl App { pub fn client (&self) -> &Client { self.jack.as_ref().unwrap().client() } @@ -113,7 +90,6 @@ impl App { } } - render!(App |self, buf, area| { Split::down([ &self.transport, @@ -128,3 +104,25 @@ render!(App |self, buf, area| { } Ok(area) }); + +process!(App |self, _client, scope| { + let ( + reset, current_frames, chunk_size, current_usecs, next_usecs, period_usecs + ) = self.transport.update(&scope); + self.chunk_size = chunk_size; + for track in self.arranger.tracks.iter_mut() { + track.process( + self.midi_in.as_ref().map(|p|p.iter(&scope)), + &self.transport.timebase, + self.transport.playing, + self.transport.started, + self.transport.quant as usize, + reset, + &scope, + (current_frames as usize, self.chunk_size), + (current_usecs as usize, next_usecs.saturating_sub(current_usecs) as usize), + period_usecs as f64 + ); + } + Control::Continue +}); diff --git a/crates/tek/src/app_paths.rs b/crates/tek/src/app_paths.rs index 91f8b766..55e65482 100644 --- a/crates/tek/src/app_paths.rs +++ b/crates/tek/src/app_paths.rs @@ -1,6 +1,6 @@ //! Global settings. -use crate::core::*; +use crate::*; use std::path::{Path, PathBuf}; use std::fs::{File, create_dir_all}; diff --git a/crates/tek/src/control.rs b/crates/tek/src/control.rs index cd6e1cc9..a4f95549 100644 --- a/crates/tek/src/control.rs +++ b/crates/tek/src/control.rs @@ -1,6 +1,6 @@ //! Handling of input events. -use crate::{core::*, handle, App, AppFocus, model::MODAL}; +use crate::*; handle!{ App |self, e| { @@ -38,12 +38,9 @@ fn handle_modal (e: &AppEvent) -> Usually { fn handle_focused (state: &mut App, e: &AppEvent) -> Usually { match state.section { - AppFocus::Transport => - handle_keymap(state, e, crate::devices::transport::KEYMAP_TRANSPORT), - AppFocus::Arranger => - handle_keymap(state, e, crate::devices::arranger::KEYMAP_ARRANGER), - AppFocus::Sequencer => - handle_keymap(state, e, crate::devices::sequencer::KEYMAP_SEQUENCER), + AppFocus::Transport => state.transport.handle(e), + AppFocus::Arranger => state.arranger.sequencer.handle(e), + AppFocus::Sequencer => state.arranger.sequencer.handle(e), AppFocus::Chain => Ok(if state.entered { handle_device(state, e)? || handle_keymap(state, e, crate::control::KEYMAP_CHAIN)? @@ -88,11 +85,11 @@ pub const KEYMAP_GLOBAL: &'static [KeyBinding] = keymap!(App { Ok(true) }], [Char('='), NONE, "zoom_in", "show fewer ticks per block", |app: &mut App| { - app.sequencer.time_axis.scale_mut(&prev_note_length); + app.arranger.sequencer.time_axis.scale_mut(&prev_note_length); Ok(true) }], [Char('-'), NONE, "zoom_out", "show more ticks per block", |app: &mut App| { - app.sequencer.time_axis.scale_mut(&next_note_length); + app.arranger.sequencer.time_axis.scale_mut(&next_note_length); Ok(true) }], [Char('x'), NONE, "extend", "double the current clip", |app: &mut App| { @@ -102,7 +99,7 @@ pub const KEYMAP_GLOBAL: &'static [KeyBinding] = keymap!(App { phrase.notes = notes; phrase.length = phrase.length * 2; }); - app.sequencer.show(app.arranger.phrase())?; + app.arranger.sequencer.show(app.arranger.phrase())?; Ok(true) }], [Char('l'), NONE, "loop_toggle", "toggle looping", |_app: &mut App| { @@ -147,14 +144,14 @@ pub const KEYMAP_FOCUS: &'static [KeyBinding] = keymap!(App { app.entered = false; app.transport.entered = app.entered; app.arranger.entered = app.entered; - app.sequencer.entered = app.entered; + app.arranger.sequencer.entered = app.entered; Ok(true) }], [Enter, NONE, "focus_enter", "activate item at cursor", |app: &mut App|{ app.entered = true; app.transport.entered = app.entered; app.arranger.entered = app.entered; - app.sequencer.entered = app.entered; + app.arranger.sequencer.entered = app.entered; Ok(true) }], }); @@ -165,8 +162,8 @@ pub fn focus_next (app: &mut App) -> Usually { app.transport.entered = app.entered; app.arranger.focused = app.section == AppFocus::Arranger; app.arranger.entered = app.entered; - app.sequencer.focused = app.section == AppFocus::Sequencer; - app.sequencer.entered = app.entered; + app.arranger.sequencer.focused = app.section == AppFocus::Sequencer; + app.arranger.sequencer.entered = app.entered; Ok(true) } @@ -176,7 +173,7 @@ pub fn focus_prev (app: &mut App) -> Usually { app.transport.entered = app.entered; app.arranger.focused = app.section == AppFocus::Arranger; app.arranger.entered = app.entered; - app.sequencer.focused = app.section == AppFocus::Sequencer; - app.sequencer.entered = app.entered; + app.arranger.sequencer.focused = app.section == AppFocus::Sequencer; + app.arranger.sequencer.entered = app.entered; Ok(true) } diff --git a/crates/tek/src/edn.rs b/crates/tek/src/edn.rs index 887159ad..60ccb8ab 100644 --- a/crates/tek/src/edn.rs +++ b/crates/tek/src/edn.rs @@ -13,13 +13,7 @@ //! * [Sample::load_edn] //! * [LV2Plugin::load_edn] -use crate::{core::*, model::*, App}; -use crate::devices::{ - arranger::Scene, - sequencer::Phrase, - sampler::{Sampler, Sample, read_sample_data} -}; -use crate::devices::plugin::{Plugin, LV2Plugin}; +use crate::*; use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; /// EDN parsing helper. @@ -239,81 +233,3 @@ impl Phrase { Ok(phrase) } } - -impl Sampler { - fn load_edn <'e> (args: &[Edn<'e>]) -> Usually { - let mut name = String::new(); - let mut dir = String::new(); - let mut samples = BTreeMap::new(); - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { - dir = String::from(*n); - } - }, - Edn::List(args) => match args.get(0) { - Some(Edn::Symbol("sample")) => { - let (midi, sample) = Sample::load_edn(&dir, &args[1..])?; - if let Some(midi) = midi { - samples.insert(midi, sample); - } else { - panic!("sample without midi binding: {}", sample.read().unwrap().name); - } - }, - _ => panic!("unexpected in sampler {name}: {args:?}") - }, - _ => panic!("unexpected in sampler {name}: {edn:?}") - }); - Self::new(&name, Some(samples)) - } -} - -impl Sample { - fn load_edn <'e> (dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { - let mut name = String::new(); - let mut file = String::new(); - let mut midi = None; - let mut start = 0usize; - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { - file = String::from(*f); - } - if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { - start = *i as usize; - } - if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { - midi = Some(u7::from(*m as u8)); - } - }, - _ => panic!("unexpected in sample {name}"), - }); - let (end, data) = read_sample_data(&format!("{dir}/{file}"))?; - Ok((midi, Arc::new(RwLock::new(Self::new(&name, start, end, data))))) - } -} - -impl LV2Plugin { - fn load_edn <'e> (args: &[Edn<'e>]) -> Usually { - let mut name = String::new(); - let mut path = String::new(); - edn!(edn in args { - Edn::Map(map) => { - if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { - name = String::from(*n); - } - if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) { - path = String::from(*p); - } - }, - _ => panic!("unexpected in lv2 '{name}'"), - }); - Plugin::lv2(&name, &path) - } -} diff --git a/crates/tek/src/help.rs b/crates/tek/src/help.rs index f2795fec..61d9b141 100644 --- a/crates/tek/src/help.rs +++ b/crates/tek/src/help.rs @@ -1,6 +1,6 @@ //! Help modal / command palette. -use crate::{core::*, view::*}; +use crate::*; /// Command palette. pub struct HelpModal { diff --git a/crates/tek/src/main.rs b/crates/tek/src/main.rs index 2d393f3b..f15e0d8a 100644 --- a/crates/tek/src/main.rs +++ b/crates/tek/src/main.rs @@ -7,11 +7,22 @@ #![allow(macro_expanded_macro_exports_accessed_by_absolute_paths)] #![allow(ambiguous_glob_reexports)] -extern crate clap; -extern crate jack as _jack; -extern crate crossterm; - pub(crate) use tek_core::*; +pub(crate) use tek_jack::{*, jack::*}; +pub(crate) use tek_timer::*; +pub(crate) use microxdg::XdgApp; + +submod! { + app + app_focus + app_paths + cli + control + edn + help + modal + setup +} /// Application entrypoint. pub fn main () -> Usually<()> { diff --git a/crates/tek/src/setup.rs b/crates/tek/src/setup.rs index 4e4ac8c4..669a3812 100644 --- a/crates/tek/src/setup.rs +++ b/crates/tek/src/setup.rs @@ -1,6 +1,6 @@ //! Inital setup dialog (TODO: make this the options dialog too?) -use crate::{core::*, config::AppPaths}; +use crate::*; /// Appears on first run (i.e. if state dir is missing). pub struct SetupModal(pub Option>, pub bool); diff --git a/crates/tek_chain/src/chain_edn.rs b/crates/tek_chain/src/chain_edn.rs new file mode 100644 index 00000000..72651114 --- /dev/null +++ b/crates/tek_chain/src/chain_edn.rs @@ -0,0 +1,45 @@ +use crate::*; + +impl Track { + fn load_edn <'a, 'e> (app: &'a mut App, args: &[Edn<'e>]) -> Usually<&'a mut Self> { + let ppq = app.transport.ppq(); + let mut name = None; + let mut _gain = 0.0f64; + let mut devices: Vec = vec![]; + let mut phrases: Vec = vec![]; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = Some(*n); + } + if let Some(Edn::Double(g)) = map.get(&Edn::Key(":gain")) { + _gain = f64::from(*g) + } + }, + Edn::List(args) => match args.get(0) { + Some(Edn::Symbol("phrase")) => { + phrases.push(Phrase::load_edn(ppq, &args[1..])?) + }, + Some(Edn::Symbol("sampler")) => { + devices.push(Sampler::load_edn(&args[1..])?) + }, + Some(Edn::Symbol("lv2")) => { + devices.push(LV2Plugin::load_edn(&args[1..])?) + }, + None => panic!("empty list track {}", + name.unwrap_or("") + ), + _ => panic!("unexpected in track {}: {:?}", + name.unwrap_or(""), + args.get(0).unwrap() + ) + }, + _ => {} + }); + let track = app.arranger.track_add(name)?; + for phrase in phrases { track.phrases.push(Arc::new(RwLock::new(phrase))); } + for device in devices { track.add_device(device)?; } + Ok(track) + } +} + diff --git a/crates/tek_core/Cargo.toml b/crates/tek_core/Cargo.toml index 2c64e610..1cc66919 100644 --- a/crates/tek_core/Cargo.toml +++ b/crates/tek_core/Cargo.toml @@ -4,11 +4,9 @@ edition = "2021" version = "0.1.0" [dependencies] -clap = { version = "4.5.4", features = [ "derive" ] } crossterm = "0.27" ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] } backtrace = "0.3.72" -microxdg = "0.1.2" toml = "0.8.12" better-panic = "0.3.0" midly = "0.5" diff --git a/crates/tek_core/src/lib.rs b/crates/tek_core/src/lib.rs index 4b5097dd..163a6381 100644 --- a/crates/tek_core/src/lib.rs +++ b/crates/tek_core/src/lib.rs @@ -2,7 +2,10 @@ pub use ratatui; pub use crossterm; pub use midly; pub use std::sync::{Arc, Mutex, RwLock}; +pub use std::collections::BTreeMap; pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers}; +pub use ratatui::prelude::{Rect, Style, Color, Buffer}; +pub use ratatui::style::Stylize; pub(crate) use std::error::Error; pub(crate) use std::io::{stdout}; diff --git a/crates/tek_plugin/src/lv2_edn.rs b/crates/tek_plugin/src/lv2_edn.rs new file mode 100644 index 00000000..99eff76c --- /dev/null +++ b/crates/tek_plugin/src/lv2_edn.rs @@ -0,0 +1,20 @@ +use crate::*; +impl LV2Plugin { + fn load_edn <'e> (args: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut path = String::new(); + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) { + path = String::from(*p); + } + }, + _ => panic!("unexpected in lv2 '{name}'"), + }); + Plugin::lv2(&name, &path) + } +} + diff --git a/crates/tek_sampler/src/sampler_edn.rs b/crates/tek_sampler/src/sampler_edn.rs new file mode 100644 index 00000000..d6501292 --- /dev/null +++ b/crates/tek_sampler/src/sampler_edn.rs @@ -0,0 +1,60 @@ +use crate::*; + +impl Sampler { + fn load_edn <'e> (args: &[Edn<'e>]) -> Usually { + let mut name = String::new(); + let mut dir = String::new(); + let mut samples = BTreeMap::new(); + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) { + dir = String::from(*n); + } + }, + Edn::List(args) => match args.get(0) { + Some(Edn::Symbol("sample")) => { + let (midi, sample) = Sample::load_edn(&dir, &args[1..])?; + if let Some(midi) = midi { + samples.insert(midi, sample); + } else { + panic!("sample without midi binding: {}", sample.read().unwrap().name); + } + }, + _ => panic!("unexpected in sampler {name}: {args:?}") + }, + _ => panic!("unexpected in sampler {name}: {edn:?}") + }); + Self::new(&name, Some(samples)) + } +} + +impl Sample { + fn load_edn <'e> (dir: &str, args: &[Edn<'e>]) -> Usually<(Option, Arc>)> { + let mut name = String::new(); + let mut file = String::new(); + let mut midi = None; + let mut start = 0usize; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) { + file = String::from(*f); + } + if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) { + start = *i as usize; + } + if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) { + midi = Some(u7::from(*m as u8)); + } + }, + _ => panic!("unexpected in sample {name}"), + }); + let (end, data) = read_sample_data(&format!("{dir}/{file}"))?; + Ok((midi, Arc::new(RwLock::new(Self::new(&name, start, end, data))))) + } +} diff --git a/crates/tek_sequencer/src/phrase_edn.rs b/crates/tek_sequencer/src/phrase_edn.rs new file mode 100644 index 00000000..1e798e92 --- /dev/null +++ b/crates/tek_sequencer/src/phrase_edn.rs @@ -0,0 +1,58 @@ +use crate::*; + +impl Phrase { + fn load_edn <'e> (ppq: usize, args: &[Edn<'e>]) -> Usually { + let mut phrase = Self::default(); + let mut name = String::new(); + let mut beats = 0usize; + let mut steps = 0usize; + edn!(edn in args { + Edn::Map(map) => { + if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) { + name = String::from(*n); + } + if let Some(Edn::Int(b)) = map.get(&Edn::Key(":beats")) { + beats = *b as usize; + phrase.length = ppq * beats; + for _ in phrase.notes.len()..phrase.length { + phrase.notes.push(Vec::with_capacity(16)) + } + } + if let Some(Edn::Int(s)) = map.get(&Edn::Key(":steps")) { + steps = *s as usize; + } + }, + Edn::List(args) => { + let time = (match args.get(0) { + Some(Edn::Key(text)) => text[1..].parse::()?, + Some(Edn::Int(i)) => *i as f64, + Some(Edn::Double(f)) => f64::from(*f), + _ => panic!("unexpected in phrase '{name}': {:?}", args.get(0)), + } * beats as f64 * ppq as f64 / steps as f64) as usize; + for edn in args[1..].iter() { + match edn { + Edn::List(args) => if let ( + Some(Edn::Int(key)), + Some(Edn::Int(vel)), + ) = ( + args.get(0), + args.get(1), + ) { + let (key, vel) = ( + u7::from((*key as u8).min(127)), + u7::from((*vel as u8).min(127)), + ); + phrase.notes[time].push(MidiMessage::NoteOn { key, vel }) + } else { + panic!("unexpected list in phrase '{name}'") + }, + _ => panic!("unexpected in phrase '{name}': {edn:?}") + } + } + }, + _ => panic!("unexpected in phrase '{name}': {edn:?}"), + }); + phrase.name = name; + Ok(phrase) + } +} diff --git a/crates/tek_sequencer/src/scene_edn.rs b/crates/tek_sequencer/src/scene_edn.rs new file mode 100644 index 00000000..e3937b4f --- /dev/null +++ b/crates/tek_sequencer/src/scene_edn.rs @@ -0,0 +1,28 @@ +use crate::*; + +impl Scene { + fn load_edn <'a, 'e> (app: &'a mut App, args: &[Edn<'e>]) -> Usually<&'a mut Self> { + let mut name = None; + let mut clips = vec![]; + edn!(edn in args { + Edn::Map(map) => { + let key = map.get(&Edn::Key(":name")); + if let Some(Edn::Str(n)) = key { + name = Some(*n); + } else { + panic!("unexpected key in scene '{name:?}': {key:?}") + } + }, + Edn::Symbol("_") => { + clips.push(None); + }, + Edn::Int(i) => { + clips.push(Some(*i as usize)); + }, + _ => panic!("unexpected in scene '{name:?}': {edn:?}") + }); + let scene = app.arranger.scene_add(name)?; + scene.clips = clips; + Ok(scene) + } +}