From 8c3cf53c670dda07462603d534a4dcd7b7e5f7d1 Mon Sep 17 00:00:00 2001 From: unspeaker Date: Wed, 3 Jul 2024 14:51:48 +0300 Subject: [PATCH] big ass refactor (rip client) --- src/config.rs | 102 +++- src/control.rs | 9 + src/control/chain.rs | 72 +++ src/{layout => control}/focus.rs | 4 +- .../handle.rs => control/launcher.rs} | 119 ++-- src/control/mixer.rs | 59 ++ src/control/plugin.rs | 69 +++ src/control/sampler.rs | 45 ++ .../handle.rs => control/sequencer.rs} | 50 +- src/{core/mod.rs => core.rs} | 13 +- src/core/device.rs | 6 +- src/core/handle.rs | 20 + src/core/jack.rs | 63 +- .../sequencer/keys.rs => core/midi.rs} | 10 + src/core/render.rs | 42 +- src/core/run.rs | 114 ++-- src/core/time.rs | 21 +- src/device/chain/plugin.rs | 281 --------- src/device/chain/sampler.rs | 263 --------- src/device/launcher/mod.rs | 281 --------- src/device/mixer.rs | 249 -------- src/device/mod.rs | 7 - src/device/sequencer/mod.rs | 210 ------- src/main.rs | 543 ++++++++++-------- src/model.rs | 20 + src/model/chain.rs | 46 ++ src/model/launcher.rs | 184 ++++++ src/{device => model}/looper.rs | 0 src/model/mixer.rs | 132 +++++ src/{device/sequencer => model}/phrase.rs | 18 +- src/model/plugin.rs | 176 ++++++ src/{device/chain => model}/plugin/lv2.rs | 1 + src/{device/chain => model}/plugin/vst2.rs | 3 +- src/{device/chain => model}/plugin/vst3.rs | 0 src/model/sampler.rs | 175 ++++++ src/{device/launcher => model}/scene.rs | 0 src/model/sequencer.rs | 141 +++++ src/{device => model}/track.rs | 18 +- src/view.rs | 15 + src/{device/chain/mod.rs => view/chain.rs} | 145 +---- src/{device/launcher => view}/grid.rs | 11 +- src/view/launcher.rs | 50 ++ src/{ => view}/layout/collect.rs | 0 src/{ => view}/layout/container.rs | 2 +- src/view/layout/focus.rs | 0 src/{ => view}/layout/lozenge.rs | 0 src/{ => view}/layout/mod.rs | 0 src/{ => view}/layout/scroll.rs | 0 src/{ => view}/layout/table.rs | 0 src/view/mixer.rs | 68 +++ src/view/plugin.rs | 48 ++ src/view/sampler.rs | 78 +++ src/view/sequencer.rs | 104 ++++ src/{device => view}/sequencer/horizontal.rs | 24 +- src/{device => view}/sequencer/vertical.rs | 1 - src/{device => view}/transport.rs | 11 +- 56 files changed, 2232 insertions(+), 1891 deletions(-) create mode 100644 src/control.rs create mode 100644 src/control/chain.rs rename src/{layout => control}/focus.rs (99%) rename src/{device/launcher/handle.rs => control/launcher.rs} (72%) create mode 100644 src/control/mixer.rs create mode 100644 src/control/plugin.rs create mode 100644 src/control/sampler.rs rename src/{device/sequencer/handle.rs => control/sequencer.rs} (91%) rename src/{core/mod.rs => core.rs} (78%) rename src/{device/sequencer/keys.rs => core/midi.rs} (82%) delete mode 100644 src/device/chain/plugin.rs delete mode 100644 src/device/chain/sampler.rs delete mode 100644 src/device/launcher/mod.rs delete mode 100644 src/device/mixer.rs delete mode 100644 src/device/mod.rs delete mode 100644 src/device/sequencer/mod.rs create mode 100644 src/model.rs create mode 100644 src/model/chain.rs create mode 100644 src/model/launcher.rs rename src/{device => model}/looper.rs (100%) create mode 100644 src/model/mixer.rs rename src/{device/sequencer => model}/phrase.rs (97%) create mode 100644 src/model/plugin.rs rename src/{device/chain => model}/plugin/lv2.rs (99%) rename src/{device/chain => model}/plugin/vst2.rs (79%) rename src/{device/chain => model}/plugin/vst3.rs (100%) create mode 100644 src/model/sampler.rs rename src/{device/launcher => model}/scene.rs (100%) create mode 100644 src/model/sequencer.rs rename src/{device => model}/track.rs (75%) create mode 100644 src/view.rs rename src/{device/chain/mod.rs => view/chain.rs} (62%) rename src/{device/launcher => view}/grid.rs (95%) create mode 100644 src/view/launcher.rs rename src/{ => view}/layout/collect.rs (100%) rename src/{ => view}/layout/container.rs (97%) create mode 100644 src/view/layout/focus.rs rename src/{ => view}/layout/lozenge.rs (100%) rename src/{ => view}/layout/mod.rs (100%) rename src/{ => view}/layout/scroll.rs (100%) rename src/{ => view}/layout/table.rs (100%) create mode 100644 src/view/mixer.rs create mode 100644 src/view/plugin.rs create mode 100644 src/view/sampler.rs create mode 100644 src/view/sequencer.rs rename src/{device => view}/sequencer/horizontal.rs (93%) rename src/{device => view}/sequencer/vertical.rs (99%) rename src/{device => view}/transport.rs (97%) diff --git a/src/config.rs b/src/config.rs index 543965af..4a7b35ea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,29 +1,85 @@ use crate::core::*; +use std::path::{Path, PathBuf}; +use std::fs::{File, create_dir_all}; const CONFIG_FILE_NAME: &'static str = "tek.toml"; const PROJECT_FILE_NAME: &'static str = "project.toml"; -pub fn create_dirs (xdg: µxdg::XdgApp) -> Result<(), Box> { - use std::{path::Path,fs::{File,create_dir_all}}; - let config_dir = xdg.app_config()?; - if !Path::new(&config_dir).exists() { - println!("Creating {config_dir:?}"); - create_dir_all(&config_dir)?; - } - let config_path = config_dir.join(CONFIG_FILE_NAME); - if !Path::new(&config_path).exists() { - println!("Creating {config_path:?}"); - File::create_new(&config_path)?; - } - let data_dir = xdg.app_data()?; - if !Path::new(&data_dir).exists() { - println!("Creating {data_dir:?}"); - create_dir_all(&data_dir)?; - } - let project_path = data_dir.join(PROJECT_FILE_NAME); - if !Path::new(&project_path).exists() { - println!("Creating {project_path:?}"); - File::create_new(&project_path)?; - } - Ok(()) +pub struct AppPaths { + config_dir: PathBuf, + config_file: PathBuf, + data_dir: PathBuf, + project_file: PathBuf, } +impl AppPaths { + pub fn new (xdg: &XdgApp) -> Usually { + let config_dir = PathBuf::from(xdg.app_config()?); + let config_file = PathBuf::from(config_dir.join(CONFIG_FILE_NAME)); + let data_dir = PathBuf::from(xdg.app_data()?); + let project_file = PathBuf::from(data_dir.join(PROJECT_FILE_NAME)); + Ok(Self { config_dir, config_file, data_dir, project_file }) + } + pub fn should_create (&self) -> bool { + for path in [ + &self.config_dir, + &self.config_file, + &self.data_dir, + &self.project_file, + ].iter() { + if !Path::new(path).exists() { + return true + } + } + false + } + pub fn create (&self) -> Usually<()> { + for dir in [&self.config_dir, &self.data_dir].iter() { + if !Path::new(dir).exists() { + create_dir_all(&dir)?; + } + } + for file in [&self.config_file, &self.project_file].iter() { + if !Path::new(file).exists() { + File::create_new(&file)?; + } + } + Ok(()) + } +} + +pub struct SetupModal(pub Option>); + +render!(SetupModal |self, buf, area| { + let lines = [ + (" ", Style::default().white().on_black().not_dim().bold()), + (" Welcome to TEK. ", Style::default().white().on_black().not_dim().bold()), + (" ", Style::default().white().on_black().not_dim().bold()), + (" Press ENTER to create the ", Style::default().white().on_black().not_dim()), + (" following directories: ", Style::default().white().on_black().not_dim()), + (" ", Style::default().white().on_black().not_dim().bold()), + (" Configuration directory: ", Style::default().white().on_black().not_dim()), + (" ~/.config/tek ", Style::default().white().on_black().not_dim().bold()), + (" ", Style::default().white().on_black().not_dim()), + (" Data directory: ", Style::default().white().on_black().not_dim()), + (" ~/.local/share/tek ", Style::default().white().on_black().not_dim().bold()), + (" ", Style::default().white().on_black().not_dim()), + ]; + let width = lines[0].0.len() as u16; + let x = area.x + (area.width - width) / 2; + for (i, (line, style)) in lines.iter().enumerate() { + line.blit(buf, x, area.y + area.height / 2 - (lines.len() / 2) as u16 + i as u16, Some(*style)); + } + Ok(area) +}); +handle!(SetupModal |self, e| { + if let AppEvent::Input(::crossterm::event::Event::Key(KeyEvent { + code: KeyCode::Enter, + .. + })) = e { + AppPaths::new(&self.0.as_ref().unwrap())?.create()?; + self.0 = None; + Ok(true) + } else { + Ok(false) + } +}); diff --git a/src/control.rs b/src/control.rs new file mode 100644 index 00000000..034ea823 --- /dev/null +++ b/src/control.rs @@ -0,0 +1,9 @@ +pub mod chain; +pub mod focus; +pub mod launcher; +pub mod mixer; +pub mod plugin; +pub mod sampler; +pub mod sequencer; + +pub use self::focus::*; diff --git a/src/control/chain.rs b/src/control/chain.rs new file mode 100644 index 00000000..4087fa61 --- /dev/null +++ b/src/control/chain.rs @@ -0,0 +1,72 @@ +use crate::{core::*, model::*}; +use super::focus::*; + +pub fn handle (state: &mut Chain, event: &AppEvent) -> Usually { + Ok(handle_focus(state, event, keymap!(Chain { + [Up, NONE, "focus_up", "focus row above", + |s: &mut Chain|s.handle_focus(&FocusEvent::Backward)], + [Down, NONE, "focus_down", "focus row below", + |s: &mut Chain|s.handle_focus(&FocusEvent::Forward)], + [Enter, NONE, "focus_down", "focus row below", + |s: &mut Chain|s.handle_focus(&FocusEvent::Inward)], + [Esc, NONE, "focus_down", "focus row below", + |s: &mut Chain|s.handle_focus(&FocusEvent::Outward)], + }))? || handle_keymap(state, event, keymap!(Chain { + [Char('a'), NONE, "add_device", "add a device", add_device] + }))?) +} + +fn add_device (state: &mut Chain) -> Usually { + state.adding = true; + Ok(true) +} + +impl Focus for Chain { + fn unfocus (&mut self) { + self.focused = false + } + fn focused (&self) -> Option<&Box> { + match self.focused { + true => self.items.get(self.focus), + false => None + } + } + fn focused_mut (&mut self) -> Option<&mut Box> { + match self.focused { + true => self.items.get_mut(self.focus), + false => None + } + } + fn handle_focus (&mut self, event: &FocusEvent) -> Usually { + Ok(match event { + FocusEvent::Backward => { + if self.focus == 0 { + self.focus = self.items.len(); + } + self.focus = self.focus - 1; + true + }, + FocusEvent::Forward => { + self.focus = self.focus + 1; + if self.focus >= self.items.len() { + self.focus = 0; + } + true + }, + FocusEvent::Inward => { + self.focused = true; + self.items[self.focus].handle(&AppEvent::Focus)?; + true + }, + FocusEvent::Outward => { + if self.focused { + self.focused = false; + self.items[self.focus].handle(&AppEvent::Blur)?; + true + } else { + false + } + }, + }) + } +} diff --git a/src/layout/focus.rs b/src/control/focus.rs similarity index 99% rename from src/layout/focus.rs rename to src/control/focus.rs index beafeff9..82a7d935 100644 --- a/src/layout/focus.rs +++ b/src/control/focus.rs @@ -1,5 +1,4 @@ -use crate::core::*; -use super::*; +use crate::{core::*, view::*}; pub trait Focus { fn unfocus (&mut self); @@ -187,3 +186,4 @@ impl Focus for FocusRow { }) } } + diff --git a/src/device/launcher/handle.rs b/src/control/launcher.rs similarity index 72% rename from src/device/launcher/handle.rs rename to src/control/launcher.rs index a35e82c4..f35a6e5e 100644 --- a/src/device/launcher/handle.rs +++ b/src/control/launcher.rs @@ -1,25 +1,22 @@ -use crate::core::*; -use super::*; +use crate::{core::*,model::*}; + pub fn handle (state: &mut Launcher, event: &AppEvent) -> Usually { - //if let Some(ref modal) = state.modal { - //if modal.handle_with_state(state, event)? { - //return Ok(true) - //} - //} Ok(handle_keymap(state, event, KEYMAP)? || match state.view { - LauncherView::Tracks => handle_keymap(state, event, KEYMAP_TRACKS)?, - LauncherView::Sequencer => { + LauncherMode::Tracks => { + handle_keymap(state, event, KEYMAP_TRACKS)? + }, + LauncherMode::Sequencer => { let i = state.col().saturating_sub(1); if let Some(track) = state.tracks.get_mut(i) { - crate::device::sequencer::handle(&mut *track.sequencer.state(), event)? + crate::control::sequencer::handle(&mut track.sequencer, event)? } else { true } }, - LauncherView::Chains => { + LauncherMode::Chains => { let i = state.col().saturating_sub(1); if let Some(track) = state.tracks.get_mut(i) { - crate::device::chain::handle(&mut *track.chain.state(), event)? + crate::control::chain::handle(&mut track.chain, event)? } else { true } @@ -50,35 +47,38 @@ pub const KEYMAP_TRACKS: &'static [KeyBinding] = keymap!(Launcher { [Enter, NONE, "activate", "activate item at cursor", activate], }); fn duplicate (_: &mut Launcher) -> Usually { + unimplemented!(); Ok(true) } -fn activate (state: &mut Launcher) -> Usually { - if let ( - Some((_scene_id, scene)), - Some((track_id, track)), - ) = (state.scene(), state.track()) { - // Launch clip - if let Some(phrase_id) = scene.clips.get(track_id) { - track.sequencer.state().sequence = *phrase_id; - } - if state.playing == TransportState::Stopped { - state.transport.start()?; - state.playing = TransportState::Starting; - } - } else if let Some((_scene_id, scene)) = state.scene() { - // Launch scene - for (track_id, track) in state.tracks.iter().enumerate() { - if let Some(phrase_id) = scene.clips.get(track_id) { - track.sequencer.state().sequence = *phrase_id; - } - } - if state.playing == TransportState::Stopped { - state.transport.start()?; - state.playing = TransportState::Starting; - } - } else if let Some((_track_id, _track)) = state.track() { - // Rename track? - } + +fn activate (_: &mut Launcher) -> Usually { + unimplemented!(); + //if let ( + //Some((_scene_id, scene)), + //Some((track_id, track)), + //) = (state.scene_mut(), state.track_mut()) { + //// Launch clip + //if let Some(phrase_id) = scene.clips.get(track_id) { + //track.sequencer.sequence = *phrase_id; + //} + //if state.playing == TransportState::Stopped { + //state.transport.start()?; + //state.playing = TransportState::Starting; + //} + //} else if let Some((_scene_id, scene)) = state.scene() { + //// Launch scene + //for (track_id, track) in state.tracks.iter().enumerate() { + //if let Some(phrase_id) = scene.clips.get(track_id) { + //track.sequencer.sequence = *phrase_id; + //} + //} + //if state.playing == TransportState::Stopped { + //state.transport.start()?; + //state.playing = TransportState::Starting; + //} + //} else if let Some((_track_id, _track)) = state.track() { + //// Rename track? + //} //let track = state.active_track().unwrap(); //let scene = state.active_scene(); @@ -104,15 +104,18 @@ fn activate (state: &mut Launcher) -> Usually { //} Ok(true) } + fn rename (_: &mut Launcher) -> Usually { Ok(true) } + fn add_track (state: &mut Launcher) -> Usually { let name = format!("Track {}", state.tracks.len() + 1); state.tracks.push(Track::new(&name, &state.timebase, None, None)?); state.cursor.0 = state.tracks.len(); Ok(true) } + fn delete_track (state: &mut Launcher) -> Usually { if state.tracks.len() > 0 && state.cursor.0 >= 1 { state.tracks.remove(state.cursor.0 - 1); @@ -120,44 +123,50 @@ fn delete_track (state: &mut Launcher) -> Usually { } Ok(true) } + fn cursor_up (state: &mut Launcher) -> Usually { state.dec_row(); Ok(true) } + fn cursor_down (state: &mut Launcher) -> Usually { state.inc_row(); Ok(true) } + fn cursor_left (state: &mut Launcher) -> Usually { state.dec_col(); Ok(true) } + fn cursor_right (state: &mut Launcher) -> Usually { state.inc_col(); Ok(true) } + fn toggle_help (state: &mut Launcher) -> Usually { state.show_help = !state.show_help; Ok(true) } + fn focus_next (state: &mut Launcher) -> Usually { match state.view { - LauncherView::Tracks => { state.view = LauncherView::Sequencer; }, - LauncherView::Sequencer => { state.view = LauncherView::Chains; }, - LauncherView::Chains => { state.view = LauncherView::Tracks; }, - _ => {}, + LauncherMode::Tracks => { state.view = LauncherMode::Sequencer; }, + LauncherMode::Sequencer => { state.view = LauncherMode::Chains; }, + LauncherMode::Chains => { state.view = LauncherMode::Tracks; }, }; Ok(true) } + fn focus_prev (state: &mut Launcher) -> Usually { match state.view { - LauncherView::Tracks => { state.view = LauncherView::Chains; }, - LauncherView::Chains => { state.view = LauncherView::Sequencer; }, - LauncherView::Sequencer => { state.view = LauncherView::Tracks; }, - _ => {}, + LauncherMode::Tracks => { state.view = LauncherMode::Chains; }, + LauncherMode::Chains => { state.view = LauncherMode::Sequencer; }, + LauncherMode::Sequencer => { state.view = LauncherMode::Tracks; }, }; Ok(true) } + fn clip_next (state: &mut Launcher) -> Usually { if state.cursor.0 >= 1 && state.cursor.1 >= 1 { let scene_id = state.cursor.1 - 1; @@ -165,7 +174,7 @@ fn clip_next (state: &mut Launcher) -> Usually { let scene = &mut state.scenes[scene_id]; scene.clips[clip_id] = match scene.clips[clip_id] { None => Some(0), - Some(i) => if i >= state.tracks[clip_id].sequencer.state().phrases.len().saturating_sub(1) { + Some(i) => if i >= state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1) { None } else { Some(i + 1) @@ -174,13 +183,14 @@ fn clip_next (state: &mut Launcher) -> Usually { } Ok(true) } + fn clip_prev (state: &mut Launcher) -> Usually { if state.cursor.0 >= 1 && state.cursor.1 >= 1 { let scene_id = state.cursor.1 - 1; let clip_id = state.cursor.0 - 1; let scene = &mut state.scenes[scene_id]; scene.clips[clip_id] = match scene.clips[clip_id] { - None => Some(state.tracks[clip_id].sequencer.state().phrases.len().saturating_sub(1)), + None => Some(state.tracks[clip_id].sequencer.phrases.len().saturating_sub(1)), Some(i) => if i == 0 { None } else { @@ -190,6 +200,7 @@ fn clip_prev (state: &mut Launcher) -> Usually { } Ok(true) } + fn play_toggle (s: &mut Launcher) -> Usually { s.playing = match s.playing { TransportState::Stopped => { @@ -207,15 +218,19 @@ fn play_toggle (s: &mut Launcher) -> Usually { //fn play_start (_: &mut Launcher) -> Usually { //unimplemented!() //} + fn record_toggle (s: &mut Launcher) -> Usually { - s.sequencer().map(|mut s|s.recording = !s.recording); + s.sequencer_mut().map(|s|s.recording = !s.recording); Ok(true) } + fn overdub_toggle (s: &mut Launcher) -> Usually { - s.sequencer().map(|mut s|s.overdub = !s.overdub); + s.sequencer_mut().map(|s|s.overdub = !s.overdub); Ok(true) } + fn monitor_toggle (s: &mut Launcher) -> Usually { - s.sequencer().map(|mut s|s.monitoring = !s.monitoring); + s.sequencer_mut().map(|s|s.monitoring = !s.monitoring); Ok(true) } + diff --git a/src/control/mixer.rs b/src/control/mixer.rs new file mode 100644 index 00000000..5029e4a6 --- /dev/null +++ b/src/control/mixer.rs @@ -0,0 +1,59 @@ +use crate::{core::*, model::*}; + +pub fn handle (state: &mut Mixer, event: &AppEvent) -> Usually { + if let AppEvent::Input(crossterm::event::Event::Key(event)) = event { + + match event.code { + //KeyCode::Char('c') => { + //if event.modifiers == KeyModifiers::CONTROL { + //state.exit(); + //} + //}, + KeyCode::Down => { + state.selected_track = (state.selected_track + 1) % state.tracks.len(); + println!("{}", state.selected_track); + return Ok(true) + }, + KeyCode::Up => { + if state.selected_track == 0 { + state.selected_track = state.tracks.len() - 1; + } else { + state.selected_track = state.selected_track - 1; + } + println!("{}", state.selected_track); + return Ok(true) + }, + KeyCode::Left => { + if state.selected_column == 0 { + state.selected_column = 6 + } else { + state.selected_column = state.selected_column - 1; + } + return Ok(true) + }, + KeyCode::Right => { + if state.selected_column == 6 { + state.selected_column = 0 + } else { + state.selected_column = state.selected_column + 1; + } + return Ok(true) + }, + _ => { + println!("\n{event:?}"); + } + } + + } + Ok(false) +} + +// TODO: +// - Meters: propagate clipping: +// - If one stage clips, all stages after it are marked red +// - If one track clips, all tracks that feed from it are marked red? + +pub const ACTIONS: [(&'static str, &'static str);2] = [ + ("+/-", "Adjust"), + ("Ins/Del", "Add/remove track"), +]; diff --git a/src/control/plugin.rs b/src/control/plugin.rs new file mode 100644 index 00000000..21286a19 --- /dev/null +++ b/src/control/plugin.rs @@ -0,0 +1,69 @@ +use crate::{core::*, model::*}; + +pub fn handle (s: &mut Plugin, event: &AppEvent) -> Usually { + handle_keymap(s, event, keymap!(Plugin { + + [Up, NONE, "cursor_up", "move cursor up", + |s: &mut Plugin|{ + if s.selected > 0 { + s.selected = s.selected - 1 + } else { + s.selected = match &s.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, + _ => 0 + } + } + Ok(true) + }], + + [Down, NONE, "cursor_down", "move cursor down", + |s: &mut Plugin|{ + s.selected = s.selected + 1; + match &s.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => { + if s.selected >= port_list.len() { + s.selected = 0; + } + }, + _ => {} + } + Ok(true) + }], + + [Char(','), NONE, "decrement", "decrement value", + |s: &mut Plugin|{ + match s.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + let index = port_list[s.selected].index; + if let Some(value) = instance.control_input(index) { + instance.set_control_input(index, value - 0.01); + } + }, + _ => {} + } + Ok(true) + }], + + [Char('.'), NONE, "increment", "increment value", + |s: &mut Plugin|{ + match s.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { + let index = port_list[s.selected].index; + if let Some(value) = instance.control_input(index) { + instance.set_control_input(index, value + 0.01); + } + }, + _ => {} + } + Ok(true) + }], + + [Char('m'), NONE, "toggle_midi_map", "toggle midi map mode", + |s: &mut Plugin|{ + s.mapping = !s.mapping; + Ok(true) + }] + + })) +} + diff --git a/src/control/sampler.rs b/src/control/sampler.rs new file mode 100644 index 00000000..9c18528f --- /dev/null +++ b/src/control/sampler.rs @@ -0,0 +1,45 @@ +use crate::{core::*, model::*}; + +pub fn handle (state: &mut Sampler, event: &AppEvent) -> Usually { + Ok(handle_keymap(state, event, KEYMAP)?) +} + +pub const KEYMAP: &'static [KeyBinding] = keymap!(Sampler { + [Up, NONE, "cursor_up", "move cursor up", cursor_up], + [Down, NONE, "cursor_down", "move cursor down", cursor_down], + [Char('t'), NONE, "trigger", "play current sample", trigger], + [Enter, NONE, "select", "select item under cursor", select], +}); + +fn cursor_up (state: &mut Sampler) -> Usually { + state.cursor.0 = if state.cursor.0 == 0 { + state.samples.len() - 1 + } else { + state.cursor.0 - 1 + }; + Ok(true) +} + +fn cursor_down (state: &mut Sampler) -> Usually { + state.cursor.0 = (state.cursor.0 + 1) % state.samples.len(); + Ok(true) +} + +fn trigger (state: &mut Sampler) -> Usually { + for (i, sample) in state.samples.values().enumerate() { + if i == state.cursor.0 { + state.voices.push(sample.play(0)) + } + } + Ok(true) +} + +fn select (state: &mut Sampler) -> Usually { + for (i, _sample) in state.samples.values().enumerate() { + if i == state.cursor.0 { + //state.voices.push(sample.play(0)) + } + } + Ok(true) +} + diff --git a/src/device/sequencer/handle.rs b/src/control/sequencer.rs similarity index 91% rename from src/device/sequencer/handle.rs rename to src/control/sequencer.rs index ea8fe332..93d29a92 100644 --- a/src/device/sequencer/handle.rs +++ b/src/control/sequencer.rs @@ -1,8 +1,9 @@ -use crate::core::*; -use super::*; -pub fn handle (s: &mut Sequencer, event: &AppEvent) -> Usually { - handle_keymap(s, event, KEYMAP) +use crate::{core::*, model::*}; + +pub fn handle (state: &mut Sequencer, event: &AppEvent) -> Usually { + handle_keymap(state, event, KEYMAP) } + pub const KEYMAP: &'static [KeyBinding] = keymap!(Sequencer { [Up, NONE, "cursor_up", "move cursor up", cursor_up], [Down, NONE, "cursor_down", "move cursor down", cursor_down], @@ -37,15 +38,18 @@ pub const KEYMAP: &'static [KeyBinding] = keymap!(Sequencer { [Char('7'), NONE, "seq_7", "Phrase 7", focus_seq(6)], [Char('8'), NONE, "seq_8", "Phrase 8", focus_seq(7)], }); + const fn focus_seq (i: usize) -> impl Fn(&mut Sequencer)->Usually { move |s: &mut Sequencer| { s.sequence = Some(i); Ok(true) } } + fn nop (_: &mut Sequencer) -> Usually { Ok(false) } + fn note_add (s: &mut Sequencer) -> Usually { if s.sequence.is_none() { return Ok(false) @@ -70,70 +74,82 @@ fn note_add (s: &mut Sequencer) -> Usually { }; Ok(true) } + fn note_del (_: &mut Sequencer) -> Usually { Ok(true) } + fn time_cursor_inc (s: &mut Sequencer) -> Usually { s.time_cursor = s.time_cursor + 1; Ok(true) } + fn time_cursor_dec (s: &mut Sequencer) -> Usually { s.time_cursor = s.time_cursor.saturating_sub(1); Ok(true) } + fn note_cursor_inc (s: &mut Sequencer) -> Usually { s.note_cursor = s.note_cursor + 1; Ok(true) } + fn note_cursor_dec (s: &mut Sequencer) -> Usually { s.note_cursor = s.note_cursor.saturating_sub(1); Ok(true) } + fn cursor_up (s: &mut Sequencer) -> Usually { match s.view { - SequencerView::Vertical => time_cursor_dec(s), - SequencerView::Horizontal => note_cursor_dec(s), + SequencerMode::Vertical => time_cursor_dec(s), + SequencerMode::Horizontal => note_cursor_dec(s), _ => Ok(false) }?; Ok(true) } + fn cursor_down (s: &mut Sequencer) -> Usually { match s.view { - SequencerView::Vertical => time_cursor_inc(s), - SequencerView::Horizontal => note_cursor_inc(s), + SequencerMode::Vertical => time_cursor_inc(s), + SequencerMode::Horizontal => note_cursor_inc(s), _ => Ok(false) }?; Ok(true) } + fn cursor_left (s: &mut Sequencer) -> Usually { match s.view { - SequencerView::Vertical => note_cursor_dec(s), - SequencerView::Horizontal => time_cursor_dec(s), + SequencerMode::Vertical => note_cursor_dec(s), + SequencerMode::Horizontal => time_cursor_dec(s), _ => Ok(false) }?; Ok(true) } + fn cursor_right (s: &mut Sequencer) -> Usually { match s.view { - SequencerView::Vertical => note_cursor_inc(s), - SequencerView::Horizontal => time_cursor_inc(s), + SequencerMode::Vertical => note_cursor_inc(s), + SequencerMode::Horizontal => time_cursor_inc(s), _ => Ok(false) }?; Ok(true) } + fn cursor_duration_inc (_: &mut Sequencer) -> Usually { //s.cursor.2 = s.cursor.2 + 1 Ok(true) } + fn cursor_duration_dec (_: &mut Sequencer) -> Usually { //if s.cursor.2 > 0 { s.cursor.2 = s.cursor.2 - 1 } Ok(true) } + fn mode_next (s: &mut Sequencer) -> Usually { s.view = s.view.next(); Ok(true) } -impl SequencerView { +impl SequencerMode { fn next (&self) -> Self { match self { Self::Horizontal => Self::Vertical, @@ -143,12 +159,14 @@ impl SequencerView { } } } + fn stop_and_rewind (s: &mut Sequencer) -> Usually { s.transport.stop()?; s.transport.locate(0)?; s.playing = TransportState::Stopped; Ok(true) } + fn toggle_play (s: &mut Sequencer) -> Usually { s.playing = match s.playing { TransportState::Stopped => { @@ -163,24 +181,29 @@ fn toggle_play (s: &mut Sequencer) -> Usually { }; Ok(true) } + fn toggle_record (s: &mut Sequencer) -> Usually { s.recording = !s.recording; Ok(true) } + fn toggle_overdub (s: &mut Sequencer) -> Usually { s.overdub = !s.overdub; Ok(true) } + fn toggle_monitor (s: &mut Sequencer) -> Usually { s.monitoring = !s.monitoring; Ok(true) } + fn quantize_next (s: &mut Sequencer) -> Usually { if s.time_zoom < 64 { s.time_zoom = s.time_zoom * 2; } Ok(true) } + fn quantize_prev (s: &mut Sequencer) -> Usually { if s.time_zoom > 1 { s.time_zoom = s.time_zoom / 2; @@ -192,6 +215,7 @@ fn zoom_in (s: &mut Sequencer) -> Usually { s.time_zoom = s.time_zoom / 2; Ok(true) } + fn zoom_out (s: &mut Sequencer) -> Usually { s.time_zoom = s.time_zoom * 2; Ok(true) diff --git a/src/core/mod.rs b/src/core.rs similarity index 78% rename from src/core/mod.rs rename to src/core.rs index 2332b938..6849295d 100644 --- a/src/core/mod.rs +++ b/src/core.rs @@ -6,6 +6,8 @@ pub use std::collections::BTreeMap; pub use std::sync::{Arc, Mutex, MutexGuard}; pub use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize}; pub use std::sync::mpsc::{channel, Sender, Receiver}; + +pub use microxdg::XdgApp; pub use ratatui::prelude::*; pub use midly::{MidiMessage, live::LiveEvent, num::u7}; pub use crossterm::{ExecutableCommand, QueueableCommand}; @@ -13,8 +15,15 @@ pub use crossterm::event::{Event, KeyEvent, KeyCode, KeyModifiers}; macro_rules! submod { ($($name:ident)*) => { $(mod $name; pub use self::$name::*;)* }; } -submod!( device handle jack keymap port render run time ); +submod!( device handle jack keymap midi port render run time ); pub type Usually = Result>; -pub use crate::{key, keymap}; +// Reexport macros: +pub use crate::{ + render, + handle, + process, + keymap, + key +}; diff --git a/src/core/device.rs b/src/core/device.rs index 10b48f81..3465c9f3 100644 --- a/src/core/device.rs +++ b/src/core/device.rs @@ -1,5 +1,9 @@ use crate::core::*; +pub trait Component: Render + Handle {} + +impl Component for T {} + /// A UI component that may have presence on the JACK grap. pub trait Device: Render + Handle + PortList + Send + Sync { fn boxed (self) -> Box where Self: Sized + 'static { @@ -25,7 +29,7 @@ impl Handle for DynamicDevice { } } -impl Render for DynamicDevice { +impl Render for DynamicDevice { fn render (&self, buf: &mut Buffer, area: Rect) -> Usually { self.render.lock().unwrap()(&*self.state.lock().unwrap(), buf, area) } diff --git a/src/core/handle.rs b/src/core/handle.rs index 246edd0c..8ba6cd6d 100644 --- a/src/core/handle.rs +++ b/src/core/handle.rs @@ -10,6 +10,26 @@ pub trait Handle { } } +#[macro_export] macro_rules! handle { + ($T:ty) => { + impl Handle for $T {} + }; + ($T:ty |$self:ident, $e:ident|$block:tt) => { + impl Handle for $T { + fn handle (&mut $self, $e: &AppEvent) -> Usually { + $block + } + } + }; + ($T:ty = $handle:path) => { + impl Handle for $T { + fn handle (&mut self, e: &AppEvent) -> Usually { + $handle(self, e) + } + } + } +} + #[derive(Debug)] pub enum AppEvent { /// Terminal input diff --git a/src/core/jack.rs b/src/core/jack.rs index 71dbec7e..fccb0743 100644 --- a/src/core/jack.rs +++ b/src/core/jack.rs @@ -1,7 +1,51 @@ use crate::core::*; +pub fn jack_run (name: &str, app: &Arc>) -> Usually + where T: Handle + Process + Send + 'static +{ + let options = ClientOptions::NO_START_SERVER; + let (client, _status) = Client::new(name, options)?; + Ok(client.activate_async( + Notifications(Box::new({ + let _app = app.clone(); + move|_event|{ + // FIXME: this deadlocks + //app.lock().unwrap().handle(&event).unwrap(); + } + }) as Box), + ClosureProcessHandler::new(Box::new({ + let app = app.clone(); + move|c: &Client, s: &ProcessScope|{ + app.lock().unwrap().process(c, s) + } + }) as BoxedProcessHandler) + )?) +} + pub trait Process { - fn process (&mut self, c: &Client, s: &ProcessScope) -> Control; + fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { + Control::Continue + } +} + +#[macro_export] macro_rules! process { + ($T:ty) => { + impl Process for $T {} + }; + ($T:ty |$self:ident, $c:ident, $s:ident|$block:tt) => { + impl Process for $T { + fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { + $block + } + } + }; + ($T:ty = $process:path) => { + impl Process for $T { + fn process (&mut self, c: &Client, s: &ProcessScope) -> Control { + $process(self, c, s) + } + } + } } pub type DynamicAsyncClient = @@ -16,23 +60,6 @@ pub type DynamicProcessHandler = pub type BoxedProcessHandler = Box Control + Send>; -pub fn jack_run (name: &str, app: &Arc>) -> Usually - where T: Handle + Process + Send + 'static -{ - let options = ClientOptions::NO_START_SERVER; - let (client, _status) = Client::new(name, options)?; - Ok(client.activate_async( - Notifications(Box::new({ - let app = app.clone(); - move|event|{app.lock().unwrap().handle(&event).unwrap();} - }) as Box), - ClosureProcessHandler::new(Box::new({ - let app = app.clone(); - move|c: &Client, s: &ProcessScope|{app.lock().unwrap().process(c, s)} - }) as BoxedProcessHandler) - )?) -} - pub use ::jack::{ AsyncClient, AudioIn, diff --git a/src/device/sequencer/keys.rs b/src/core/midi.rs similarity index 82% rename from src/device/sequencer/keys.rs rename to src/core/midi.rs index 80a4c730..88f5ed4a 100644 --- a/src/device/sequencer/keys.rs +++ b/src/core/midi.rs @@ -1,5 +1,14 @@ use crate::core::*; +pub type PhraseData = + BTreeMap>; + +pub type MIDIMessage = + Vec; + +pub type MIDIChunk = + [Option>]; + pub const KEY_WHITE: Style = Style { fg: Some(Color::Gray), bg: None, @@ -24,3 +33,4 @@ pub const KEY_STYLE: [Style;12] = [ pub const KEYS_VERTICAL: [&'static str; 6] = [ "▄", "▄", "█", "▀", "▀", "▀", ]; + diff --git a/src/core/render.rs b/src/core/render.rs index 7ec278fa..84a3dd64 100644 --- a/src/core/render.rs +++ b/src/core/render.rs @@ -2,34 +2,38 @@ use crate::core::*; use ratatui::widgets::WidgetRef; /// Trait for things that render to the display. -pub trait Render { +pub trait Render: Send { // Render something to an area of the buffer. // Returns area used by component. // This is insufficient but for the most basic dynamic layout algorithms. fn render (&self, _b: &mut Buffer, _a: Rect) -> Usually { Ok(Rect { x: 0, y: 0, width: 0, height: 0 }) } - fn min_width (&self) -> u16 { - 0 - } - fn max_width (&self) -> u16 { - u16::MAX - } - fn min_height (&self) -> u16 { - 0 - } - fn max_height (&self) -> u16 { - u16::MAX - } - - //fn boxed (self) -> Box where Self: Sized + 'static { - //Box::new(self) - //} } -impl Usually> Render for T { +#[macro_export] macro_rules! render { + ($T:ty) => { + impl Render for $T {} + }; + ($T:ty |$self:ident, $buf:ident, $area:ident|$block:tt) => { + impl Render for $T { + fn render (&$self, $buf: &mut Buffer, $area: Rect) -> Usually { + $block + } + } + }; + ($T:ty = $render:path) => { + impl Render for $T { + fn render (&self, buf: &mut Buffer, area: Rect) -> Usually { + $render(self, buf, area) + } + } + } +} + +impl Usually + Send> Render for T { fn render (&self, b: &mut Buffer, a: Rect) -> Usually { - (*self).render(b, a) + (*self)(b, a) } } diff --git a/src/core/run.rs b/src/core/run.rs index 39ecdb80..0a9ad3a7 100644 --- a/src/core/run.rs +++ b/src/core/run.rs @@ -1,28 +1,77 @@ use crate::core::*; +use better_panic::{Settings, Verbosity}; use crossterm::terminal::{ EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode }; +impl Run for T {} + pub trait Run: Render + Handle + Send + Sized + 'static { - fn run (self, callback: Option>)->Usually<()>>) -> Usually<()> { - let device = Arc::new(Mutex::new(self)); + fn run (self, init: Option>)->Usually<()>>) -> Usually<()> { + let app = Arc::new(Mutex::new(self)); let exited = Arc::new(AtomicBool::new(false)); - let _input_thread = input_thread(&exited, &device); + let _input_thread = input_thread(&exited, &app); terminal_setup()?; panic_hook_setup(); - let main_thread = main_thread(&exited, &device)?; - if let Some(callback) = callback { - callback(device); + let main_thread = main_thread(&exited, &app)?; + if let Some(init) = init { + init(app)?; } - main_thread.join(); + main_thread.join().unwrap(); terminal_teardown()?; Ok(()) } } -impl Run for T {} +/// Set up panic hook +pub fn panic_hook_setup () { + let better_panic_handler = Settings::auto().verbosity(Verbosity::Full).create_panic_handler(); + std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{ + stdout().execute(LeaveAlternateScreen).unwrap(); + disable_raw_mode().unwrap(); + better_panic_handler(info); + })); +} +/// Set up terminal +pub fn terminal_setup () -> Usually<()> { + stdout().execute(EnterAlternateScreen)?; + enable_raw_mode()?; + Ok(()) +} +/// Cleanup +pub fn terminal_teardown () -> Usually<()> { + stdout().execute(LeaveAlternateScreen)?; + disable_raw_mode()?; + Ok(()) +} + +/// Main thread render loop +pub fn main_thread ( + exited: &Arc, + device: &Arc> +) -> Usually> { + let exited = exited.clone(); + let device = device.clone(); + let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?; + let sleep = std::time::Duration::from_millis(16); + Ok(spawn(move || loop { + + terminal.draw(|frame|{ + let area = frame.size(); + let buffer = frame.buffer_mut(); + device.lock().unwrap().render(buffer, area).expect("Failed to render content."); + }).expect("Failed to render frame"); + + if exited.fetch_and(true, Ordering::Relaxed) { + break + } + + std::thread::sleep(sleep); + + })) +} /// Spawn thread that listens for user input pub fn input_thread ( exited: &Arc, @@ -54,52 +103,3 @@ pub fn input_thread ( } }) } - -/// Set up terminal -pub fn terminal_setup () -> Usually<()> { - stdout().execute(EnterAlternateScreen)?; - enable_raw_mode()?; - Ok(()) -} -/// Set up panic hook -pub fn panic_hook_setup () { - let better_panic_handler = better_panic::Settings::auto() - .verbosity(better_panic::Verbosity::Full) - .create_panic_handler(); - std::panic::set_hook(Box::new(move |info: &std::panic::PanicInfo|{ - stdout().execute(LeaveAlternateScreen).unwrap(); - crossterm::terminal::disable_raw_mode().unwrap(); - better_panic_handler(info); - })); -} -/// Main thread render loop -pub fn main_thread ( - exited: &Arc, - device: &Arc> -) -> Usually> { - let exited = exited.clone(); - let device = device.clone(); - let mut terminal = ratatui::Terminal::new(CrosstermBackend::new(stdout()))?; - let sleep = std::time::Duration::from_millis(16); - Ok(spawn(move || loop { - - terminal.draw(|frame|{ - let area = frame.size(); - let buffer = frame.buffer_mut(); - device.lock().unwrap().render(buffer, area).expect("Failed to render content."); - }).expect("Failed to render frame"); - - if exited.fetch_and(true, Ordering::Relaxed) { - break - } - - std::thread::sleep(sleep); - - })) -} -/// Cleanup -pub fn terminal_teardown () -> Usually<()> { - stdout().execute(LeaveAlternateScreen)?; - crossterm::terminal::disable_raw_mode()?; - Ok(()) -} diff --git a/src/core/time.rs b/src/core/time.rs index da54d441..2218ac5b 100644 --- a/src/core/time.rs +++ b/src/core/time.rs @@ -1,15 +1,23 @@ use crate::core::*; - -#[derive(Default)] +use atomic_float::AtomicF64; +#[derive(Debug)] pub struct Timebase { /// Frames per second - pub rate: ::atomic_float::AtomicF64, + pub rate: AtomicF64, /// Beats per minute - pub bpm: ::atomic_float::AtomicF64, + pub bpm: AtomicF64, /// Ticks per beat - pub ppq: ::atomic_float::AtomicF64, + pub ppq: AtomicF64, +} +impl Default for Timebase { + fn default () -> Self { + Self { + rate: 48000f64.into(), + bpm: 125f64.into(), + ppq: 96f64.into(), + } + } } - impl Timebase { pub fn new (rate: f64, bpm: f64, ppq: f64) -> Self { Self { rate: rate.into(), bpm: bpm.into(), ppq: ppq.into() } @@ -146,7 +154,6 @@ impl Timebase { ticks } } - #[cfg(test)] mod test { use super::*; diff --git a/src/device/chain/plugin.rs b/src/device/chain/plugin.rs deleted file mode 100644 index 4150a833..00000000 --- a/src/device/chain/plugin.rs +++ /dev/null @@ -1,281 +0,0 @@ -use crate::core::*; - -mod lv2; pub use lv2::*; -mod vst2; -mod vst3; - -pub struct Plugin { - name: String, - path: Option, - plugin: Option, - selected: usize, - mapping: bool, - midi_ins: Vec>, - midi_outs: Vec>, - audio_ins: Vec>, - audio_outs: Vec>, -} - -enum PluginKind { - LV2(LV2Plugin), - VST2 { - instance: ::vst::host::PluginInstance - }, - VST3, -} - -const HELM: &'static str = "file:///nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lv2/helm.lv2"; - -impl Plugin { - /// Load a LV2 plugin. - pub fn lv2 (name: &str, path: &str) -> Usually> { - let host = Self::new(name)?; - let plugin = LV2Plugin::new(path)?; - let mut state = host.state(); - let client = host.client.as_ref().unwrap().as_client(); - let (midi_ins, midi_outs, audio_ins, audio_outs) = ( - plugin.plugin.port_counts().atom_sequence_inputs, - plugin.plugin.port_counts().atom_sequence_outputs, - plugin.plugin.port_counts().audio_inputs, - plugin.plugin.port_counts().audio_outputs, - ); - state.midi_ins = { - let mut ports = vec![]; - for i in 0..midi_ins { - ports.push(client.register_port(&format!("midi-in-{i}"), MidiIn::default())?) - } - ports - }; - state.midi_outs = { - let mut ports = vec![]; - for i in 0..midi_outs { - ports.push(client.register_port(&format!("midi-out-{i}"), MidiOut::default())?) - } - ports - }; - state.audio_ins = { - let mut ports = vec![]; - for i in 0..audio_ins { - ports.push(client.register_port(&format!("audio-in-{i}"), AudioIn::default())?) - } - ports - }; - state.audio_outs = { - let mut ports = vec![]; - for i in 0..audio_outs { - ports.push(client.register_port(&format!("audio-out-{i}"), AudioOut::default())?) - } - ports - }; - state.plugin = Some(PluginKind::LV2(plugin)); - state.path = Some(String::from(path)); - std::mem::drop(state); - Ok(host) - } - pub fn new (name: &str) -> Usually> { - DynamicDevice::new(render, handle, Self::process, Self { - name: name.into(), - path: None, - plugin: None, - selected: 0, - mapping: false, - midi_ins: vec![], - midi_outs: vec![], - audio_ins: vec![], - audio_outs: vec![], - }).activate(Client::new(name, ClientOptions::NO_START_SERVER)?.0) - } - pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - match self.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { features, ref mut instance, .. })) => { - let urid = features.midi_urid(); - let mut inputs = vec![]; - for port in self.midi_ins.iter() { - let mut atom = ::livi::event::LV2AtomSequence::new( - &features, - scope.n_frames() as usize - ); - for event in port.iter(scope) { - match event.bytes.len() { - 3 => atom.push_midi_event::<3>( - event.time as i64, - urid, - &event.bytes[0..3] - ).unwrap(), - _ => {} - } - } - inputs.push(atom); - } - let mut outputs = vec![]; - for _ in self.midi_outs.iter() { - outputs.push(::livi::event::LV2AtomSequence::new( - &features, - scope.n_frames() as usize - )); - } - let ports = ::livi::EmptyPortConnections::new() - .with_atom_sequence_inputs( - inputs.iter() - ) - .with_atom_sequence_outputs( - outputs.iter_mut() - ) - .with_audio_inputs( - self.audio_ins.iter().map(|o|o.as_slice(scope)) - ) - .with_audio_outputs( - self.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)) - ); - unsafe { - instance.run(scope.n_frames() as usize, ports).unwrap() - }; - }, - _ => {} - } - Control::Continue - } -} - -impl PortList for Plugin { - fn audio_ins (&self) -> Usually> { - let mut ports = vec![]; - for port in self.audio_ins.iter() { - ports.push(port.name()?); - } - Ok(ports) - } - fn audio_outs (&self) -> Usually> { - let mut ports = vec![]; - for port in self.audio_outs.iter() { - ports.push(port.name()?); - } - Ok(ports) - } - fn midi_ins (&self) -> Usually> { - let mut ports = vec![]; - for port in self.midi_ins.iter() { - ports.push(port.name()?); - } - Ok(ports) - } - fn midi_outs (&self) -> Usually> { - let mut ports = vec![]; - for port in self.midi_outs.iter() { - ports.push(port.name()?); - } - Ok(ports) - } -} - -pub fn render (state: &Plugin, buf: &mut Buffer, area: Rect) - -> Usually -{ - let Rect { x, y, height, .. } = area; - let mut width = 20u16; - match &state.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => { - let start = state.selected.saturating_sub((height as usize / 2).saturating_sub(1)); - let end = start + height as usize - 2; - //draw_box(buf, Rect { x, y, width, height }); - for i in start..end { - if let Some(port) = port_list.get(i) { - let value = if let Some(value) = instance.control_input(port.index) { - value - } else { - port.default_value - }; - //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); - let label = &format!("{:25} = {value:.03}", port.name); - width = width.max(label.len() as u16 + 4); - label.blit(buf, x + 2, y + 1 + i as u16 - start as u16, if i == state.selected { - Some(Style::default().green()) - } else { - None - }); - } else { - break - } - } - }, - _ => {} - }; - draw_header(state, buf, area.x, area.y, width)?; - Ok(Rect { width, ..area }) -} - -fn draw_header (state: &Plugin, buf: &mut Buffer, x: u16, y: u16, w: u16) -> Usually { - let style = Style::default().gray(); - let label1 = format!(" {}", state.name); - label1.blit(buf, x + 1, y, Some(style.white().bold())); - let label2 = format!("{}…", &HELM[..(w as usize - 10).min(HELM.len())]); - label2.blit(buf, x + 2 + label1.len() as u16, y, Some(style.not_dim())); - Ok(Rect { x, y, width: w, height: 1 }) -} - -pub fn handle (s: &mut Plugin, event: &AppEvent) -> Usually { - handle_keymap(s, event, keymap!(Plugin { - - [Up, NONE, "cursor_up", "move cursor up", - |s: &mut Plugin|{ - if s.selected > 0 { - s.selected = s.selected - 1 - } else { - s.selected = match &s.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1, - _ => 0 - } - } - Ok(true) - }], - - [Down, NONE, "cursor_down", "move cursor down", - |s: &mut Plugin|{ - s.selected = s.selected + 1; - match &s.plugin { - Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => { - if s.selected >= port_list.len() { - s.selected = 0; - } - }, - _ => {} - } - Ok(true) - }], - - [Char(','), NONE, "decrement", "decrement value", - |s: &mut Plugin|{ - match s.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - let index = port_list[s.selected].index; - if let Some(value) = instance.control_input(index) { - instance.set_control_input(index, value - 0.01); - } - }, - _ => {} - } - Ok(true) - }], - - [Char('.'), NONE, "increment", "increment value", - |s: &mut Plugin|{ - match s.plugin.as_mut() { - Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => { - let index = port_list[s.selected].index; - if let Some(value) = instance.control_input(index) { - instance.set_control_input(index, value + 0.01); - } - }, - _ => {} - } - Ok(true) - }], - - [Char('m'), NONE, "toggle_midi_map", "toggle midi map mode", - |s: &mut Plugin|{ - s.mapping = !s.mapping; - Ok(true) - }] - - })) -} diff --git a/src/device/chain/sampler.rs b/src/device/chain/sampler.rs deleted file mode 100644 index 5944c504..00000000 --- a/src/device/chain/sampler.rs +++ /dev/null @@ -1,263 +0,0 @@ -use crate::core::*; - -pub struct Voice { - sample: Arc, - after: usize, - position: usize, -} -impl Voice { - fn chunk (&mut self, mut frames: usize) -> Option>> { - // Create output buffer for each channel - let mut chunk = vec![vec![];self.sample.channels.len()]; - // If it's not time to play yet, count down - if self.after >= frames { - self.after = self.after - frames; - return Some(chunk) - } - // If the voice will start playing within the current buffer, - // subtract the remaining number of wait frames. - if self.after > 0 && self.after < frames { - chunk = vec![vec![0.0;self.after];self.sample.channels.len()]; - frames = frames - self.after; - self.after = 0; - } - if self.position < self.sample.end { - let start = self.position.min(self.sample.end); - let end = (self.position + frames).min(self.sample.end); - for (i, channel) in self.sample.channels.iter().enumerate() { - chunk[i].extend_from_slice(&channel[start..end]); - }; - self.position = self.position + frames; - Some(chunk) - } else { - None - } - } -} - -pub struct Sample { - pub name: String, - pub start: usize, - pub end: usize, - pub channels: Vec>, -} -impl Sample { - pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Arc { - Arc::new(Self { name: name.to_string(), start, end, channels }) - } - fn play (self: &Arc, after: usize) -> Voice { - Voice { sample: self.clone(), after, position: self.start } - } -} - -pub struct Sampler { - name: String, - cursor: (usize, usize), - samples: BTreeMap>, - voices: Vec, - midi_in: Port, - audio_ins: Vec>, - audio_outs: Vec>, -} - -impl Sampler { - pub fn new ( - name: &str, - samples: Option>>, - ) -> Result, Box> { - let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; - DynamicDevice::new(render, handle, Self::process, Self { - name: name.into(), - cursor: (0, 0), - samples: samples.unwrap_or(BTreeMap::new()), - voices: vec![], - midi_in: client.register_port("midi", ::jack::MidiIn::default())?, - audio_ins: vec![ - client.register_port("recL", ::jack::AudioIn::default())?, - client.register_port("recR", ::jack::AudioIn::default())?, - ], - audio_outs: vec![ - client.register_port("outL", ::jack::AudioOut::default())?, - client.register_port("outR", ::jack::AudioOut::default())?, - ], - }).activate(client) - } - - pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - // Output buffer: this will be copied to the audio outs. - let channel_count = self.audio_outs.len(); - let mut mixed = vec![vec![0.0;scope.n_frames() as usize];channel_count]; - // Process MIDI input to add new voices. - for RawMidi { time, bytes } in self.midi_in.iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - if let MidiMessage::NoteOn { ref key, .. } = message { - if let Some(sample) = self.samples.get(key) { - self.voices.push(sample.play(time as usize)); - } - } - } - } - // Emit next chunk of each currently playing voice, - // dropping voices that have reached their ends. - let mut voices = vec![]; - std::mem::swap(&mut voices, &mut self.voices); - loop { - if voices.len() < 1 { - break - } - let mut voice = voices.swap_remove(0); - if let Some(chunk) = voice.chunk(scope.n_frames() as usize) { - for (i, channel) in chunk.iter().enumerate() { - let buffer = &mut mixed[i % channel_count]; - for (i, sample) in channel.iter().enumerate() { - buffer[i] += sample; - } - } - self.voices.push(voice); - } - } - // Write output buffer to output ports. - for (i, port) in self.audio_outs.iter_mut().enumerate() { - let buffer = &mixed[i]; - for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { - *value = *buffer.get(i).unwrap_or(&0.0); - } - } - Control::Continue - } - - fn load_sample (&mut self, _path: &str) {} -} - -impl PortList for Sampler { - fn midi_ins (&self) -> Usually> { - Ok(vec![self.midi_in.name()?]) - } - fn audio_ins (&self) -> Usually> { - let mut ports = vec![]; - for port in self.audio_ins.iter() { - ports.push(port.name()?); - } - Ok(ports) - } - fn audio_outs (&self) -> Usually> { - let mut ports = vec![]; - for port in self.audio_outs.iter() { - ports.push(port.name()?); - } - Ok(ports) - } -} - -pub fn render (state: &Sampler, buf: &mut Buffer, Rect { x, y, height, .. }: Rect) - -> Usually -{ - let style = Style::default().gray(); - let title = format!(" {} ({})", state.name, state.voices.len()); - title.blit(buf, x+1, y, Some(style.white().bold().not_dim())); - let mut width = title.len() + 2; - for (i, (note, sample)) in state.samples.iter().enumerate() { - let style = if i == state.cursor.0 { - Style::default().green() - } else { - Style::default() - }; - let i = i as u16; - let y1 = y+1+i; - if y1 >= y + height { - break - } - if i as usize == state.cursor.0 { - "⯈".blit(buf, x+1, y1, Some(style.bold())); - } - let label1 = format!("{note:3} {:8}", sample.name); - let label2 = format!("{:>6} {:>6}", sample.start, sample.end); - label1.blit(buf, x+2, y1, Some(style.bold())); - label2.blit(buf, x+3+label1.len()as u16, y1, Some(style)); - width = width.max(label1.len() + label2.len() + 4); - } - let height = ((1 + state.samples.len()) as u16).min(height); - Ok(Rect { x, y, width: width as u16, height }) -} - -//fn render_table ( - //state: &mut Sampler, - //stdout: &mut Stdout, - //offset: (u16, u16), -//) -> Result<(), Box> { - //let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); - //stdout.queue(move_to(0, 3))?.queue( - //Print(" Name Rate Trigger Route") - //)?; - //for (i, sample) in state.samples.lock().unwrap().iter().enumerate() { - //let row = 4 + i as u16; - //for (j, (column, field)) in [ - //(0, format!(" {:7} ", sample.name)), - //(9, format!(" {:.1}Hz ", sample.rate)), - //(18, format!(" MIDI C{} {} ", sample.trigger.0, sample.trigger.1)), - //(33, format!(" {:.1}dB -> Output ", sample.gain)), - //(50, format!(" {} ", sample.playing.unwrap_or(0))), - //].into_iter().enumerate() { - //stdout.queue(move_to(column, row))?; - //if state.selected_sample == i && state.selected_column == j { - //stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?; - //} else { - //stdout.queue(PrintStyledContent(field.to_string().bold()))?; - //} - //} - //} - //Ok(()) -//} - -//fn render_meters ( - //state: &mut Sampler, - //stdout: &mut Stdout, - //offset: (u16, u16), -//) -> Result<(), Box> { - //let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); - //for (i, sample) in state.samples.lock().iter().enumerate() { - //let row = 4 + i as u16; - //stdout.queue(move_to(32, row))?.queue( - //PrintStyledContent("▁".green()) - //)?; - //} - //Ok(()) -//} - -pub fn handle (state: &mut Sampler, event: &AppEvent) -> Usually { - Ok(handle_keymap(state, event, KEYMAP)?) -} -pub const KEYMAP: &'static [KeyBinding] = keymap!(Sampler { - [Up, NONE, "cursor_up", "move cursor up", cursor_up], - [Down, NONE, "cursor_down", "move cursor down", cursor_down], - [Char('t'), NONE, "trigger", "play current sample", trigger], - [Enter, NONE, "select", "select item under cursor", select], -}); -fn cursor_up (state: &mut Sampler) -> Usually { - state.cursor.0 = if state.cursor.0 == 0 { - state.samples.len() - 1 - } else { - state.cursor.0 - 1 - }; - Ok(true) -} -fn cursor_down (state: &mut Sampler) -> Usually { - state.cursor.0 = (state.cursor.0 + 1) % state.samples.len(); - Ok(true) -} -fn trigger (state: &mut Sampler) -> Usually { - for (i, sample) in state.samples.values().enumerate() { - if i == state.cursor.0 { - state.voices.push(sample.play(0)) - } - } - Ok(true) -} -fn select (state: &mut Sampler) -> Usually { - for (i, _sample) in state.samples.values().enumerate() { - if i == state.cursor.0 { - //state.voices.push(sample.play(0)) - } - } - Ok(true) -} diff --git a/src/device/launcher/mod.rs b/src/device/launcher/mod.rs deleted file mode 100644 index 21119ec0..00000000 --- a/src/device/launcher/mod.rs +++ /dev/null @@ -1,281 +0,0 @@ -use crate::core::*; -use crate::layout::*; -use crate::device::*; -mod grid; pub use self::grid::*; -mod handle; pub use self::handle::*; -mod scene; pub use self::scene::*; -pub struct Launcher { - name: String, - timebase: Arc, - transport: ::jack::Transport, - playing: TransportState, - monitoring: bool, - recording: bool, - overdub: bool, - current_frame: usize, - cursor: (usize, usize), - pub tracks: Vec, - scenes: Vec, - show_help: bool, - view: LauncherView, - modal: Option>>, -} -impl Launcher { - pub fn new ( - name: &str, - timebase: &Arc, - tracks: Option>, - scenes: Option> - ) -> Result, Box> { - let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; - let transport = client.transport(); - let ppq = timebase.ppq() as usize; - DynamicDevice::new(render, handle, process, Self { - name: name.into(), - view: LauncherView::Chains, - playing: transport.query_state()?, - transport, - timebase: timebase.clone(), - monitoring: true, - recording: false, - overdub: true, - cursor: (2, 2), - current_frame: 0, - scenes: scenes.unwrap_or_else(||vec![Scene::new(&"Scene 1", &[None])]), - tracks: if let Some(tracks) = tracks { tracks } else { vec![ - Track::new("Track 1", &timebase, None, Some(vec![ - Phrase::new("MIDI Clip 1", ppq * 4, Some(BTreeMap::from([ - ( ppq * 0, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ), - ( ppq * 1, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ), - ( ppq * 2, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ), - ( ppq * 3, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ), - ]))) - ]))?, - ] }, - show_help: true, - modal: None - }).activate(client) - } - fn cols (&self) -> usize { - (self.tracks.len() + 2) as usize - } - fn col (&self) -> usize { - self.cursor.0 as usize - } - fn dec_col (&mut self) { - self.cursor.0 = if self.cursor.0 > 0 { - self.cursor.0 - 1 - } else { - (self.cols() - 1) as usize - } - } - fn inc_col (&mut self) { - self.cursor.0 = if self.cursor.0 >= self.cols() - 1 { - 0 - } else { - self.cursor.0 + 1 - } - } - fn rows (&self) -> usize { - (self.scenes.len() + 2) as usize - } - fn row (&self) -> usize { - self.cursor.1 as usize - } - fn dec_row (&mut self) { - self.cursor.1 = if self.cursor.1 > 0 { - self.cursor.1 - 1 - } else { - self.rows() - 1 - } - } - fn inc_row (&mut self) { - self.cursor.1 = if self.cursor.1 >= self.rows() - 1 { - 0 - } else { - self.cursor.1 + 1 - } - } - - pub fn track <'a> (&'a self) -> Option<(usize, &'a Track)> { - match self.col() { 0 => None, _ => { - let id = self.col() as usize - 1; - self.tracks.get(id).map(|t|(id, t)) - } } - } - pub fn scene <'a> (&'a self) -> Option<(usize, &'a Scene)> { - match self.row() { 0 => None, _ => { - let id = self.row() as usize - 1; - self.scenes.get(id).map(|t|(id, t)) - } } - } - pub fn sequencer <'a> (&'a self) -> Option> { - Some(self.track()?.1.sequencer.state()) - } - pub fn chain <'a> (&'a self) -> Option> { - Some(self.track()?.1.chain.state()) - } - pub fn phrase_id (&self) -> Option { - let (track_id, _) = self.track()?; - let (_, scene) = self.scene()?; - *scene.clips.get(track_id)? - } -} -impl DynamicDevice { - pub fn connect (&self, midi_in: &str, audio_outs: &[&str]) -> Usually<&Self> { - { - let state = &self.state(); - let (client, _status) = Client::new( - &format!("{}-init", &state.name), ClientOptions::NO_START_SERVER - )?; - let midi_ins = client.ports(Some(midi_in), None, PortFlags::IS_OUTPUT); - let audio_outs: Vec> = audio_outs.iter() - .map(|pattern|client.ports(Some(pattern), None, PortFlags::IS_INPUT)) - .collect(); - for (i, sequencer) in state.tracks.iter().enumerate() { - for sequencer_midi_in in sequencer.midi_ins()?.iter() { - for midi_in in midi_ins.iter() { - client.connect_ports_by_name(&midi_in, &sequencer_midi_in)?; - } - } - let chain: &DynamicDevice = &state.tracks[i].chain; - for port in sequencer.midi_outs()?.iter() { - for midi_in in chain.midi_ins()?.iter() { - client.connect_ports_by_name(&port, &midi_in)?; - } - } - for (j, port) in chain.audio_outs()?.iter().enumerate() { - for audio_out in audio_outs[j % audio_outs.len()].iter() { - client.connect_ports_by_name(&port, &audio_out)?; - } - } - } - } - Ok(self) - } -} -impl PortList for Launcher {} -pub fn process (state: &mut Launcher, _: &Client, _: &ProcessScope) -> Control { - let transport = state.transport.query().unwrap(); - state.playing = transport.state; - state.current_frame = transport.pos.frame() as usize; - Control::Continue -} -pub fn render (state: &Launcher, buf: &mut Buffer, mut area: Rect) -> Usually { - //area.width = 80; // DOS mode - //area.height = 25; - let Rect { x, mut y, width, height } = area; - - y = y + crate::device::Transport { - timebase: &state.timebase, - playing: state.playing, - record: state.sequencer().map(|s|s.recording).unwrap_or(false), - overdub: state.sequencer().map(|s|s.overdub).unwrap_or(false), - monitor: state.sequencer().map(|s|s.monitoring).unwrap_or(false), - frame: state.current_frame - }.render(buf, area)?.height; - - y = y + crate::device::launcher::SceneGrid { - buf, - area: Rect { x, y, width, height }, - name: &state.name, - focused: state.view.is_tracks(), - scenes: &state.scenes, - tracks: &state.tracks, - cursor: &state.cursor - }.draw()?.height; - - y = y + draw_section_chains( - state, - buf, - Rect { x, y, width, height: height/3 } - )?.height; - - y = y + draw_section_sequencer( - state, - buf, - Rect { x, y, width, height: height - y } - )?.height; - - area.height = y; - if state.show_help { - let style = Some(Style::default().bold().white().not_dim().on_black().italic()); - let hide = "[Tab] Mode [Arrows] Move [.,] Value [F1] Toggle help "; - hide.blit(buf, x + (width - hide.len() as u16) / 2, height - 1, style); - } - - if let Some(ref modal) = state.modal { - for cell in buf.content.iter_mut() { - cell.fg = ::ratatui::style::Color::Gray; - cell.modifier = ::ratatui::style::Modifier::DIM; - } - } - Ok(area) -} -fn draw_section_sequencer (state: &Launcher, buf: &mut Buffer, area: Rect) -> Usually { - let Rect { x, y, width, height } = area; - let style = Some(Style::default().green().dim()); - let view = &state.view; - match view { - LauncherView::Sequencer => { - lozenge_left(buf, x, y, height, style); - lozenge_right(buf, x + width - 1, y, height, style); - }, - _ => {}, - }; - let track = state.track(); - if track.is_none() { - return Ok(area); - } - let track = track.unwrap().1; - let sequencer = track.sequencer.state(); - crate::device::sequencer::horizontal::draw( - buf, - area, - match state.phrase_id().map(|id|sequencer.phrases.get(id)) { - Some(Some(phrase)) => Some(phrase), - _ => None - }, - state.timebase.ppq() as usize, - sequencer.time_cursor, - sequencer.time_start, - sequencer.time_zoom, - sequencer.note_cursor, - sequencer.note_start, - Some(match view { - LauncherView::Sequencer => Style::default().green().not_dim(), - _ => Style::default().green().dim(), - }) - ) -} -fn draw_section_chains (state: &Launcher, buf: &mut Buffer, area: Rect) -> Usually { - let style = Some(Style::default().green().dim()); - if state.view.is_chains() { - let Rect { x, y, width, height} = area; - lozenge_left(buf, x, y, height, style); - lozenge_right(buf, x + width - 1, y, height, style); - } - let chain = state.chain(); - let _ = if let Some(chain) = &chain { - let (_, plugins) = crate::device::chain::draw_as_row( - &*chain, buf, area, style - )?; - plugins - } else { - vec![] - }; - Ok(area) -} -pub enum LauncherView { - Tracks, - Sequencer, - Chains -} -impl LauncherView { - fn is_chains (&self) -> bool { - match self { Self::Chains => true, _ => false } - } - fn is_tracks (&self) -> bool { - match self { Self::Tracks => true, _ => false } - } -} diff --git a/src/device/mixer.rs b/src/device/mixer.rs deleted file mode 100644 index 409d8871..00000000 --- a/src/device/mixer.rs +++ /dev/null @@ -1,249 +0,0 @@ -use crate::core::*; -use crate::layout::*; - -pub struct Mixer { - name: String, - tracks: Vec, - selected_track: usize, - selected_column: usize, -} - -impl Mixer { - pub fn new (name: &str) -> Result, Box> { - let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?; - Ok(DynamicDevice::new(render, handle, process, Self { - name: name.into(), - selected_column: 0, - selected_track: 1, - tracks: vec![ - Track::new(&client, 1, "Mono 1")?, - Track::new(&client, 1, "Mono 2")?, - Track::new(&client, 2, "Stereo 1")?, - Track::new(&client, 2, "Stereo 2")?, - Track::new(&client, 2, "Stereo 3")?, - Track::new(&client, 2, "Bus 1")?, - Track::new(&client, 2, "Bus 2")?, - Track::new(&client, 2, "Mix")?, - ], - })) - } -} - -pub fn process ( - _: &mut Mixer, - _: &Client, - _: &ProcessScope -) -> Control { - Control::Continue -} - -pub fn render (state: &Mixer, buf: &mut Buffer, mut area: Rect) - -> Usually -{ - if area.height < 2 { - return Ok(area) - } - area.x = area.width.saturating_sub(80) / 2; - area.width = area.width.min(80); - area.height = state.tracks.len() as u16 + 2; - draw_box(buf, area); - let x = area.x + 1; - let y = area.y + 1; - let _h = area.height - 2; - for (i, track) in state.tracks.iter().enumerate() { - //buf.set_string( - //x, y + index as u16, - //&track.name, Style::default().bold().not_dim() - //); - for (j, (column, field)) in [ - (0, format!(" {:10} ", track.name)), - (12, format!(" {:.1}dB ", track.gain)), - (22, format!(" [ ] ")), - (30, format!(" C ")), - (35, format!(" {:.1}dB ", track.level)), - (45, format!(" [ ] ")), - (51, format!(" {:7} ", track.route)), - ].into_iter().enumerate() { - buf.set_string( - x + column as u16, - y + i as u16, - field, - if state.selected_track == i && state.selected_column == j { - Style::default().white().bold().not_dim() - } else { - Style::default().not_dim() - } - ); - //stdout.queue(move_to(column, row))?; - //if state.selected_track == i && state.selected_column == j { - //stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?; - //} else { - //stdout.queue(PrintStyledContent(field.to_string().bold()))?; - //} - //fn render_meters ( - //state: &mut Mixer, - //stdout: &mut Stdout, - //offset: Rect - //) -> Result<(), Box> { - //let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); - //for (i, track) in state.tracks.iter().enumerate() { - //let row = (i + 1) as u16; - //stdout - //.queue(move_to(10, row))?.queue(PrintStyledContent("▁".green()))? - //.queue(move_to(20, row))?.queue(PrintStyledContent("▁".green()))? - //.queue(move_to(28, row))?.queue(PrintStyledContent("▁".green()))? - //.queue(move_to(43, row))?.queue(PrintStyledContent("▁".green()))?; - //} - //Ok(()) - //} - } - } - Ok(area) -} - -pub fn handle (state: &mut Mixer, event: &AppEvent) -> Usually { - if let AppEvent::Input(crossterm::event::Event::Key(event)) = event { - - match event.code { - //KeyCode::Char('c') => { - //if event.modifiers == KeyModifiers::CONTROL { - //state.exit(); - //} - //}, - KeyCode::Down => { - state.selected_track = (state.selected_track + 1) % state.tracks.len(); - println!("{}", state.selected_track); - return Ok(true) - }, - KeyCode::Up => { - if state.selected_track == 0 { - state.selected_track = state.tracks.len() - 1; - } else { - state.selected_track = state.selected_track - 1; - } - println!("{}", state.selected_track); - return Ok(true) - }, - KeyCode::Left => { - if state.selected_column == 0 { - state.selected_column = 6 - } else { - state.selected_column = state.selected_column - 1; - } - return Ok(true) - }, - KeyCode::Right => { - if state.selected_column == 6 { - state.selected_column = 0 - } else { - state.selected_column = state.selected_column + 1; - } - return Ok(true) - }, - _ => { - println!("\n{event:?}"); - } - } - - } - Ok(false) -} - -// TODO: -// - Meters: propagate clipping: -// - If one stage clips, all stages after it are marked red -// - If one track clips, all tracks that feed from it are marked red? - -pub const ACTIONS: [(&'static str, &'static str);2] = [ - ("+/-", "Adjust"), - ("Ins/Del", "Add/remove track"), -]; - -pub struct Track { - name: String, - channels: u8, - input_ports: Vec>, - pre_gain_meter: f64, - gain: f64, - insert_ports: Vec>, - return_ports: Vec>, - post_gain_meter: f64, - post_insert_meter: f64, - level: f64, - pan: f64, - output_ports: Vec>, - post_fader_meter: f64, - route: String, -} - -impl Track { - pub fn new (jack: &Client, channels: u8, name: &str) -> Result> { - let mut input_ports = vec![]; - let mut insert_ports = vec![]; - let mut return_ports = vec![]; - let mut output_ports = vec![]; - for channel in 1..=channels { - input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?); - output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?); - let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?; - let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?; - jack.connect_ports(&insert_port, &return_port)?; - insert_ports.push(insert_port); - return_ports.push(return_port); - } - Ok(Self { - name: name.into(), - channels, - input_ports, - pre_gain_meter: 0.0, - gain: 0.0, - post_gain_meter: 0.0, - insert_ports, - return_ports, - post_insert_meter: 0.0, - level: 0.0, - pan: 0.0, - post_fader_meter: 0.0, - route: "---".into(), - output_ports, - }) - } -} - -//impl Input, bool> for Mixer { - //fn handle (&mut self, engine: &mut TUI) -> Result> { - //Ok(None) - //} -// - -//impl Output, [u16;2]> for Mixer { - //fn render (&self, envine: &mut TUI) -> Result> { - - //let tracks_table = Columns::new() - //.add(titles) - //.add(input_meters) - //.add(gains) - //.add(gain_meters) - //.add(pres) - //.add(pre_meters) - //.add(levels) - //.add(pans) - //.add(pan_meters) - //.add(posts) - //.add(routes) - - //Rows::new() - //.add(Columns::new() - //.add(Rows::new() - //.add("[Arrows]".bold()) - //.add("Navigate")) - //.add(Rows::new() - //.add("[+/-]".bold()) - //.add("Adjust")) - //.add(Rows::new() - //.add("[Ins/Del]".bold()) - //.add("Add/remove track"))) - //.add(tracks_table) - //.render(engine) - //} -//} diff --git a/src/device/mod.rs b/src/device/mod.rs deleted file mode 100644 index b829d327..00000000 --- a/src/device/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod chain; pub use self::chain::{Chain, Plugin, Sampler, Sample, Voice}; -mod launcher; pub use self::launcher::{Launcher, Scene}; -mod looper; pub use self::looper::Looper; -mod mixer; pub use self::mixer::Mixer; -mod sequencer; pub use self::sequencer::{Sequencer, Phrase}; -mod track; pub use self::track::Track; -mod transport; pub use self::transport::Transport; diff --git a/src/device/sequencer/mod.rs b/src/device/sequencer/mod.rs deleted file mode 100644 index c97c9dc3..00000000 --- a/src/device/sequencer/mod.rs +++ /dev/null @@ -1,210 +0,0 @@ -use crate::core::*; -use crate::layout::*; - -mod keys; use self::keys::*; -mod handle; pub use self::handle::*; -mod phrase; pub use self::phrase::*; - -pub mod horizontal; -pub mod vertical; - -pub struct Sequencer { - pub name: String, - /// JACK transport handle. - pub transport: ::jack::Transport, - /// JACK MIDI input port that will be created. - pub midi_in: Port, - /// JACK MIDI output port that will be created. - pub midi_out: Port, - /// Holds info about tempo - pub timebase: Arc, - /// Phrase selector - pub sequence: Option, - /// Map: tick -> MIDI events at tick - pub phrases: Vec, - /// Red keys on piano roll. - pub notes_on: Vec, - /// Play sequence to output. - pub playing: TransportState, - /// Play input through output. - pub monitoring: bool, - /// Write input to sequence. - pub recording: bool, - /// Don't delete when recording. - pub overdub: bool, - /// Display mode - pub view: SequencerView, - /// Range of notes to display - pub note_start: usize, - /// Position of cursor within note range - pub note_cursor: usize, - /// PPM per display unit - pub time_zoom: usize, - /// Range of time steps to display - pub time_start: usize, - /// Position of cursor within time range - pub time_cursor: usize, -} - -#[derive(Debug, Clone)] -pub enum SequencerView { Tiny, Compact, Horizontal, Vertical, } - -impl Sequencer { - pub fn new ( - name: &str, - timebase: &Arc, - phrases: Option>, - ) -> Usually> { - let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?; - let transport = client.transport(); - DynamicDevice::new(render, handle, Self::process, Self { - name: name.into(), - timebase: timebase.clone(), - phrases: phrases.unwrap_or_else(||vec![Phrase::default()]), - sequence: Some(0), - - transport, - midi_in: client.register_port("in", MidiIn::default())?, - monitoring: true, - recording: true, - midi_out: client.register_port("out", MidiOut::default())?, - playing: TransportState::Starting, - overdub: true, - - view: SequencerView::Horizontal, - notes_on: vec![false;128], - note_start: 12, - note_cursor: 0, - time_zoom: 24, - time_start: 0, - time_cursor: 0, - }).activate(client) - } - - pub fn phrase <'a> (&'a self) -> Option<&'a Phrase> { - self.phrases.get(self.sequence?) - } - - pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { - if self.sequence.is_none() { return Control::Continue } - let phrase = self.phrases.get_mut(self.sequence.unwrap()); - if phrase.is_none() { return Control::Continue } - let phrase = phrase.unwrap(); - let frame = scope.last_frame_time() as usize; - let frames = scope.n_frames() as usize; - let mut output: Vec>>> = vec![None;frames]; - let transport = self.transport.query().unwrap(); - if transport.state != self.playing { - all_notes_off(&mut output); - } - self.playing = transport.state; - // Play from phrase into output buffer - if self.playing == TransportState::Rolling { - phrase.process_out( - &mut output, - &mut self.notes_on, - &self.timebase, - frame, - frames - ); - } - // Play from input to monitor, and record into phrase. - phrase.process_in( - self.midi_in.iter(scope), - &mut self.notes_on, - if self.monitoring { Some(&mut output) } else { None }, - self.recording && self.playing == TransportState::Rolling, - &self.timebase, - frame, - ); - write_output(&mut self.midi_out.writer(scope), &mut output, frames); - Control::Continue - } -} - -impl PortList for Sequencer { - fn midi_ins (&self) -> Usually> { Ok(vec![self.midi_in.name()?]) } - fn midi_outs (&self) -> Usually> { Ok(vec![self.midi_out.name()?]) } -} -fn render (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually { - let Rect { x, y, width, height } = area; - let header = draw_header(s, buf, area)?; - let piano = match s.view { - SequencerView::Tiny => Rect { x, y, width, height: 0 }, - SequencerView::Compact => Rect { x, y, width, height: 0 }, - SequencerView::Vertical => self::vertical::draw(s, buf, Rect { - x, y: y + header.height, width, height, - })?, - SequencerView::Horizontal => self::horizontal::draw( - buf, - Rect { x, y: y + header.height, width, height, }, - s.phrase(), - s.timebase.ppq() as usize, - s.time_cursor, - s.time_start, - s.time_zoom, - s.note_cursor, - s.note_start, - None - )?, - }; - Ok(draw_box(buf, Rect { - x, y, - width: header.width.max(piano.width), - height: header.height + piano.height - })) -} -pub fn draw_header (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually { - let Rect { x, y, width, .. } = area; - let style = Style::default().gray(); - crate::device::Transport { - timebase: &s.timebase, - playing: s.playing, - record: s.recording, - overdub: s.overdub, - monitor: s.monitoring, - frame: 0 - }.render(buf, area)?; - let separator = format!("├{}┤", "-".repeat((width - 2).into())); - separator.blit(buf, x, y + 2, Some(style.dim())); - let _ = draw_clips(s, buf, area)?; - Ok(Rect { x, y, width, height: 3 }) -} -pub fn draw_clips (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually { - let Rect { x, y, .. } = area; - let style = Style::default().gray(); - for (i, sequence) in s.phrases.iter().enumerate() { - let label = format!("▶ {}", &sequence.name); - label.blit(buf, x + 2, y + 3 + (i as u16)*2, Some(if Some(i) == s.sequence { - match s.playing { - TransportState::Rolling => style.white().bold(), - _ => style.not_dim().bold() - } - } else { - style.dim() - })); - } - Ok(Rect { x, y, width: 14, height: 14 }) -} -/// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut MIDIChunk) { - output[0] = Some(vec![]); - if let Some(Some(frame)) = output.get_mut(0) { - let mut buf = vec![]; - let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; - let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; - evt.write(&mut buf).unwrap(); - frame.push(buf); - } -} -/// Write to JACK port from output buffer (containing notes from sequence and/or monitor) -fn write_output (writer: &mut ::jack::MidiWriter, output: &mut MIDIChunk, frames: usize) { - for time in 0..frames { - if let Some(Some(frame)) = output.get_mut(time ) { - for event in frame.iter() { - writer.write(&::jack::RawMidi { time: time as u32, bytes: &event }) - .expect(&format!("{event:?}")); - } - } - } -} diff --git a/src/main.rs b/src/main.rs index 2ba4316d..ffe01959 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,256 +6,321 @@ extern crate clap; extern crate jack; extern crate crossterm; -use clap::{Parser}; -use std::error::Error; - -pub mod core; pub mod cli; pub mod config; -pub mod device; -pub mod layout; +pub mod control; +pub mod core; +pub mod model; +pub mod view; use crate::core::*; -use crate::device::*; +use crate::model::*; +use crate::view::*; -mod new { - use crate::core::*; - use crate::layout::Stack; - type Phrase = (String, usize, BTreeMap>); - type Scene = (String, Option, Vec>); - type Track = (String, usize); - #[derive(Default)] - pub struct App { - client: Option, - phrases: BTreeMap, - scenes: BTreeMap, - tracks: BTreeMap, - frame: usize, - scene: Vec, - timebase: Arc, - } - struct SceneGrid {} - impl Render for SceneGrid {} - struct Chains {} - impl Render for Chains {} - struct Sequencer {} - impl Render for Sequencer {} - impl Render for App { - fn render (&self, buf: &mut Buffer, area: Rect) -> Usually { - crate::device::Transport { - timebase: &self.timebase, - playing: TransportState::Stopped, - record: false, - overdub: false, - monitor: false, - frame: 0, - }.render(buf, area)?; - SceneGrid { - }.render(buf, area)?; - Chains {}.render(buf, area)?; - Sequencer {}.render(buf, area)?; - Ok(area) +pub fn main () -> Usually<()> { + App::default().run(Some(|app: Arc>|{ + let mut state = app.lock().unwrap(); + let xdg = Arc::new(microxdg::XdgApp::new("tek")?); + state.xdg = Some(xdg.clone()); + if crate::config::AppPaths::new(&xdg)?.should_create() { + state.modal = Some(Box::new(crate::config::SetupModal(Some(xdg.clone())))); } - } - impl Handle for App { - fn handle (&mut self, _e: &AppEvent) -> Usually { - Ok(true) - } - } - impl Process for App { - fn process (&mut self, _c: &Client, _s: &ProcessScope) -> Control { - Control::Continue - } - } - pub fn main () -> Usually<()> { - App::default().run(Some(|app: Arc>|{ - app.lock().unwrap().client = Some(jack_run("tek", &app)?); - Ok(()) - })) - } -} - -macro_rules! sample { - ($note:expr, $name:expr, $src:expr) => { - { - let mut channels: Vec> = vec![]; - for channel in wavers::Wav::from_path($src)?.channels() { - channels.push(channel); - } - let mut end = 0; - let mut data: Vec> = vec![]; - for samples in channels.iter() { - let channel = Vec::from(samples.as_ref()); - end = end.max(channel.len()); - data.push(channel); - } - (u7::from_int_lossy($note).into(), Sample::new($name, 0, end, data).into()) - } - }; -} - -fn main () -> Usually<()> { - let _cli = cli::Cli::parse(); - let xdg = microxdg::XdgApp::new("tek")?; - crate::config::create_dirs(&xdg)?; - //run(Sampler::new("Sampler#000")?) - let (client, _) = Client::new("init", ClientOptions::NO_START_SERVER)?; - let timebase = Arc::new(Timebase::new(client.sample_rate() as f64, 125.0, 96.0)); - let ppq = timebase.ppq() as usize; - macro_rules! play { - ($t1:expr => [ $($msg:expr),* $(,)? ]) => { - ( $t1 * ppq / 4, vec![ $($msg),* ] ) - } - } - macro_rules! phrase { - ($($t:expr => $msg:expr),* $(,)?) => {{ - let mut phrase = BTreeMap::new(); - $(phrase.insert($t, vec![]);)* - $(phrase.get_mut(&$t).unwrap().push($msg);)* - phrase - }} - } - Launcher::new("Launcher#0", &timebase, - Some(vec![ - - Track::new("Drums", &timebase, Some(vec![ - + state.scenes = vec![ + Scene::new("Intro", vec![None, None, None, None]), + ]; + state.phrases = vec![ + Phrase::new("4 kicks", state.timebase.ppq() as usize * 4, None), + ]; + state.tracks = vec![ + Track::new("Drums", &state.timebase, Some(vec![ Sampler::new("Sampler", Some(BTreeMap::from([ sample!(36, "Kick", "/home/user/Lab/Music/pak/kik.wav"), sample!(40, "Snare", "/home/user/Lab/Music/pak/sna.wav"), sample!(44, "Hihat", "/home/user/Lab/Music/pak/chh.wav"), ])))?.boxed(), - - //Plugin::lv2("Panagement", "file:///home/user/.lv2/Auburn Sounds Panagement 2.lv2")?.boxed(), - - ]), Some(vec![ - - Phrase::new("KSH", ppq * 4, Some(phrase! { - 00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 00 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - 01 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - 02 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - 04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - 04 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - 06 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - 08 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - 10 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 10 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - 11 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - 12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - 12 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, - 14 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - 14 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() } - })), - - Phrase::new("4K", ppq * 4, Some(phrase! { - 00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 04 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 08 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 12 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - })), - - Phrase::new("KS", ppq * 4, Some(phrase! { - 00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - 10 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - 12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, - })), - - ]))?, - - Track::new("Odin2", &timebase, Some(vec![ - - Plugin::lv2("Odin2", "file:///home/user/.lv2/Odin2.lv2")?.boxed(), - - ]), Some(vec![ - Phrase::new("E G A Bb", ppq * 4, Some(BTreeMap::from([ - play!(2 => [ - MidiMessage::NoteOff { key: 42.into(), vel: 100.into() }, - MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - ]), - play!(6 => [ - MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, - MidiMessage::NoteOn { key: 39.into(), vel: 100.into() }, - ]), - play!(10 => [ - MidiMessage::NoteOff { key: 39.into(), vel: 100.into() }, - MidiMessage::NoteOn { key: 41.into(), vel: 100.into() }, - ]), - play!(14 => [ - MidiMessage::NoteOff { key: 41.into(), vel: 100.into() }, - MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, - ]), - ]))), - Phrase::new("E E G Bb", ppq * 4, Some(BTreeMap::from([ - play!(2 => [ - MidiMessage::NoteOff { key: 42.into(), vel: 100.into() }, - MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - ]), - play!(6 => [ - MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, - MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - ]), - play!(10 => [ - MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, - MidiMessage::NoteOn { key: 39.into(), vel: 100.into() }, - ]), - play!(14 => [ - MidiMessage::NoteOff { key: 39.into(), vel: 100.into() }, - MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, - ]), - ]))), - Phrase::new("E E E E", ppq * 4, Some(BTreeMap::from([ - play!(0 => [ - MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, - ]), - play!(2 => [ - MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - ]), - play!(4 => [ - MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, - ]), - play!(6 => [ - MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - ]), - play!(8 => [ - MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, - ]), - play!(10 => [ - MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - ]), - play!(12 => [ - MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, - ]), - play!(14 => [ - MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, - ]), - ]))) - ]))?, - - //Plugin::lv2("Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(), - //Plugin::lv2("Kick/ChowKick", "file:///home/user/.lv2/ChowKick.lv2", &[1, 1, 0, 2])?.boxed(), - //Plugin::lv2("Bass/Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(), - //Plugin::lv2("Pads/Odin2", "file:///home/user/.lv2/Odin2.lv2", &[1, 0, 0, 2])?.boxed(), - ]), - Some(vec![ - Scene::new(&"Scene 1", &[Some(0), None, None, None]), - Scene::new(&"Scene 2", &[Some(0), Some(0), None, None]), - Scene::new(&"Scene 3", &[Some(1), Some(1), None, None]), - Scene::new(&"Scene 4", &[Some(2), Some(2), None, None]), - //Scene::new(&"Scene#03", &[None, Some(0), None, None]), - //Scene::new(&"Scene#04", &[None, None, None, None]), - //Scene::new(&"Scene#05", &[None, None, None, None]), - ]) - - )? - .run(Some(init)) + ]), None)?, + ]; + state.jack = Some(jack_run("tek", &app)?); + Ok(()) + })) } -fn init (state: Arc>>) -> Usually<()> { - let input = ".*nanoKEY.*"; - let output = ["Komplete.*:playback_FL", "Komplete.*:playback_FR"]; - let state = state.lock().unwrap(); - state.connect(input, &output)?; - Ok(()) +#[derive(Default)] +pub struct App { + pub xdg: Option>, + pub jack: Option, + pub phrases: Vec, + pub scenes: Vec, + pub tracks: Vec, + pub frame: usize, + pub scene: Vec, + pub timebase: Arc, + pub modal: Option>, } + +process!(App); + +render!(App |self, buf, area| { + let Rect { x, mut y, width, height } = area; + + y = y + TransportView { + timebase: &self.timebase, + playing: TransportState::Stopped, + record: false, + overdub: false, + monitor: false, + frame: 0, + }.render(buf, area)?.height; + + y = y + SceneGridView { + buf, + area: Rect { x, y, width, height: height / 3 }, + name: "", + focused: true, + scenes: &self.scenes, + tracks: &self.tracks, + cursor: &(0, 0) + }.draw()?.height; + + y = y + ChainView { + focused: true, + chain: None + }.render(buf, Rect { x, y, width, height: height / 3 })?.height; + + y = y + SequencerView { + focused: true, + ppq: self.timebase.ppq() as usize, + track: None, + phrase: None, + }.render(buf, Rect { x, y, width, height })?.height; + + if let Some(ref modal) = self.modal { + for cell in buf.content.iter_mut() { + cell.fg = ratatui::style::Color::Gray; + cell.modifier = ratatui::style::Modifier::DIM; + } + modal.render(buf, area)?; + } + + Ok(area) +}); + +handle!(App |self, e| { + if let Some(ref mut modal) = self.modal { + if modal.handle(e)? { + self.modal = None; + return Ok(true) + }; + } + handle_keymap(self, e, keymap!(App { + [F(1), NONE, "toggle_help", "toggle help", toggle_help], + + [Up, NONE, "cursor_up", "move cursor up", cursor_up], + [Down, NONE, "cursor_down", "move cursor down", cursor_down], + [Left, NONE, "cursor_left", "move cursor left", cursor_left], + [Right, NONE, "cursor_right", "move cursor right", cursor_right], + [Char('.'), NONE, "increment", "increment value at cursor", increment], + [Char(','), NONE, "decrement", "decrement value at cursor", decrement], + [Delete, CONTROL, "delete", "delete track", delete], + [Char('d'), CONTROL, "duplicate", "duplicate scene or track", duplicate], + [Enter, NONE, "activate", "activate item at cursor", activate], + + [Tab, NONE, "focus_next", "focus next area", focus_next], + [Tab, SHIFT, "focus_prev", "focus previous area", focus_prev], + [Char(' '), NONE, "play_toggle", "play or pause", play_toggle], + [Char('r'), NONE, "record_toggle", "toggle recording", record_toggle], + [Char('d'), NONE, "overdub_toggle", "toggle overdub", overdub_toggle], + [Char('m'), NONE, "monitor_toggle", "toggle input monitoring", monitor_toggle], + [Char('r'), CONTROL, "rename", "rename current element", rename], + [Char('t'), CONTROL, "add_track", "add a new track", add_track], + //[Char(' '), SHIFT, "play_start", "play from start", play_start], + })) +}); + +fn increment (_: &mut App) -> Usually { Ok(true) } +fn decrement (_: &mut App) -> Usually { Ok(true) } +fn delete (_: &mut App) -> Usually { Ok(true) } +fn duplicate (_: &mut App) -> Usually { Ok(true) } +fn activate (_: &mut App) -> Usually { Ok(true) } +fn rename (_: &mut App) -> Usually { Ok(true) } +fn add_track (_: &mut App) -> Usually { Ok(true) } +fn delete_track (_: &mut App) -> Usually { Ok(true) } +fn cursor_up (_: &mut App) -> Usually { Ok(true) } +fn cursor_down (_: &mut App) -> Usually { Ok(true) } +fn cursor_left (_: &mut App) -> Usually { Ok(true) } +fn cursor_right (_: &mut App) -> Usually { Ok(true) } +fn toggle_help (_: &mut App) -> Usually { Ok(true) } +fn focus_next (_: &mut App) -> Usually { Ok(true) } +fn focus_prev (_: &mut App) -> Usually { Ok(true) } +fn clip_next (_: &mut App) -> Usually { Ok(true) } +fn clip_prev (_: &mut App) -> Usually { Ok(true) } +fn play_toggle (_: &mut App) -> Usually { Ok(true) } +fn record_toggle (_: &mut App) -> Usually { Ok(true) } +fn overdub_toggle (_: &mut App) -> Usually { Ok(true) } +fn monitor_toggle (_: &mut App) -> Usually { Ok(true) } + +//fn main () -> Usually<()> { + //let _cli = cli::Cli::parse(); + //let xdg = microxdg::XdgApp::new("tek")?; + //crate::config::create_dirs(&xdg)?; + ////run(Sampler::new("Sampler#000")?) + //let (client, _) = Client::new("init", ClientOptions::NO_START_SERVER)?; + //let timebase = Arc::new(Timebase::new(client.sample_rate() as f64, 125.0, 96.0)); + //let ppq = timebase.ppq() as usize; + //macro_rules! play { + //($t1:expr => [ $($msg:expr),* $(,)? ]) => { + //( $t1 * ppq / 4, vec![ $($msg),* ] ) + //} + //} + //macro_rules! phrase { + //($($t:expr => $msg:expr),* $(,)?) => {{ + //let mut phrase = BTreeMap::new(); + //$(phrase.insert($t, vec![]);)* + //$(phrase.get_mut(&$t).unwrap().push($msg);)* + //phrase + //}} + //} + //Launcher::new("Launcher#0", &timebase, + //Some(vec![ + + //Track::new("Drums", &timebase, Some(vec![ + + //Sampler::new("Sampler", Some(BTreeMap::from([ + //sample!(36, "Kick", "/home/user/Lab/Music/pak/kik.wav"), + //sample!(40, "Snare", "/home/user/Lab/Music/pak/sna.wav"), + //sample!(44, "Hihat", "/home/user/Lab/Music/pak/chh.wav"), + //])))?.boxed(), + + ////Plugin::lv2("Panagement", "file:///home/user/.lv2/Auburn Sounds Panagement 2.lv2")?.boxed(), + + //]), Some(vec![ + + //Phrase::new("KSH", ppq * 4, Some(phrase! { + //00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //00 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, + //01 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, + //02 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, + //04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + //04 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, + //06 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, + //08 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, + //10 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //10 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, + //11 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, + //12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + //12 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() }, + //14 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + //14 * ppq/4 => MidiMessage::NoteOn { key: 44.into(), vel: 100.into() } + //})), + + //Phrase::new("4K", ppq * 4, Some(phrase! { + //00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //04 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //08 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //12 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //})), + + //Phrase::new("KS", ppq * 4, Some(phrase! { + //00 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //04 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + //10 * ppq/4 => MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //12 * ppq/4 => MidiMessage::NoteOn { key: 40.into(), vel: 100.into() }, + //})), + + //]))?, + + //Track::new("Odin2", &timebase, Some(vec![ + + //Plugin::lv2("Odin2", "file:///home/user/.lv2/Odin2.lv2")?.boxed(), + + //]), Some(vec![ + //Phrase::new("E G A Bb", ppq * 4, Some(BTreeMap::from([ + //play!(2 => [ + //MidiMessage::NoteOff { key: 42.into(), vel: 100.into() }, + //MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //]), + //play!(6 => [ + //MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, + //MidiMessage::NoteOn { key: 39.into(), vel: 100.into() }, + //]), + //play!(10 => [ + //MidiMessage::NoteOff { key: 39.into(), vel: 100.into() }, + //MidiMessage::NoteOn { key: 41.into(), vel: 100.into() }, + //]), + //play!(14 => [ + //MidiMessage::NoteOff { key: 41.into(), vel: 100.into() }, + //MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, + //]), + //]))), + //Phrase::new("E E G Bb", ppq * 4, Some(BTreeMap::from([ + //play!(2 => [ + //MidiMessage::NoteOff { key: 42.into(), vel: 100.into() }, + //MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //]), + //play!(6 => [ + //MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, + //MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //]), + //play!(10 => [ + //MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, + //MidiMessage::NoteOn { key: 39.into(), vel: 100.into() }, + //]), + //play!(14 => [ + //MidiMessage::NoteOff { key: 39.into(), vel: 100.into() }, + //MidiMessage::NoteOn { key: 42.into(), vel: 100.into() }, + //]), + //]))), + //Phrase::new("E E E E", ppq * 4, Some(BTreeMap::from([ + //play!(0 => [ + //MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, + //]), + //play!(2 => [ + //MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //]), + //play!(4 => [ + //MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, + //]), + //play!(6 => [ + //MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //]), + //play!(8 => [ + //MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, + //]), + //play!(10 => [ + //MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //]), + //play!(12 => [ + //MidiMessage::NoteOff { key: 36.into(), vel: 100.into() }, + //]), + //play!(14 => [ + //MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }, + //]), + //]))) + //]))?, + + ////Plugin::lv2("Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(), + ////Plugin::lv2("Kick/ChowKick", "file:///home/user/.lv2/ChowKick.lv2", &[1, 1, 0, 2])?.boxed(), + ////Plugin::lv2("Bass/Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(), + ////Plugin::lv2("Pads/Odin2", "file:///home/user/.lv2/Odin2.lv2", &[1, 0, 0, 2])?.boxed(), + //]), + //Some(vec![ + //Scene::new(&"Scene 1", &[Some(0), None, None, None]), + //Scene::new(&"Scene 2", &[Some(0), Some(0), None, None]), + //Scene::new(&"Scene 3", &[Some(1), Some(1), None, None]), + //Scene::new(&"Scene 4", &[Some(2), Some(2), None, None]), + ////Scene::new(&"Scene#03", &[None, Some(0), None, None]), + ////Scene::new(&"Scene#04", &[None, None, None, None]), + ////Scene::new(&"Scene#05", &[None, None, None, None]), + //]) + + //)? + //.run(Some(init)) +//} + +//fn init (state: Arc>>) -> Usually<()> { + //let input = ".*nanoKEY.*"; + //let output = ["Komplete.*:playback_FL", "Komplete.*:playback_FR"]; + //let state = state.lock().unwrap(); + //state.connect(input, &output)?; + //Ok(()) +//} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 00000000..75da9382 --- /dev/null +++ b/src/model.rs @@ -0,0 +1,20 @@ +pub mod chain; +pub mod launcher; +pub mod looper; +pub mod mixer; +pub mod phrase; +pub mod plugin; +pub mod sampler; +pub mod scene; +pub mod sequencer; +pub mod track; + +pub use self::phrase::Phrase; +pub use self::scene::Scene; +pub use self::track::Track; +pub use self::sequencer::{Sequencer, SequencerMode}; +pub use self::launcher::{Launcher, LauncherMode}; +pub use self::chain::Chain; +pub use self::sampler::{Sampler, Sample}; +pub use self::mixer::Mixer; +pub use self::plugin::{Plugin, PluginKind, lv2::LV2Plugin}; diff --git a/src/model/chain.rs b/src/model/chain.rs new file mode 100644 index 00000000..ac091b14 --- /dev/null +++ b/src/model/chain.rs @@ -0,0 +1,46 @@ +use crate::{core::*, view::*}; + +pub struct Chain { + pub name: String, + pub focused: bool, + pub focus: usize, + pub items: Vec>, + pub view: ChainViewMode, + pub adding: bool, +} +render!(Chain = crate::view::chain::render); +handle!(Chain = crate::control::chain::handle); +process!(Chain); +impl Chain { + pub fn new (name: &str, items: Option>>) -> Usually { + Ok(Self { + name: name.into(), + focused: false, + focus: 0, + items: items.unwrap_or_else(||vec![]), + view: ChainViewMode::Column, + adding: false + }) + } +} + +impl PortList for Chain { + fn midi_ins (&self) -> Usually> { + if let Some(device) = self.items.get(0) { + device.midi_ins() + } else { + Ok(vec![]) + } + } + fn audio_outs (&self) -> Usually> { + if let Some(device) = self.items.get(self.items.len().saturating_sub(1)) { + device.audio_outs() + } else { + Ok(vec![]) + } + } +} + +pub fn process (_: &mut Chain, _: &Client, _: &ProcessScope) -> Control { + Control::Continue +} diff --git a/src/model/launcher.rs b/src/model/launcher.rs new file mode 100644 index 00000000..3e358b5d --- /dev/null +++ b/src/model/launcher.rs @@ -0,0 +1,184 @@ +use crate::{core::*, model::*}; +pub enum LauncherMode { + Tracks, + Sequencer, + Chains +} +impl LauncherMode { + pub fn is_chains (&self) -> bool { + match self { Self::Chains => true, _ => false } + } + pub fn is_tracks (&self) -> bool { + match self { Self::Tracks => true, _ => false } + } + pub fn is_sequencer (&self) -> bool { + match self { Self::Sequencer => true, _ => false } + } +} +pub struct Launcher { + pub name: String, + pub timebase: Arc, + pub transport: ::jack::Transport, + pub playing: TransportState, + pub monitoring: bool, + pub recording: bool, + pub overdub: bool, + pub current_frame: usize, + pub cursor: (usize, usize), + pub tracks: Vec, + pub scenes: Vec, + pub show_help: bool, + pub view: LauncherMode, +} +render!(Launcher = crate::view::launcher::render); +handle!(Launcher = crate::control::launcher::handle); +process!(Launcher |self, _client, _scope| { + let transport = self.transport.query().unwrap(); + self.playing = transport.state; + self.current_frame = transport.pos.frame() as usize; + Control::Continue +}); +impl PortList for Launcher {} +impl Launcher { + pub fn new ( + name: &str, + timebase: &Arc, + tracks: Option>, + scenes: Option> + ) -> Result> { + let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; + let transport = client.transport(); + let ppq = timebase.ppq() as usize; + Ok(Self { + name: name.into(), + view: LauncherMode::Chains, + playing: transport.query_state()?, + transport, + timebase: timebase.clone(), + monitoring: true, + recording: false, + overdub: true, + cursor: (2, 2), + current_frame: 0, + scenes: scenes.unwrap_or_else(||vec![Scene::new(&"Scene 1", &[None])]), + tracks: if let Some(tracks) = tracks { tracks } else { vec![ + Track::new("Track 1", &timebase, None, Some(vec![ + Phrase::new("MIDI Clip 1", ppq * 4, Some(BTreeMap::from([ + ( ppq * 0, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ), + ( ppq * 1, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ), + ( ppq * 2, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ), + ( ppq * 3, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ), + ]))) + ]))?, + ] }, + show_help: true, + }) + } + pub fn connect (&self, midi_in: &str, audio_outs: &[&str]) -> Usually<()> { + let (client, _status) = Client::new( + &format!("{}-init", &self.name), ClientOptions::NO_START_SERVER + )?; + let midi_ins = client.ports(Some(midi_in), None, PortFlags::IS_OUTPUT); + let audio_outs: Vec> = audio_outs.iter() + .map(|pattern|client.ports(Some(pattern), None, PortFlags::IS_INPUT)) + .collect(); + for (i, sequencer) in self.tracks.iter().enumerate() { + for sequencer_midi_in in sequencer.midi_ins()?.iter() { + for midi_in in midi_ins.iter() { + client.connect_ports_by_name(&midi_in, &sequencer_midi_in)?; + } + } + let chain: &Chain = &self.tracks[i].chain; + for port in sequencer.midi_outs()?.iter() { + for midi_in in chain.midi_ins()?.iter() { + client.connect_ports_by_name(&port, &midi_in)?; + } + } + for (j, port) in chain.audio_outs()?.iter().enumerate() { + for audio_out in audio_outs[j % audio_outs.len()].iter() { + client.connect_ports_by_name(&port, &audio_out)?; + } + } + } + Ok(()) + } + pub fn cols (&self) -> usize { + (self.tracks.len() + 2) as usize + } + pub fn col (&self) -> usize { + self.cursor.0 as usize + } + pub fn dec_col (&mut self) { + self.cursor.0 = if self.cursor.0 > 0 { + self.cursor.0 - 1 + } else { + (self.cols() - 1) as usize + } + } + pub fn inc_col (&mut self) { + self.cursor.0 = if self.cursor.0 >= self.cols() - 1 { + 0 + } else { + self.cursor.0 + 1 + } + } + pub fn rows (&self) -> usize { + (self.scenes.len() + 2) as usize + } + pub fn row (&self) -> usize { + self.cursor.1 as usize + } + pub fn dec_row (&mut self) { + self.cursor.1 = if self.cursor.1 > 0 { + self.cursor.1 - 1 + } else { + self.rows() - 1 + } + } + pub fn inc_row (&mut self) { + self.cursor.1 = if self.cursor.1 >= self.rows() - 1 { + 0 + } else { + self.cursor.1 + 1 + } + } + + pub fn track (&self) -> Option<(usize, &Track)> { + match self.col() { 0 => None, _ => { + let id = self.col() as usize - 1; + self.tracks.get(id).map(|t|(id, t)) + } } + } + pub fn track_mut (&mut self) -> Option<(usize, &mut Track)> { + match self.col() { 0 => None, _ => { + let id = self.col() as usize - 1; + self.tracks.get_mut(id).map(|t|(id, t)) + } } + } + pub fn scene (&self) -> Option<(usize, &Scene)> { + match self.row() { 0 => None, _ => { + let id = self.row() as usize - 1; + self.scenes.get(id).map(|t|(id, t)) + } } + } + pub fn scene_mut (&mut self) -> Option<(usize, &mut Scene)> { + match self.row() { 0 => None, _ => { + let id = self.row() as usize - 1; + self.scenes.get_mut(id).map(|t|(id, t)) + } } + } + pub fn sequencer (&self) -> Option<&Sequencer> { + Some(&self.track()?.1.sequencer) + } + pub fn sequencer_mut (&mut self) -> Option<&mut Sequencer> { + Some(&mut self.track_mut()?.1.sequencer) + } + pub fn chain (&self) -> Option<&Chain> { + Some(&self.track()?.1.chain) + } + pub fn phrase_id (&self) -> Option { + let (track_id, _) = self.track()?; + let (_, scene) = self.scene()?; + *scene.clips.get(track_id)? + } +} diff --git a/src/device/looper.rs b/src/model/looper.rs similarity index 100% rename from src/device/looper.rs rename to src/model/looper.rs diff --git a/src/model/mixer.rs b/src/model/mixer.rs new file mode 100644 index 00000000..2a5571e1 --- /dev/null +++ b/src/model/mixer.rs @@ -0,0 +1,132 @@ +use crate::core::*; + +pub struct Mixer { + pub name: String, + pub tracks: Vec, + pub selected_track: usize, + pub selected_column: usize, +} + +impl Mixer { + pub fn new (name: &str) -> Result, Box> { + let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?; + Ok(DynamicDevice::new( + crate::view::mixer::render, + crate::control::mixer::handle, + process, + Self { + name: name.into(), + selected_column: 0, + selected_track: 1, + tracks: vec![ + MixerTrack::new(&client, 1, "Mono 1")?, + MixerTrack::new(&client, 1, "Mono 2")?, + MixerTrack::new(&client, 2, "Stereo 1")?, + MixerTrack::new(&client, 2, "Stereo 2")?, + MixerTrack::new(&client, 2, "Stereo 3")?, + MixerTrack::new(&client, 2, "Bus 1")?, + MixerTrack::new(&client, 2, "Bus 2")?, + MixerTrack::new(&client, 2, "Mix")?, + ], + } + )) + } +} + +pub fn process ( + _: &mut Mixer, + _: &Client, + _: &ProcessScope +) -> Control { + Control::Continue +} + +pub struct MixerTrack { + pub name: String, + pub channels: u8, + pub input_ports: Vec>, + pub pre_gain_meter: f64, + pub gain: f64, + pub insert_ports: Vec>, + pub return_ports: Vec>, + pub post_gain_meter: f64, + pub post_insert_meter: f64, + pub level: f64, + pub pan: f64, + pub output_ports: Vec>, + pub post_fader_meter: f64, + pub route: String, +} + +impl MixerTrack { + pub fn new (jack: &Client, channels: u8, name: &str) -> Result> { + let mut input_ports = vec![]; + let mut insert_ports = vec![]; + let mut return_ports = vec![]; + let mut output_ports = vec![]; + for channel in 1..=channels { + input_ports.push(jack.register_port(&format!("{name} [input {channel}]"), AudioIn::default())?); + output_ports.push(jack.register_port(&format!("{name} [out {channel}]"), AudioOut::default())?); + let insert_port = jack.register_port(&format!("{name} [pre {channel}]"), AudioOut::default())?; + let return_port = jack.register_port(&format!("{name} [insert {channel}]"), AudioIn::default())?; + jack.connect_ports(&insert_port, &return_port)?; + insert_ports.push(insert_port); + return_ports.push(return_port); + } + Ok(Self { + name: name.into(), + channels, + input_ports, + pre_gain_meter: 0.0, + gain: 0.0, + post_gain_meter: 0.0, + insert_ports, + return_ports, + post_insert_meter: 0.0, + level: 0.0, + pan: 0.0, + post_fader_meter: 0.0, + route: "---".into(), + output_ports, + }) + } +} + +//impl Input, bool> for Mixer { + //fn handle (&mut self, engine: &mut TUI) -> Result> { + //Ok(None) + //} +// + +//impl Output, [u16;2]> for Mixer { + //fn render (&self, envine: &mut TUI) -> Result> { + + //let tracks_table = Columns::new() + //.add(titles) + //.add(input_meters) + //.add(gains) + //.add(gain_meters) + //.add(pres) + //.add(pre_meters) + //.add(levels) + //.add(pans) + //.add(pan_meters) + //.add(posts) + //.add(routes) + + //Rows::new() + //.add(Columns::new() + //.add(Rows::new() + //.add("[Arrows]".bold()) + //.add("Navigate")) + //.add(Rows::new() + //.add("[+/-]".bold()) + //.add("Adjust")) + //.add(Rows::new() + //.add("[Ins/Del]".bold()) + //.add("Add/remove track"))) + //.add(tracks_table) + //.render(engine) + //} +//} + diff --git a/src/device/sequencer/phrase.rs b/src/model/phrase.rs similarity index 97% rename from src/device/sequencer/phrase.rs rename to src/model/phrase.rs index a6949552..58ddae72 100644 --- a/src/device/sequencer/phrase.rs +++ b/src/model/phrase.rs @@ -1,17 +1,17 @@ use crate::core::*; -pub type PhraseData = BTreeMap>; - -pub type MIDIMessage = Vec; - -pub type MIDIChunk = [Option>]; - pub struct Phrase { pub name: String, pub length: usize, pub notes: PhraseData, } +impl Default for Phrase { + fn default () -> Self { + Self::new("", 0, None) + } +} + impl Phrase { pub fn new (name: &str, length: usize, notes: Option) -> Self { Self { name: name.to_string(), length, notes: notes.unwrap_or(BTreeMap::new()) } @@ -127,9 +127,3 @@ impl Phrase { } } - -impl Default for Phrase { - fn default () -> Self { - Self::new("", 0, None) - } -} diff --git a/src/model/plugin.rs b/src/model/plugin.rs new file mode 100644 index 00000000..305988cb --- /dev/null +++ b/src/model/plugin.rs @@ -0,0 +1,176 @@ +use crate::core::*; + +pub mod lv2; +pub mod vst2; +pub mod vst3; + +use self::lv2::*; + +pub struct Plugin { + pub name: String, + pub path: Option, + pub plugin: Option, + pub selected: usize, + pub mapping: bool, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, +} + +pub enum PluginKind { + LV2(LV2Plugin), + VST2 { + instance: ::vst::host::PluginInstance + }, + VST3, +} + +const HELM: &'static str = "file:///nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lv2/helm.lv2"; + +impl Plugin { + /// Load a LV2 plugin. + pub fn lv2 (name: &str, path: &str) -> Usually> { + let host = Self::new(name)?; + let plugin = LV2Plugin::new(path)?; + let mut state = host.state(); + let client = host.client.as_ref().unwrap().as_client(); + let (midi_ins, midi_outs, audio_ins, audio_outs) = ( + plugin.plugin.port_counts().atom_sequence_inputs, + plugin.plugin.port_counts().atom_sequence_outputs, + plugin.plugin.port_counts().audio_inputs, + plugin.plugin.port_counts().audio_outputs, + ); + state.midi_ins = { + let mut ports = vec![]; + for i in 0..midi_ins { + ports.push(client.register_port(&format!("midi-in-{i}"), MidiIn::default())?) + } + ports + }; + state.midi_outs = { + let mut ports = vec![]; + for i in 0..midi_outs { + ports.push(client.register_port(&format!("midi-out-{i}"), MidiOut::default())?) + } + ports + }; + state.audio_ins = { + let mut ports = vec![]; + for i in 0..audio_ins { + ports.push(client.register_port(&format!("audio-in-{i}"), AudioIn::default())?) + } + ports + }; + state.audio_outs = { + let mut ports = vec![]; + for i in 0..audio_outs { + ports.push(client.register_port(&format!("audio-out-{i}"), AudioOut::default())?) + } + ports + }; + state.plugin = Some(PluginKind::LV2(plugin)); + state.path = Some(String::from(path)); + std::mem::drop(state); + Ok(host) + } + pub fn new (name: &str) -> Usually> { + DynamicDevice::new( + crate::view::plugin::render, + crate::control::plugin::handle, + Self::process, + Self { + name: name.into(), + path: None, + plugin: None, + selected: 0, + mapping: false, + midi_ins: vec![], + midi_outs: vec![], + audio_ins: vec![], + audio_outs: vec![], + } + ).activate(Client::new(name, ClientOptions::NO_START_SERVER)?.0) + } + pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + match self.plugin.as_mut() { + Some(PluginKind::LV2(LV2Plugin { features, ref mut instance, .. })) => { + let urid = features.midi_urid(); + let mut inputs = vec![]; + for port in self.midi_ins.iter() { + let mut atom = ::livi::event::LV2AtomSequence::new( + &features, + scope.n_frames() as usize + ); + for event in port.iter(scope) { + match event.bytes.len() { + 3 => atom.push_midi_event::<3>( + event.time as i64, + urid, + &event.bytes[0..3] + ).unwrap(), + _ => {} + } + } + inputs.push(atom); + } + let mut outputs = vec![]; + for _ in self.midi_outs.iter() { + outputs.push(::livi::event::LV2AtomSequence::new( + &features, + scope.n_frames() as usize + )); + } + let ports = ::livi::EmptyPortConnections::new() + .with_atom_sequence_inputs( + inputs.iter() + ) + .with_atom_sequence_outputs( + outputs.iter_mut() + ) + .with_audio_inputs( + self.audio_ins.iter().map(|o|o.as_slice(scope)) + ) + .with_audio_outputs( + self.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)) + ); + unsafe { + instance.run(scope.n_frames() as usize, ports).unwrap() + }; + }, + _ => {} + } + Control::Continue + } +} + +impl PortList for Plugin { + fn audio_ins (&self) -> Usually> { + let mut ports = vec![]; + for port in self.audio_ins.iter() { + ports.push(port.name()?); + } + Ok(ports) + } + fn audio_outs (&self) -> Usually> { + let mut ports = vec![]; + for port in self.audio_outs.iter() { + ports.push(port.name()?); + } + Ok(ports) + } + fn midi_ins (&self) -> Usually> { + let mut ports = vec![]; + for port in self.midi_ins.iter() { + ports.push(port.name()?); + } + Ok(ports) + } + fn midi_outs (&self) -> Usually> { + let mut ports = vec![]; + for port in self.midi_outs.iter() { + ports.push(port.name()?); + } + Ok(ports) + } +} diff --git a/src/device/chain/plugin/lv2.rs b/src/model/plugin/lv2.rs similarity index 99% rename from src/device/chain/plugin/lv2.rs rename to src/model/plugin/lv2.rs index 063c44a7..7d6664b5 100644 --- a/src/device/chain/plugin/lv2.rs +++ b/src/model/plugin/lv2.rs @@ -39,3 +39,4 @@ impl LV2Plugin { }) } } + diff --git a/src/device/chain/plugin/vst2.rs b/src/model/plugin/vst2.rs similarity index 79% rename from src/device/chain/plugin/vst2.rs rename to src/model/plugin/vst2.rs index ad2a4888..b94c50c0 100644 --- a/src/device/chain/plugin/vst2.rs +++ b/src/model/plugin/vst2.rs @@ -3,7 +3,7 @@ use super::*; impl ::vst::host::Host for Plugin {} -fn set_vst_plugin (host: &Arc>, path: &str) -> Usually { +fn set_vst_plugin (host: &Arc>, _path: &str) -> Usually { let mut loader = ::vst::host::PluginLoader::load( &std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"), host.clone() @@ -12,3 +12,4 @@ fn set_vst_plugin (host: &Arc>, path: &str) -> Usually instance: loader.instance()? }) } + diff --git a/src/device/chain/plugin/vst3.rs b/src/model/plugin/vst3.rs similarity index 100% rename from src/device/chain/plugin/vst3.rs rename to src/model/plugin/vst3.rs diff --git a/src/model/sampler.rs b/src/model/sampler.rs new file mode 100644 index 00000000..e07fcc6a --- /dev/null +++ b/src/model/sampler.rs @@ -0,0 +1,175 @@ +use crate::core::*; + +pub struct Voice { + pub sample: Arc, + pub after: usize, + pub position: usize, +} + +pub struct Sample { + pub name: String, + pub start: usize, + pub end: usize, + pub channels: Vec>, +} + +pub struct Sampler { + pub name: String, + pub cursor: (usize, usize), + pub samples: BTreeMap>, + pub voices: Vec, + pub midi_in: Port, + pub audio_ins: Vec>, + pub audio_outs: Vec>, +} + +impl Voice { + pub fn chunk (&mut self, mut frames: usize) -> Option>> { + // Create output buffer for each channel + let mut chunk = vec![vec![];self.sample.channels.len()]; + // If it's not time to play yet, count down + if self.after >= frames { + self.after = self.after - frames; + return Some(chunk) + } + // If the voice will start playing within the current buffer, + // subtract the remaining number of wait frames. + if self.after > 0 && self.after < frames { + chunk = vec![vec![0.0;self.after];self.sample.channels.len()]; + frames = frames - self.after; + self.after = 0; + } + if self.position < self.sample.end { + let start = self.position.min(self.sample.end); + let end = (self.position + frames).min(self.sample.end); + for (i, channel) in self.sample.channels.iter().enumerate() { + chunk[i].extend_from_slice(&channel[start..end]); + }; + self.position = self.position + frames; + Some(chunk) + } else { + None + } + } +} + +impl Sample { + pub fn new (name: &str, start: usize, end: usize, channels: Vec>) -> Arc { + Arc::new(Self { name: name.to_string(), start, end, channels }) + } + pub fn play (self: &Arc, after: usize) -> Voice { + Voice { sample: self.clone(), after, position: self.start } + } +} + +impl Sampler { + pub fn new ( + name: &str, + samples: Option>>, + ) -> Result, Box> { + let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?; + DynamicDevice::new( + crate::view::sampler::render, + crate::control::sampler::handle, + Self::process, + Self { + name: name.into(), + cursor: (0, 0), + samples: samples.unwrap_or(BTreeMap::new()), + voices: vec![], + midi_in: client.register_port("midi", ::jack::MidiIn::default())?, + audio_ins: vec![ + client.register_port("recL", ::jack::AudioIn::default())?, + client.register_port("recR", ::jack::AudioIn::default())?, + ], + audio_outs: vec![ + client.register_port("outL", ::jack::AudioOut::default())?, + client.register_port("outR", ::jack::AudioOut::default())?, + ], + }).activate(client) + } + + pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { + // Output buffer: this will be copied to the audio outs. + let channel_count = self.audio_outs.len(); + let mut mixed = vec![vec![0.0;scope.n_frames() as usize];channel_count]; + // Process MIDI input to add new voices. + for RawMidi { time, bytes } in self.midi_in.iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + if let MidiMessage::NoteOn { ref key, .. } = message { + if let Some(sample) = self.samples.get(key) { + self.voices.push(sample.play(time as usize)); + } + } + } + } + // Emit next chunk of each currently playing voice, + // dropping voices that have reached their ends. + let mut voices = vec![]; + std::mem::swap(&mut voices, &mut self.voices); + loop { + if voices.len() < 1 { + break + } + let mut voice = voices.swap_remove(0); + if let Some(chunk) = voice.chunk(scope.n_frames() as usize) { + for (i, channel) in chunk.iter().enumerate() { + let buffer = &mut mixed[i % channel_count]; + for (i, sample) in channel.iter().enumerate() { + buffer[i] += sample; + } + } + self.voices.push(voice); + } + } + // Write output buffer to output ports. + for (i, port) in self.audio_outs.iter_mut().enumerate() { + let buffer = &mixed[i]; + for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { + *value = *buffer.get(i).unwrap_or(&0.0); + } + } + Control::Continue + } + + fn load_sample (&mut self, _path: &str) {} +} + +impl PortList for Sampler { + fn midi_ins (&self) -> Usually> { + Ok(vec![self.midi_in.name()?]) + } + fn audio_ins (&self) -> Usually> { + let mut ports = vec![]; + for port in self.audio_ins.iter() { + ports.push(port.name()?); + } + Ok(ports) + } + fn audio_outs (&self) -> Usually> { + let mut ports = vec![]; + for port in self.audio_outs.iter() { + ports.push(port.name()?); + } + Ok(ports) + } +} + +#[macro_export] macro_rules! sample { + ($note:expr, $name:expr, $src:expr) => { + { + let mut channels: Vec> = vec![]; + for channel in wavers::Wav::from_path($src)?.channels() { + channels.push(channel); + } + let mut end = 0; + let mut data: Vec> = vec![]; + for samples in channels.iter() { + let channel = Vec::from(samples.as_ref()); + end = end.max(channel.len()); + data.push(channel); + } + (u7::from_int_lossy($note).into(), Sample::new($name, 0, end, data).into()) + } + }; +} diff --git a/src/device/launcher/scene.rs b/src/model/scene.rs similarity index 100% rename from src/device/launcher/scene.rs rename to src/model/scene.rs diff --git a/src/model/sequencer.rs b/src/model/sequencer.rs new file mode 100644 index 00000000..475c2040 --- /dev/null +++ b/src/model/sequencer.rs @@ -0,0 +1,141 @@ +use crate::core::*; +use crate::model::*; +#[derive(Debug, Clone)] +pub enum SequencerMode { Tiny, Compact, Horizontal, Vertical, } +pub struct Sequencer { + pub name: String, + /// JACK transport handle. + pub transport: ::jack::Transport, + /// JACK MIDI input port that will be created. + pub midi_in: Port, + /// JACK MIDI output port that will be created. + pub midi_out: Port, + /// Holds info about tempo + pub timebase: Arc, + /// Phrase selector + pub sequence: Option, + /// Map: tick -> MIDI events at tick + pub phrases: Vec, + /// Red keys on piano roll. + pub notes_on: Vec, + /// Play sequence to output. + pub playing: TransportState, + /// Play input through output. + pub monitoring: bool, + /// Write input to sequence. + pub recording: bool, + /// Don't delete when recording. + pub overdub: bool, + /// Display mode + pub view: SequencerMode, + /// Range of notes to display + pub note_start: usize, + /// Position of cursor within note range + pub note_cursor: usize, + /// PPM per display unit + pub time_zoom: usize, + /// Range of time steps to display + pub time_start: usize, + /// Position of cursor within time range + pub time_cursor: usize, +} +render!(Sequencer = crate::view::sequencer::render); +handle!(Sequencer = crate::control::sequencer::handle); +process!(Sequencer |self, _client, scope| { + if self.sequence.is_none() { return Control::Continue } + let phrase = self.phrases.get_mut(self.sequence.unwrap()); + if phrase.is_none() { return Control::Continue } + let phrase = phrase.unwrap(); + let frame = scope.last_frame_time() as usize; + let frames = scope.n_frames() as usize; + let mut output: Vec>>> = vec![None;frames]; + let transport = self.transport.query().unwrap(); + if transport.state != self.playing { + all_notes_off(&mut output); + } + self.playing = transport.state; + // Play from phrase into output buffer + if self.playing == TransportState::Rolling { + phrase.process_out( + &mut output, + &mut self.notes_on, + &self.timebase, + frame, + frames + ); + } + // Play from input to monitor, and record into phrase. + phrase.process_in( + self.midi_in.iter(scope), + &mut self.notes_on, + if self.monitoring { Some(&mut output) } else { None }, + self.recording && self.playing == TransportState::Rolling, + &self.timebase, + frame, + ); + write_output(&mut self.midi_out.writer(scope), &mut output, frames); + Control::Continue +}); +impl Sequencer { + pub fn new ( + name: &str, + timebase: &Arc, + phrases: Option>, + ) -> Usually { + let (client, _status) = Client::new(name, ClientOptions::NO_START_SERVER)?; + let transport = client.transport(); + Ok(Self { + name: name.into(), + timebase: timebase.clone(), + phrases: phrases.unwrap_or_else(||vec![Phrase::default()]), + sequence: Some(0), + + transport, + midi_in: client.register_port("in", MidiIn::default())?, + monitoring: true, + recording: true, + midi_out: client.register_port("out", MidiOut::default())?, + playing: TransportState::Starting, + overdub: true, + + view: SequencerMode::Horizontal, + notes_on: vec![false;128], + note_start: 12, + note_cursor: 0, + time_zoom: 24, + time_start: 0, + time_cursor: 0, + }) + } + + pub fn phrase <'a> (&'a self) -> Option<&'a Phrase> { + self.phrases.get(self.sequence?) + } +} +impl PortList for Sequencer { + fn midi_ins (&self) -> Usually> { Ok(vec![self.midi_in.name()?]) } + fn midi_outs (&self) -> Usually> { Ok(vec![self.midi_out.name()?]) } +} +/// Add "all notes off" to the start of a buffer. +pub fn all_notes_off (output: &mut MIDIChunk) { + output[0] = Some(vec![]); + if let Some(Some(frame)) = output.get_mut(0) { + let mut buf = vec![]; + let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; + let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; + evt.write(&mut buf).unwrap(); + frame.push(buf); + } +} + +/// Write to JACK port from output buffer (containing notes from sequence and/or monitor) +fn write_output (writer: &mut ::jack::MidiWriter, output: &mut MIDIChunk, frames: usize) { + for time in 0..frames { + if let Some(Some(frame)) = output.get_mut(time ) { + for event in frame.iter() { + writer.write(&::jack::RawMidi { time: time as u32, bytes: &event }) + .expect(&format!("{event:?}")); + } + } + } +} diff --git a/src/device/track.rs b/src/model/track.rs similarity index 75% rename from src/device/track.rs rename to src/model/track.rs index 7803c336..862df5ea 100644 --- a/src/device/track.rs +++ b/src/model/track.rs @@ -1,11 +1,12 @@ use crate::core::*; -use crate::device::*; -use crate::layout::*; +use crate::model::*; + pub struct Track { pub name: String, - pub sequencer: DynamicDevice, - pub chain: DynamicDevice, + pub sequencer: Sequencer, + pub chain: Chain, } + impl Track { pub fn new ( name: &str, @@ -15,10 +16,11 @@ impl Track { ) -> Usually { let sequencer = Sequencer::new(&name, tempo, phrases)?; let chain = Chain::new(&name, devices)?; + let (client, _status) = Client::new("init", ClientOptions::NO_START_SERVER)?; { if let (Some(output), Some(input)) = ( sequencer.midi_outs()?.get(0).clone(), - if let Some(item) = chain.state().items.get(0) { + if let Some(item) = chain.items.get(0) { if let Some(port) = item.midi_ins()?.get(0) { Some(port.clone()) } else { @@ -28,14 +30,13 @@ impl Track { None } ) { - if let Some(client) = &sequencer.client { - client.as_client().connect_ports_by_name(&output, &input)?; - } + client.connect_ports_by_name(&output, &input)?; } } Ok(Self { name: name.to_string(), sequencer, chain }) } } + impl PortList for Track { fn midi_ins (&self) -> Usually> { self.sequencer.midi_ins() @@ -44,3 +45,4 @@ impl PortList for Track { self.chain.audio_outs() } } + diff --git a/src/view.rs b/src/view.rs new file mode 100644 index 00000000..9504d073 --- /dev/null +++ b/src/view.rs @@ -0,0 +1,15 @@ +pub mod chain; +pub mod grid; +pub mod layout; +pub mod launcher; +pub mod mixer; +pub mod sampler; +pub mod sequencer; +pub mod transport; +pub mod plugin; + +pub use self::layout::*; +pub use self::transport::TransportView; +pub use self::grid::SceneGridView; +pub use self::chain::{ChainView, ChainViewMode}; +pub use self::sequencer::SequencerView; diff --git a/src/device/chain/mod.rs b/src/view/chain.rs similarity index 62% rename from src/device/chain/mod.rs rename to src/view/chain.rs index b275ecb1..3ef6968f 100644 --- a/src/device/chain/mod.rs +++ b/src/view/chain.rs @@ -1,57 +1,34 @@ -mod plugin; pub use self::plugin::*; -mod sampler; pub use self::sampler::*; - use crate::core::*; -use crate::layout::*; +use crate::view::*; +use crate::model::*; -pub struct Chain { - pub name: String, - pub focused: bool, - pub focus: usize, - pub items: Vec>, - pub view: ChainView, - pub adding: bool, -} - -pub enum ChainView { +pub enum ChainViewMode { Hidden, Compact, Row, Column, } -impl Chain { - pub fn new (name: &str, items: Option>>) -> Result, Box> { - Ok(DynamicDevice::new(render, handle, process, Self { - name: name.into(), - focused: false, - focus: 0, - items: items.unwrap_or_else(||vec![]), - view: ChainView::Column, - adding: false - })) - } +pub struct ChainView<'a> { + pub focused: bool, + pub chain: Option<&'a Chain>, } -impl PortList for Chain { - fn midi_ins (&self) -> Usually> { - if let Some(device) = self.items.get(0) { - device.midi_ins() +impl<'a> Render for ChainView<'a> { + fn render (&self, buf: &mut Buffer, area: Rect) -> Usually { + let style = Some(Style::default().green().dim()); + if self.focused { + let Rect { x, y, width, height} = area; + lozenge_left(buf, x, y, height, style); + lozenge_right(buf, x + width - 1, y, height, style); + }; + let (area, _plugins) = if let Some(ref chain) = self.chain { + draw_as_row(&*chain, buf, area, style)? } else { - Ok(vec![]) - } + (area, vec![]) + }; + Ok(area) } - fn audio_outs (&self) -> Usually> { - if let Some(device) = self.items.get(self.items.len().saturating_sub(1)) { - device.audio_outs() - } else { - Ok(vec![]) - } - } -} - -pub fn process (_: &mut Chain, _: &Client, _: &ProcessScope) -> Control { - Control::Continue } pub fn render (state: &Chain, buf: &mut Buffer, area: Rect) @@ -64,18 +41,18 @@ pub fn render (state: &Chain, buf: &mut Buffer, area: Rect) Style::default().green().dim() }); let result = match state.view { - ChainView::Hidden => Rect { x, y, width: 0, height: 0 }, - ChainView::Compact => { + ChainViewMode::Hidden => Rect { x, y, width: 0, height: 0 }, + ChainViewMode::Compact => { let area = Rect { x, y, width: (state.name.len() + 4) as u16, height: 3 }; buf.set_string(area.x + 2, area.y + 1, &state.name, Style::default()); draw_box_styled(buf, area, selected) }, - ChainView::Row => { + ChainViewMode::Row => { draw_box_styled(buf, area, selected); let (area, _) = Row::draw(buf, area, &state.items, 0)?; area }, - ChainView::Column => { + ChainViewMode::Column => { draw_as_column(state, buf, area, selected)? }, }; @@ -89,8 +66,8 @@ pub fn draw_as_row ( let mut h = 0u16; let mut frames = vec![]; for (i, device) in state.items.iter().enumerate() { - let mut x2 = 0u16; - let mut y2 = 1u16; + let x2 = 0u16; + let _y2 = 1u16; //for port in device.midi_ins()?.iter() { //port.blit(buf, x, y + y2, Some(Style::default())); //x2 = x2.max(port.len() as u16); @@ -129,7 +106,7 @@ pub fn draw_as_row ( h = h.max(frame.height); x = x + frame.width; } - Ok((area, frames)) + Ok((Rect { x: area.x, y: area.y, width, height: h }, frames)) } pub fn draw_as_column ( @@ -215,73 +192,3 @@ pub fn draw_as_column ( draw_box_styled(buf, frames[state.focus], selected); Ok(area) } - -impl Focus for Chain { - fn unfocus (&mut self) { - self.focused = false - } - fn focused (&self) -> Option<&Box> { - match self.focused { - true => self.items.get(self.focus), - false => None - } - } - fn focused_mut (&mut self) -> Option<&mut Box> { - match self.focused { - true => self.items.get_mut(self.focus), - false => None - } - } - fn handle_focus (&mut self, event: &FocusEvent) -> Usually { - Ok(match event { - FocusEvent::Backward => { - if self.focus == 0 { - self.focus = self.items.len(); - } - self.focus = self.focus - 1; - true - }, - FocusEvent::Forward => { - self.focus = self.focus + 1; - if self.focus >= self.items.len() { - self.focus = 0; - } - true - }, - FocusEvent::Inward => { - self.focused = true; - self.items[self.focus].handle(&AppEvent::Focus)?; - true - }, - FocusEvent::Outward => { - if self.focused { - self.focused = false; - self.items[self.focus].handle(&AppEvent::Blur)?; - true - } else { - false - } - }, - }) - } -} - -pub fn handle (state: &mut Chain, event: &AppEvent) -> Usually { - Ok(handle_focus(state, event, keymap!(Chain { - [Up, NONE, "focus_up", "focus row above", - |s: &mut Chain|s.handle_focus(&FocusEvent::Backward)], - [Down, NONE, "focus_down", "focus row below", - |s: &mut Chain|s.handle_focus(&FocusEvent::Forward)], - [Enter, NONE, "focus_down", "focus row below", - |s: &mut Chain|s.handle_focus(&FocusEvent::Inward)], - [Esc, NONE, "focus_down", "focus row below", - |s: &mut Chain|s.handle_focus(&FocusEvent::Outward)], - }))? || handle_keymap(state, event, keymap!(Chain { - [Char('a'), NONE, "add_device", "add a device", add_device] - }))?) -} - -fn add_device (state: &mut Chain) -> Usually { - state.adding = true; - Ok(true) -} diff --git a/src/device/launcher/grid.rs b/src/view/grid.rs similarity index 95% rename from src/device/launcher/grid.rs rename to src/view/grid.rs index 935173be..bcf231a4 100644 --- a/src/device/launcher/grid.rs +++ b/src/view/grid.rs @@ -1,6 +1,7 @@ use crate::core::*; -use super::*; -pub struct SceneGrid<'a> { +use crate::model::*; +use crate::view::*; +pub struct SceneGridView<'a> { pub buf: &'a mut Buffer, pub area: Rect, pub name: &'a str, @@ -9,7 +10,7 @@ pub struct SceneGrid<'a> { pub tracks: &'a[Track], pub cursor: &'a(usize, usize), } -impl<'a> SceneGrid<'a> { +impl<'a> SceneGridView<'a> { pub fn new ( buf: &'a mut Buffer, area: Rect, @@ -123,9 +124,7 @@ impl<'a> SceneGrid<'a> { let clip = scene.clips.get(track); let index = index as u16; let label = if let Some(Some(clip)) = clip { - let track = self.tracks[track].sequencer.state(); - let phrase = track.phrases.get(*clip); - if let Some(phrase) = phrase { + if let Some(phrase) = self.tracks[track].sequencer.phrases.get(*clip) { format!("⯈{}", phrase.name) } else { format!("????") diff --git a/src/view/launcher.rs b/src/view/launcher.rs new file mode 100644 index 00000000..a5713226 --- /dev/null +++ b/src/view/launcher.rs @@ -0,0 +1,50 @@ +use crate::{core::*, model::*, view::*}; + +pub fn render (state: &Launcher, buf: &mut Buffer, mut area: Rect) -> Usually { + //area.width = 80; // DOS mode + //area.height = 25; + let Rect { x, mut y, width, height } = area; + + y = y + TransportView { + timebase: &state.timebase, + playing: state.playing, + record: state.sequencer().map(|s|s.recording).unwrap_or(false), + overdub: state.sequencer().map(|s|s.overdub).unwrap_or(false), + monitor: state.sequencer().map(|s|s.monitoring).unwrap_or(false), + frame: state.current_frame + }.render(buf, area)?.height; + + y = y + SceneGridView { + buf, + area: Rect { x, y, width, height }, + name: &state.name, + focused: state.view.is_tracks(), + scenes: &state.scenes, + tracks: &state.tracks, + cursor: &state.cursor + }.draw()?.height; + + if let Some(chain) = state.chain() { + y = y + ChainView { + focused: state.view.is_chains(), + chain: Some(&*chain), + }.render(buf, Rect { x, y, width, height: height/3 })?.height + } + + let track = state.track().map(|t|t.1); + y = y + SequencerView { + focused: state.view.is_sequencer(), + ppq: state.timebase.ppq() as usize, + track: track, + phrase: track.unwrap().sequencer.phrases.get(state.phrase_id().unwrap()) + }.render(buf, Rect { x, y, width, height: height - y })?.height; + + area.height = y; + if state.show_help { + let style = Some(Style::default().bold().white().not_dim().on_black().italic()); + let hide = "[Tab] Mode [Arrows] Move [.,] Value [F1] Toggle help "; + hide.blit(buf, x + (width - hide.len() as u16) / 2, height - 1, style); + } + + Ok(area) +} diff --git a/src/layout/collect.rs b/src/view/layout/collect.rs similarity index 100% rename from src/layout/collect.rs rename to src/view/layout/collect.rs diff --git a/src/layout/container.rs b/src/view/layout/container.rs similarity index 97% rename from src/layout/container.rs rename to src/view/layout/container.rs index 257839a4..4311ed31 100644 --- a/src/layout/container.rs +++ b/src/view/layout/container.rs @@ -1,6 +1,6 @@ use crate::core::*; -pub struct Stack<'a>(pub &'a[Box]); +pub struct Stack<'a>(pub &'a[Box]); impl<'a> Render for Stack<'a> { fn render (&self, buf: &mut Buffer, area: Rect) -> Usually { diff --git a/src/view/layout/focus.rs b/src/view/layout/focus.rs new file mode 100644 index 00000000..e69de29b diff --git a/src/layout/lozenge.rs b/src/view/layout/lozenge.rs similarity index 100% rename from src/layout/lozenge.rs rename to src/view/layout/lozenge.rs diff --git a/src/layout/mod.rs b/src/view/layout/mod.rs similarity index 100% rename from src/layout/mod.rs rename to src/view/layout/mod.rs diff --git a/src/layout/scroll.rs b/src/view/layout/scroll.rs similarity index 100% rename from src/layout/scroll.rs rename to src/view/layout/scroll.rs diff --git a/src/layout/table.rs b/src/view/layout/table.rs similarity index 100% rename from src/layout/table.rs rename to src/view/layout/table.rs diff --git a/src/view/mixer.rs b/src/view/mixer.rs new file mode 100644 index 00000000..0df7905e --- /dev/null +++ b/src/view/mixer.rs @@ -0,0 +1,68 @@ +use crate::core::*; +use crate::view::*; +use crate::model::*; + +pub fn render (state: &Mixer, buf: &mut Buffer, mut area: Rect) + -> Usually +{ + if area.height < 2 { + return Ok(area) + } + area.x = area.width.saturating_sub(80) / 2; + area.width = area.width.min(80); + area.height = state.tracks.len() as u16 + 2; + draw_box(buf, area); + let x = area.x + 1; + let y = area.y + 1; + let _h = area.height - 2; + for (i, track) in state.tracks.iter().enumerate() { + //buf.set_string( + //x, y + index as u16, + //&track.name, Style::default().bold().not_dim() + //); + for (j, (column, field)) in [ + (0, format!(" {:10} ", track.name)), + (12, format!(" {:.1}dB ", track.gain)), + (22, format!(" [ ] ")), + (30, format!(" C ")), + (35, format!(" {:.1}dB ", track.level)), + (45, format!(" [ ] ")), + (51, format!(" {:7} ", track.route)), + ].into_iter().enumerate() { + buf.set_string( + x + column as u16, + y + i as u16, + field, + if state.selected_track == i && state.selected_column == j { + Style::default().white().bold().not_dim() + } else { + Style::default().not_dim() + } + ); + //stdout.queue(move_to(column, row))?; + //if state.selected_track == i && state.selected_column == j { + //stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?; + //} else { + //stdout.queue(PrintStyledContent(field.to_string().bold()))?; + //} + //fn render_meters ( + //state: &mut Mixer, + //stdout: &mut Stdout, + //offset: Rect + //) -> Result<(), Box> { + //let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); + //for (i, track) in state.tracks.iter().enumerate() { + //let row = (i + 1) as u16; + //stdout + //.queue(move_to(10, row))?.queue(PrintStyledContent("▁".green()))? + //.queue(move_to(20, row))?.queue(PrintStyledContent("▁".green()))? + //.queue(move_to(28, row))?.queue(PrintStyledContent("▁".green()))? + //.queue(move_to(43, row))?.queue(PrintStyledContent("▁".green()))?; + //} + //Ok(()) + //} + } + } + Ok(area) +} + diff --git a/src/view/plugin.rs b/src/view/plugin.rs new file mode 100644 index 00000000..b704fa91 --- /dev/null +++ b/src/view/plugin.rs @@ -0,0 +1,48 @@ +use crate::{core::*, model::*}; + +pub fn render (state: &Plugin, buf: &mut Buffer, area: Rect) + -> Usually +{ + let Rect { x, y, height, .. } = area; + let mut width = 20u16; + match &state.plugin { + Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => { + let start = state.selected.saturating_sub((height as usize / 2).saturating_sub(1)); + let end = start + height as usize - 2; + //draw_box(buf, Rect { x, y, width, height }); + for i in start..end { + if let Some(port) = port_list.get(i) { + let value = if let Some(value) = instance.control_input(port.index) { + value + } else { + port.default_value + }; + //let label = &format!("C·· M·· {:25} = {value:.03}", port.name); + let label = &format!("{:25} = {value:.03}", port.name); + width = width.max(label.len() as u16 + 4); + label.blit(buf, x + 2, y + 1 + i as u16 - start as u16, if i == state.selected { + Some(Style::default().green()) + } else { + None + }); + } else { + break + } + } + }, + _ => {} + }; + draw_header(state, buf, area.x, area.y, width)?; + Ok(Rect { width, ..area }) +} + +fn draw_header (state: &Plugin, buf: &mut Buffer, x: u16, y: u16, w: u16) -> Usually { + let style = Style::default().gray(); + let label1 = format!(" {}", state.name); + label1.blit(buf, x + 1, y, Some(style.white().bold())); + if let Some(ref path) = state.path { + let label2 = format!("{}…", &path[..((w as usize - 10).min(path.len()))]); + label2.blit(buf, x + 2 + label1.len() as u16, y, Some(style.not_dim())); + } + Ok(Rect { x, y, width: w, height: 1 }) +} diff --git a/src/view/sampler.rs b/src/view/sampler.rs new file mode 100644 index 00000000..b431292f --- /dev/null +++ b/src/view/sampler.rs @@ -0,0 +1,78 @@ +use crate::core::*; +use crate::model::*; + +pub fn render (state: &Sampler, buf: &mut Buffer, Rect { x, y, height, .. }: Rect) + -> Usually +{ + let style = Style::default().gray(); + let title = format!(" {} ({})", state.name, state.voices.len()); + title.blit(buf, x+1, y, Some(style.white().bold().not_dim())); + let mut width = title.len() + 2; + for (i, (note, sample)) in state.samples.iter().enumerate() { + let style = if i == state.cursor.0 { + Style::default().green() + } else { + Style::default() + }; + let i = i as u16; + let y1 = y+1+i; + if y1 >= y + height { + break + } + if i as usize == state.cursor.0 { + "⯈".blit(buf, x+1, y1, Some(style.bold())); + } + let label1 = format!("{note:3} {:8}", sample.name); + let label2 = format!("{:>6} {:>6}", sample.start, sample.end); + label1.blit(buf, x+2, y1, Some(style.bold())); + label2.blit(buf, x+3+label1.len()as u16, y1, Some(style)); + width = width.max(label1.len() + label2.len() + 4); + } + let height = ((1 + state.samples.len()) as u16).min(height); + Ok(Rect { x, y, width: width as u16, height }) +} + +//fn render_table ( + //state: &mut Sampler, + //stdout: &mut Stdout, + //offset: (u16, u16), +//) -> Result<(), Box> { + //let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); + //stdout.queue(move_to(0, 3))?.queue( + //Print(" Name Rate Trigger Route") + //)?; + //for (i, sample) in state.samples.lock().unwrap().iter().enumerate() { + //let row = 4 + i as u16; + //for (j, (column, field)) in [ + //(0, format!(" {:7} ", sample.name)), + //(9, format!(" {:.1}Hz ", sample.rate)), + //(18, format!(" MIDI C{} {} ", sample.trigger.0, sample.trigger.1)), + //(33, format!(" {:.1}dB -> Output ", sample.gain)), + //(50, format!(" {} ", sample.playing.unwrap_or(0))), + //].into_iter().enumerate() { + //stdout.queue(move_to(column, row))?; + //if state.selected_sample == i && state.selected_column == j { + //stdout.queue(PrintStyledContent(field.to_string().bold().reverse()))?; + //} else { + //stdout.queue(PrintStyledContent(field.to_string().bold()))?; + //} + //} + //} + //Ok(()) +//} + +//fn render_meters ( + //state: &mut Sampler, + //stdout: &mut Stdout, + //offset: (u16, u16), +//) -> Result<(), Box> { + //let move_to = |col, row| crossterm::cursor::MoveTo(offset.0 + col, offset.1 + row); + //for (i, sample) in state.samples.lock().iter().enumerate() { + //let row = 4 + i as u16; + //stdout.queue(move_to(32, row))?.queue( + //PrintStyledContent("▁".green()) + //)?; + //} + //Ok(()) +//} + diff --git a/src/view/sequencer.rs b/src/view/sequencer.rs new file mode 100644 index 00000000..50eed021 --- /dev/null +++ b/src/view/sequencer.rs @@ -0,0 +1,104 @@ +use crate::{core::*,model::*,view::*}; + +pub mod horizontal; +pub mod vertical; + +pub struct SequencerView<'a> { + pub focused: bool, + pub phrase: Option<&'a Phrase>, + pub track: Option<&'a Track>, + pub ppq: usize, +} + +impl<'a> Render for SequencerView<'a> { + fn render (&self, buf: &mut Buffer, area: Rect) -> Usually { + let Rect { x, y, width, height } = area; + let style = Some(Style::default().green().dim()); + if self.focused { + lozenge_left(buf, x, y, height, style); + lozenge_right(buf, x + width - 1, y, height, style); + } + if let Some(ref track) = self.track { + self::horizontal::draw( + buf, + area, + self.phrase, + self.ppq, + track.sequencer.time_cursor, + track.sequencer.time_start, + track.sequencer.time_zoom, + track.sequencer.note_cursor, + track.sequencer.note_start, + Some(if self.focused { + Style::default().green().not_dim() + } else { + Style::default().green().dim() + }) + )?; + } + Ok(area) + } +} + +pub fn render (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually { + let Rect { x, y, width, height } = area; + let header = draw_header(s, buf, area)?; + let piano = match s.view { + SequencerMode::Tiny => Rect { x, y, width, height: 0 }, + SequencerMode::Compact => Rect { x, y, width, height: 0 }, + SequencerMode::Vertical => self::vertical::draw(s, buf, Rect { + x, y: y + header.height, width, height, + })?, + SequencerMode::Horizontal => self::horizontal::draw( + buf, + Rect { x, y: y + header.height, width, height, }, + s.phrase(), + s.timebase.ppq() as usize, + s.time_cursor, + s.time_start, + s.time_zoom, + s.note_cursor, + s.note_start, + None + )?, + }; + Ok(draw_box(buf, Rect { + x, y, + width: header.width.max(piano.width), + height: header.height + piano.height + })) +} + +pub fn draw_header (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually { + let Rect { x, y, width, .. } = area; + let style = Style::default().gray(); + crate::view::TransportView { + timebase: &s.timebase, + playing: s.playing, + record: s.recording, + overdub: s.overdub, + monitor: s.monitoring, + frame: 0 + }.render(buf, area)?; + let separator = format!("├{}┤", "-".repeat((width - 2).into())); + separator.blit(buf, x, y + 2, Some(style.dim())); + let _ = draw_clips(s, buf, area)?; + Ok(Rect { x, y, width, height: 3 }) +} + +pub fn draw_clips (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually { + let Rect { x, y, .. } = area; + let style = Style::default().gray(); + for (i, sequence) in s.phrases.iter().enumerate() { + let label = format!("▶ {}", &sequence.name); + label.blit(buf, x + 2, y + 3 + (i as u16)*2, Some(if Some(i) == s.sequence { + match s.playing { + TransportState::Rolling => style.white().bold(), + _ => style.not_dim().bold() + } + } else { + style.dim() + })); + } + Ok(Rect { x, y, width: 14, height: 14 }) +} diff --git a/src/device/sequencer/horizontal.rs b/src/view/sequencer/horizontal.rs similarity index 93% rename from src/device/sequencer/horizontal.rs rename to src/view/sequencer/horizontal.rs index b0a18851..020546b6 100644 --- a/src/device/sequencer/horizontal.rs +++ b/src/view/sequencer/horizontal.rs @@ -45,12 +45,7 @@ pub fn draw ( Ok(area) } -pub fn timer ( - buf: &mut Buffer, - area: Rect, - time0: usize, - now: usize -) { +pub fn timer (buf: &mut Buffer, area: Rect, time0: usize, now: usize) { let x = area.x + 5; for step in time0..(time0+area.width as usize).saturating_sub(5) { buf.set_string(x + step as u16, area.y, &"-", if step == now { @@ -61,12 +56,9 @@ pub fn timer ( } } -pub fn keys ( - buf: &mut Buffer, - area: Rect, - note0: usize, - notes: &[bool], -) -> Usually { +pub fn keys (buf: &mut Buffer, area: Rect, note0: usize, _notes: &[bool]) + -> Usually +{ let bw = Style::default().dim(); let Rect { x, y, width, height } = area; let h = height.saturating_sub(2); @@ -98,13 +90,13 @@ pub fn lanes ( phrase: &Phrase, ppq: usize, time_z: usize, - time0: usize, + _time0: usize, note0: usize, ) { let Rect { x, y, width, height } = area; - let time0 = time0 / time_z; - let time1 = time0 + width as usize; - let note1 = note0 + height as usize; + //let time0 = time0 / time_z; + //let time1 = time0 + width as usize; + //let note1 = note0 + height as usize; let bg = Style::default(); let (bw, wh) = (bg.dim(), bg.white()); let offset = 5; diff --git a/src/device/sequencer/vertical.rs b/src/view/sequencer/vertical.rs similarity index 99% rename from src/device/sequencer/vertical.rs rename to src/view/sequencer/vertical.rs index a6933487..e68f5bb1 100644 --- a/src/device/sequencer/vertical.rs +++ b/src/view/sequencer/vertical.rs @@ -1,4 +1,3 @@ -use crate::core::*; use super::*; pub fn draw ( diff --git a/src/device/transport.rs b/src/view/transport.rs similarity index 97% rename from src/device/transport.rs rename to src/view/transport.rs index 8d5b8930..0634f2ec 100644 --- a/src/device/transport.rs +++ b/src/view/transport.rs @@ -1,7 +1,6 @@ use crate::core::*; -use crate::layout::*; -pub struct Transport<'a> { +pub struct TransportView<'a> { pub timebase: &'a Arc, pub playing: TransportState, pub record: bool, @@ -10,7 +9,7 @@ pub struct Transport<'a> { pub frame: usize, } -impl<'a> Render for Transport<'a> { +impl<'a> Render for TransportView<'a> { fn render (&self, buf: &mut Buffer, area: Rect) -> Usually { let Rect { x, y, width, .. } = area; draw_play_stop(buf, x + 1, y, &self.playing); @@ -40,6 +39,7 @@ pub fn draw_timer (buf: &mut Buffer, x: u16, y: u16, timebase: &Arc, f ); timer.blit(buf, x - timer.len() as u16, y, Some(Style::default().not_dim())); } + pub fn draw_play_stop (buf: &mut Buffer, x: u16, y: u16, state: &TransportState) { let style = Style::default().gray(); match state { @@ -52,6 +52,7 @@ pub fn draw_play_stop (buf: &mut Buffer, x: u16, y: u16, state: &TransportState) TransportState::Rolling => style.not_dim().white().bold() })); } + pub fn draw_rec (buf: &mut Buffer, x: u16, y: u16, on: bool) { "⏺ REC".blit(buf, x, y, Some(if on { Style::default().bold().red() @@ -59,6 +60,7 @@ pub fn draw_rec (buf: &mut Buffer, x: u16, y: u16, on: bool) { Style::default().bold().dim() })) } + pub fn draw_dub (buf: &mut Buffer, x: u16, y: u16, on: bool) { "⏺ DUB".blit(buf, x, y, Some(if on { Style::default().bold().yellow() @@ -66,6 +68,7 @@ pub fn draw_dub (buf: &mut Buffer, x: u16, y: u16, on: bool) { Style::default().bold().dim() })) } + pub fn draw_mon (buf: &mut Buffer, x: u16, y: u16, on: bool) { "⏺ MON".blit(buf, x, y, Some(if on { Style::default().bold().green() @@ -73,6 +76,7 @@ pub fn draw_mon (buf: &mut Buffer, x: u16, y: u16, on: bool) { Style::default().bold().dim() })) } + pub fn draw_bpm (buf: &mut Buffer, x: u16, y: u16, bpm: usize) { let style = Style::default().not_dim(); "BPM" @@ -88,3 +92,4 @@ pub fn draw_bpm (buf: &mut Buffer, x: u16, y: u16, bpm: usize) { "1/16" .blit(buf, x + 29, y, Some(style.bold())); } +