diff --git a/edn/src/lib.rs b/edn/src/lib.rs index cb660327..ea7fec7b 100644 --- a/edn/src/lib.rs +++ b/edn/src/lib.rs @@ -10,7 +10,7 @@ pub(crate) use self::ParseError::*; pub(crate) use konst::iter::{ConstIntoIter, IsIteratorKind}; pub(crate) use konst::string::{split_at, str_range, char_indices}; pub(crate) use std::error::Error; -pub(crate) use std::fmt::{Debug, Display, Formatter, Result as FormatResult, Error as FormatError}; +pub(crate) use std::fmt::{Debug, Display, Formatter, Result as FormatResult}; /// Static iteration helper. #[macro_export] macro_rules! iterate { ($expr:expr => $arg: pat => $body:expr) => { diff --git a/tek/src/audio.rs b/tek/src/audio.rs new file mode 100644 index 00000000..22bb402c --- /dev/null +++ b/tek/src/audio.rs @@ -0,0 +1,71 @@ +use crate::*; +has_jack!(|self: Tek|&self.jack); +audio!(|self: Tek, client, scope|{ + // Start profiling cycle + let t0 = self.perf.get_t0(); + // Update transport clock + self.clock().update_from_scope(scope).unwrap(); + // Collect MIDI input (TODO preallocate) + let midi_in = self.midi_ins.iter() + .map(|port|port.port.iter(scope) + .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) + .collect::>()) + .collect::>(); + // Update standalone MIDI sequencer + if let Some(player) = self.player.as_mut() { + if Control::Quit == PlayerAudio( + player, + &mut self.note_buf, + &mut self.midi_buf, + ).process(client, scope) { + return Control::Quit + } + } + // Update standalone sampler + if let Some(sampler) = self.sampler.as_mut() { + if Control::Quit == SamplerAudio(sampler).process(client, scope) { + return Control::Quit + } + //for port in midi_in.iter() { + //for message in port.iter() { + //match message { + //Ok(M + //} + //} + //} + } + // TODO move these to editor and sampler?: + for port in midi_in.iter() { + for event in port.iter() { + match event { + (time, Ok(LiveEvent::Midi {message, ..})) => match message { + MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { + editor.set_note_pos(key.as_int() as usize); + }, + MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( + self.editor.as_ref(), + self.sampler.as_ref(), + ) => { + // TODO: give sampler its own cursor + if let Some(sample) = &sampler.mapped[editor.note_pos()] { + sample.write().unwrap().handle_cc(*controller, *value) + } + } + _ =>{} + }, + _ =>{} + } + } + } + // Update track sequencers + for track in self.tracks.iter_mut() { + if PlayerAudio( + track.player_mut(), &mut self.note_buf, &mut self.midi_buf + ).process(client, scope) == Control::Quit { + return Control::Quit + } + } + // End profiling cycle + self.perf.update(t0, scope); + Control::Continue +}); diff --git a/tek/src/cli.rs b/tek/src/cli.rs new file mode 100644 index 00000000..18fce22a --- /dev/null +++ b/tek/src/cli.rs @@ -0,0 +1,182 @@ +use crate::*; +use clap::{self, Parser, Subcommand}; +#[derive(Debug, Parser)] +#[command(version, about, long_about = None)] +pub struct TekCli { + /// Which app to initialize + #[command(subcommand)] mode: TekMode, + /// Name of JACK client + #[arg(short='n', long)] name: Option, + /// Whether to attempt to become transport master + #[arg(short='S', long, default_value_t = false)] sync_lead: bool, + /// Whether to sync to external transport master + #[arg(short='s', long, default_value_t = true)] sync_follow: bool, + /// Initial tempo in beats per minute + #[arg(short='b', long, default_value = None)] bpm: Option, + /// Whether to include a transport toolbar (default: true) + #[arg(short='t', long, default_value_t = true)] show_clock: bool, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='I', long)] midi_from: Vec, + /// MIDI outs to connect to (multiple instances accepted) + #[arg(short='i', long)] midi_from_re: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='O', long)] midi_to: Vec, + /// MIDI ins to connect to (multiple instances accepted) + #[arg(short='o', long)] midi_to_re: Vec, + /// Audio outs to connect to left input + #[arg(short='l', long)] left_from: Vec, + /// Audio outs to connect to right input + #[arg(short='r', long)] right_from: Vec, + /// Audio ins to connect from left output + #[arg(short='L', long)] left_to: Vec, + /// Audio ins to connect from right output + #[arg(short='R', long)] right_to: Vec, +} +#[derive(Debug, Clone, Subcommand)] pub enum TekMode { + /// A standalone transport clock. + Clock, + /// A MIDI sequencer. + Sequencer, + /// A MIDI-controlled audio sampler. + Sampler, + /// Sequencer and sampler together.12 + Groovebox, + /// Multi-track MIDI sequencer. + Arranger { + /// Number of scenes + #[arg(short = 'y', long, default_value_t = 1)] scenes: usize, + /// Number of tracks + #[arg(short = 'x', long, default_value_t = 1)] tracks: usize, + /// Width of tracks + #[arg(short = 'w', long, default_value_t = 9)] track_width: usize, + }, + /// TODO: A MIDI-controlled audio mixer + Mixer, + /// TODO: A customizable channel strip + Track, + /// TODO: An audio plugin host + Plugin, +} +impl TekCli { + pub fn run (&self) -> Usually<()> { + let name = self.name.as_ref().map_or("tek", |x|x.as_str()); + //let color = ItemPalette::random(); + let jack = JackConnection::new(name)?; + let engine = Tui::new()?; + let empty = &[] as &[&str]; + let midi_froms = PortConnection::collect(&self.midi_from, &self.midi_from_re, empty); + let midi_tos = PortConnection::collect(&self.midi_to, &self.midi_to_re, empty); + let left_froms = PortConnection::collect(&self.left_from, empty, empty); + let left_tos = PortConnection::collect(&self.left_to, empty, empty); + let right_froms = PortConnection::collect(&self.right_from, empty, empty); + let right_tos = PortConnection::collect(&self.right_to, empty, empty); + let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; + let audio_tos = &[left_tos.as_slice(), right_tos.as_slice() ]; + engine.run(&jack.activate_with(|jack|match self.mode { + TekMode::Clock => Tek::new_clock( + jack, self.bpm, self.sync_lead, self.sync_follow, + &midi_froms, &midi_tos), + TekMode::Sequencer => Tek::new_sequencer( + jack, self.bpm, self.sync_lead, self.sync_follow, + &midi_froms, &midi_tos), + TekMode::Groovebox => Tek::new_groovebox( + jack, self.bpm, self.sync_lead, self.sync_follow, + &midi_froms, &midi_tos, + &audio_froms, &audio_tos), + TekMode::Arranger { scenes, tracks, track_width, .. } => Tek::new_arranger( + jack, self.bpm, self.sync_lead, self.sync_follow, + &midi_froms, &midi_tos, + &audio_froms, &audio_tos, + scenes, tracks, track_width), + _ => todo!() + })?) + } +} +impl Tek { + pub fn new_clock ( + jack: &Arc>, + bpm: Option, sync_lead: bool, sync_follow: bool, + midi_froms: &[PortConnection], midi_tos: &[PortConnection], + ) -> Usually { + let tek = Self { + view: SourceIter(include_str!("./view_transport.edn")), + jack: jack.clone(), + color: ItemPalette::random(), + clock: Clock::new(jack, bpm), + midi_ins: vec![JackPort::::new(jack, "GlobalI", midi_froms)?], + midi_outs: vec![JackPort::::new(jack, "GlobalO", midi_tos)?], + keys: SourceIter(KEYS_APP), + keys_clip: SourceIter(KEYS_CLIP), + keys_track: SourceIter(KEYS_TRACK), + keys_scene: SourceIter(KEYS_SCENE), + keys_mix: SourceIter(KEYS_MIX), + fmtd_beat: Arc::new(RwLock::new(String::with_capacity(16))), + fmtd_time: Arc::new(RwLock::new(String::with_capacity(16))), + fmtd_bpm: Arc::new(RwLock::new(String::with_capacity(16))), + fmtd_sr: Arc::new(RwLock::new(String::with_capacity(16))), + fmtd_buf: Arc::new(RwLock::new(String::with_capacity(16))), + fmtd_lat: Arc::new(RwLock::new(String::with_capacity(16))), + fmtd_stop: "⏹".into(), + ..Default::default() + }; + tek.sync_lead(sync_lead); + tek.sync_follow(sync_follow); + Ok(tek) + } + pub fn new_sequencer ( + jack: &Arc>, + bpm: Option, sync_lead: bool, sync_follow: bool, + midi_froms: &[PortConnection], midi_tos: &[PortConnection], + ) -> Usually { + let clip = MidiClip::new("Clip", true, 384usize, None, Some(ItemColor::random().into())); + let clip = Arc::new(RwLock::new(clip)); + Ok(Self { + view: SourceIter(include_str!("./view_sequencer.edn")), + pool: Some((&clip).into()), + editor: Some((&clip).into()), + editing: false.into(), + midi_buf: vec![vec![];65536], + player: Some(MidiPlayer::new(&jack, "sequencer", Some(&clip), &midi_froms, &midi_tos)?), + ..Self::new_clock(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)? + }) + } + pub fn new_groovebox ( + jack: &Arc>, + bpm: Option, sync_lead: bool, sync_follow: bool, + midi_froms: &[PortConnection], midi_tos: &[PortConnection], + audio_froms: &[&[PortConnection];2], audio_tos: &[&[PortConnection];2], + ) -> Usually { + let app = Self { + view: SourceIter(include_str!("./view_groovebox.edn")), + sampler: Some(Sampler::new(jack, &"sampler", midi_froms, audio_froms, audio_tos)?), + ..Self::new_sequencer(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)? + }; + if let Some(sampler) = app.sampler.as_ref().unwrap().midi_in.as_ref() { + jack.connect_ports(&app.player.as_ref().unwrap().midi_outs[0].port, &sampler.port)?; + } + Ok(app) + } + pub fn new_arranger ( + jack: &Arc>, + bpm: Option, sync_lead: bool, sync_follow: bool, + midi_froms: &[PortConnection], midi_tos: &[PortConnection], + audio_froms: &[&[PortConnection];2], audio_tos: &[&[PortConnection];2], + scenes: usize, tracks: usize, track_width: usize, + ) -> Usually { + let mut arranger = Self { + view: SourceIter(include_str!("./view_arranger.edn")), + ..Self::new_groovebox( + jack, bpm, sync_lead, sync_follow, + midi_froms, midi_tos, audio_froms, audio_tos, + )? + }; + arranger.scenes_add(scenes); + arranger.tracks_add(tracks, track_width, midi_froms, midi_tos); + arranger.selected = Selection::Clip(1, 1); + Ok(arranger) + } +} +#[cfg(test)] fn test_tek_cli () { + use clap::CommandFactory; + TekCli::command().debug_assert(); +} diff --git a/tek/src/keys.rs b/tek/src/keys.rs new file mode 100644 index 00000000..5f85c485 --- /dev/null +++ b/tek/src/keys.rs @@ -0,0 +1,308 @@ +use crate::*; +pub const KEYS_APP: &str = include_str!("keys.edn"); +pub const KEYS_CLIP: &str = include_str!("keys_clip.edn"); +pub const KEYS_TRACK: &str = include_str!("keys_track.edn"); +pub const KEYS_SCENE: &str = include_str!("keys_scene.edn"); +pub const KEYS_MIX: &str = include_str!("keys_mix.edn"); +pub struct Keymaps { +} +handle!(TuiIn: |self: Tek, input|Ok({ + // If editing, editor keys take priority + if self.is_editing() { + if self.editor.handle(input)? == Some(true) { + return Ok(Some(true)) + } + } + // Handle from root keymap + if let Some(command) = self.keys.command::<_, TekCommand, _>(self, input) { + if let Some(undo) = command.execute(self)? { self.history.push(undo); } + return Ok(Some(true)) + } + // Handle from selection-dependent keymaps + if let Some(command) = match self.selected() { + Selection::Clip(_, _) => self.keys_clip, + Selection::Track(_) => self.keys_track, + Selection::Scene(_) => self.keys_scene, + Selection::Mix => self.keys_mix, + }.command::<_, TekCommand, _>(self, input) { + if let Some(undo) = command.execute(self)? { self.history.push(undo); } + return Ok(Some(true)) + } + None +})); +#[derive(Clone, Debug)] pub enum TekCommand { + Clip(ClipCommand), + Clock(ClockCommand), + Color(ItemPalette), + Edit(Option), + Editor(MidiEditCommand), + Enqueue(Option>>), + History(isize), + Pool(PoolCommand), + Sampler(SamplerCommand), + Scene(SceneCommand), + Select(Selection), + StopAll, + Track(TrackCommand), + Zoom(Option), +} +atom_command!(TekCommand: |app: Tek| { + ("stop" [] Self::StopAll) + ("undo" [d: usize] Self::History(-(d.unwrap_or(0)as isize))) + ("redo" [d: usize] Self::History(d.unwrap_or(0) as isize)) + ("zoom" [z: usize] Self::Zoom(z)) + ("edit" [] Self::Edit(None)) + ("edit" [c: bool] Self::Edit(c)) + ("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default())) + ("enqueue" [c: Arc>] Self::Enqueue(c)) + ("select" [t: usize, s: usize] match (t.expect("no track"), s.expect("no scene")) { + (0, 0) => Self::Select(Selection::Mix), + (t, 0) => Self::Select(Selection::Track(t)), + (0, s) => Self::Select(Selection::Scene(s)), + (t, s) => Self::Select(Selection::Clip(t, s)), + }) + ("clip" [,..a] Self::Clip( + ClipCommand::try_from_expr(app, a).expect("invalid command"))) + ("clock" [,..a] Self::Clock( + ClockCommand::try_from_expr(app.clock(), a).expect("invalid command"))) + ("editor" [,..a] Self::Editor( + MidiEditCommand::try_from_expr(app.editor.as_ref().expect("no editor"), a).expect("invalid command"))) + ("pool" [,..a] Self::Pool( + PoolCommand::try_from_expr(app.pool.as_ref().expect("no pool"), a).expect("invalid command"))) + ("sampler" [,..a] Self::Sampler( + SamplerCommand::try_from_expr(app.sampler.as_ref().expect("no sampler"), a).expect("invalid command"))) + ("scene" [,..a] Self::Scene( + SceneCommand::try_from_expr(app, a).expect("invalid command"))) + ("track" [,..a] Self::Track( + TrackCommand::try_from_expr(app, a).expect("invalid command"))) +}); +command!(|self: TekCommand, app: Tek|match self { + Self::Zoom(_) => { println!("\n\rtodo: global zoom"); None }, + Self::History(delta) => { println!("\n\rtodo: undo/redo"); None }, + Self::Select(s) => { + app.selected = s; + // autoedit: load focused clip in editor. + if let Some(ref mut editor) = app.editor { + editor.set_clip(match app.selected { + Selection::Clip(t, s) if let Some(Some(Some(clip))) = app + .scenes.get(s).map(|s|s.clips.get(t)) => Some(clip), + _ => None + }); + } + None + }, + Self::Edit(value) => { + if let Some(value) = value { + if app.is_editing() != value { + app.editing.store(value, Relaxed); + } + } else { + app.editing.store(!app.is_editing(), Relaxed); + }; + // autocreate: create new clip from pool when entering empty cell + if let Some(ref pool) = app.pool { + if app.is_editing() { + if let Selection::Clip(t, s) = app.selected { + if let Some(scene) = app.scenes.get_mut(s.saturating_sub(1)) { + if let Some(slot) = scene.clips.get_mut(t.saturating_sub(1)) { + if slot.is_none() { + let (index, mut clip) = pool.add_new_clip(); + // autocolor: new clip colors from scene and track color + clip.write().unwrap().color = ItemColor::random_near( + app.tracks[t.saturating_sub(1)].color.base.mix( + scene.color.base, + 0.5 + ), + 0.2 + ).into(); + if let Some(ref mut editor) = app.editor { + editor.set_clip(Some(&clip)); + } + *slot = Some(clip); + } + } + } + } + } + } + None + }, + Self::Clock(cmd) => cmd.delegate(app, Self::Clock)?, + Self::Scene(cmd) => cmd.delegate(app, Self::Scene)?, + Self::Track(cmd) => cmd.delegate(app, Self::Track)?, + Self::Clip(cmd) => cmd.delegate(app, Self::Clip)?, + Self::Editor(cmd) => app.editor.as_mut() + .map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(), + Self::Sampler(cmd) => app.sampler.as_mut() + .map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(), + Self::Enqueue(clip) => app.player.as_mut() + .map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(), + Self::Color(palette) => { + use Selection::*; + Some(Self::Color(match app.selected { + Mix => { + let old = app.color; + app.color = palette; + old + }, + Track(t) => { + let t = t.saturating_sub(1); + let old = app.tracks[t].color; + app.tracks[t].color = palette; + old + } + Scene(s) => { + let s = s.saturating_sub(1); + let old = app.scenes[s].color; + app.scenes[s].color = palette; + old + } + Clip(t, s) => { + let t = t.saturating_sub(1); + let s = s.saturating_sub(1); + if let Some(ref clip) = app.scenes[s].clips[t] { + let mut clip = clip.write().unwrap(); + let old = clip.color; + clip.color = palette; + old + } else { + return Ok(None) + } + } + })) + }, + Self::StopAll => { + for track in 0..app.tracks.len(){app.tracks[track].player.enqueue_next(None);} + None + }, + Self::Pool(cmd) => if let Some(pool) = app.pool.as_mut() { + let undo = cmd.clone().delegate(pool, Self::Pool)?; + if let Some(editor) = app.editor.as_mut() { + match cmd { + // autoselect: automatically load selected clip in editor + // autocolor: update color in all places simultaneously + PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) => + editor.set_clip(pool.clip().as_ref()), + _ => {} + } + }; + undo + } else { + None + }, +}); +#[derive(Clone, Debug)] pub enum TrackCommand { + Add, + Del(usize), + Stop(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), +} +atom_command!(TrackCommand: |app: Tek| { + ("add" [] Self::Add) + ("size" [a: usize] Self::SetSize(a.unwrap())) + ("zoom" [a: usize] Self::SetZoom(a.unwrap())) + ("color" [a: usize] Self::SetColor(a.unwrap().saturating_sub(1), ItemPalette::random())) + ("del" [a: usize] Self::Del(a.unwrap().saturating_sub(1))) + ("stop" [a: usize] Self::Stop(a.unwrap().saturating_sub(1))) + ("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap())) +}); +command!(|self: TrackCommand, app: Tek|match self { + Self::Add => { + use Selection::*; + let index = app.track_add(None, None, &[], &[])?.0 + 1; + app.selected = match app.selected { + Track(t) => Track(index), + Clip(t, s) => Clip(index, s), + _ => app.selected + }; + Some(Self::Del(index)) + }, + Self::Del(index) => { app.track_del(index); None }, + Self::Stop(track) => { app.tracks[track].player.enqueue_next(None); None }, + Self::SetColor(index, color) => { + let old = app.tracks[index].color; + app.tracks[index].color = color; + Some(Self::SetColor(index, old)) + }, + _ => None +}); +#[derive(Clone, Debug)] pub enum SceneCommand { + Add, + Del(usize), + Swap(usize, usize), + SetSize(usize), + SetZoom(usize), + SetColor(usize, ItemPalette), + Enqueue(usize), +} +atom_command!(SceneCommand: |app: Tek| { + ("add" [] Self::Add) + ("del" [a: usize] Self::Del(0)) + ("zoom" [a: usize] Self::SetZoom(a.unwrap())) + ("color" [a: usize] Self::SetColor(a.unwrap().saturating_sub(1), ItemPalette::random())) + ("enqueue" [a: usize] Self::Enqueue(a.unwrap().saturating_sub(1))) + ("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap())) +}); +command!(|self: SceneCommand, app: Tek|match self { + Self::Add => { + use Selection::*; + let index = app.scene_add(None, None)?.0 + 1; + app.selected = match app.selected { + Scene(s) => Scene(index), + Clip(t, s) => Clip(t, index), + _ => app.selected + }; + Some(Self::Del(index)) + }, + Self::Del(index) => { app.scene_del(index); None }, + Self::SetColor(index, color) => { + let old = app.scenes[index].color; + app.scenes[index].color = color; + Some(Self::SetColor(index, old)) + }, + Self::Enqueue(scene) => { + for track in 0..app.tracks.len() { + app.tracks[track].player.enqueue_next(app.scenes[scene].clips[track].as_ref()); + } + None + }, + _ => None +}); +#[derive(Clone, Debug)] pub enum ClipCommand { + Get(usize, usize), + Put(usize, usize, Option>>), + Enqueue(usize, usize), + Edit(Option>>), + SetLoop(usize, usize, bool), + SetColor(usize, usize, ItemPalette), +} +atom_command!(ClipCommand: |app: Tek| { + ("get" [a: usize ,b: usize] + Self::Get(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1))) + ("put" [a: usize, b: usize, c: Option>>] + Self::Put(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1), c.unwrap())) + ("enqueue" [a: usize, b: usize] + Self::Enqueue(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1))) + ("edit" [a: Option>>] + Self::Edit(a.unwrap())) + ("loop" [a: usize, b: usize, c: bool] + Self::SetLoop(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1), c.unwrap())) + ("color" [a: usize, b: usize] + Self::SetColor(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1), ItemPalette::random())) +}); +command!(|self: ClipCommand, app: Tek|match self { + Self::Get(track, scene) => { todo!() }, + Self::Put(track, scene, clip) => { + let old = app.scenes[scene].clips[track].clone(); + app.scenes[scene].clips[track] = clip; + Some(Self::Put(track, scene, old)) + }, + Self::Enqueue(track, scene) => { + app.tracks[track].player.enqueue_next(app.scenes[scene].clips[track].as_ref()); + None + }, + _ => None +}); diff --git a/tek/src/lib.rs b/tek/src/lib.rs index 937e6c44..d022eecf 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -22,1306 +22,9 @@ pub use ::tek_tui::{ Event, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, KeyCode::{self, *}, }, }; +mod cli; pub use self::cli::*; +mod model; pub use self::model::*; +mod view; pub use self::view::*; +mod keys; pub use self::keys::*; +mod audio; pub use self::audio::*; use std::fmt::Write; -use clap::{self, Parser, Subcommand}; -#[derive(Debug, Parser)] -#[command(version, about, long_about = None)] -pub struct TekCli { - /// Which app to initialize - #[command(subcommand)] mode: TekMode, - /// Name of JACK client - #[arg(short='n', long)] name: Option, - /// Whether to attempt to become transport master - #[arg(short='S', long, default_value_t = false)] sync_lead: bool, - /// Whether to sync to external transport master - #[arg(short='s', long, default_value_t = true)] sync_follow: bool, - /// Initial tempo in beats per minute - #[arg(short='b', long, default_value = None)] bpm: Option, - /// Whether to include a transport toolbar (default: true) - #[arg(short='t', long, default_value_t = true)] show_clock: bool, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='I', long)] midi_from: Vec, - /// MIDI outs to connect to (multiple instances accepted) - #[arg(short='i', long)] midi_from_re: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='O', long)] midi_to: Vec, - /// MIDI ins to connect to (multiple instances accepted) - #[arg(short='o', long)] midi_to_re: Vec, - /// Audio outs to connect to left input - #[arg(short='l', long)] left_from: Vec, - /// Audio outs to connect to right input - #[arg(short='r', long)] right_from: Vec, - /// Audio ins to connect from left output - #[arg(short='L', long)] left_to: Vec, - /// Audio ins to connect from right output - #[arg(short='R', long)] right_to: Vec, -} -#[derive(Debug, Clone, Subcommand)] pub enum TekMode { - /// A standalone transport clock. - Clock, - /// A MIDI sequencer. - Sequencer, - /// A MIDI-controlled audio sampler. - Sampler, - /// Sequencer and sampler together.12 - Groovebox, - /// Multi-track MIDI sequencer. - Arranger { - /// Number of scenes - #[arg(short = 'y', long, default_value_t = 1)] scenes: usize, - /// Number of tracks - #[arg(short = 'x', long, default_value_t = 1)] tracks: usize, - /// Width of tracks - #[arg(short = 'w', long, default_value_t = 9)] track_width: usize, - }, - /// TODO: A MIDI-controlled audio mixer - Mixer, - /// TODO: A customizable channel strip - Track, - /// TODO: An audio plugin host - Plugin, -} -impl TekCli { - pub fn run (&self) -> Usually<()> { - let name = self.name.as_ref().map_or("tek", |x|x.as_str()); - //let color = ItemPalette::random(); - let jack = JackConnection::new(name)?; - let engine = Tui::new()?; - let empty = &[] as &[&str]; - let midi_froms = PortConnection::collect(&self.midi_from, &self.midi_from_re, empty); - let midi_tos = PortConnection::collect(&self.midi_to, &self.midi_to_re, empty); - let left_froms = PortConnection::collect(&self.left_from, empty, empty); - let left_tos = PortConnection::collect(&self.left_to, empty, empty); - let right_froms = PortConnection::collect(&self.right_from, empty, empty); - let right_tos = PortConnection::collect(&self.right_to, empty, empty); - let audio_froms = &[left_froms.as_slice(), right_froms.as_slice()]; - let audio_tos = &[left_tos.as_slice(), right_tos.as_slice() ]; - engine.run(&jack.activate_with(|jack|match self.mode { - TekMode::Clock => Tek::new_clock( - jack, self.bpm, self.sync_lead, self.sync_follow, - &midi_froms, &midi_tos), - TekMode::Sequencer => Tek::new_sequencer( - jack, self.bpm, self.sync_lead, self.sync_follow, - &midi_froms, &midi_tos), - TekMode::Groovebox => Tek::new_groovebox( - jack, self.bpm, self.sync_lead, self.sync_follow, - &midi_froms, &midi_tos, - &audio_froms, &audio_tos), - TekMode::Arranger { scenes, tracks, track_width, .. } => Tek::new_arranger( - jack, self.bpm, self.sync_lead, self.sync_follow, - &midi_froms, &midi_tos, - &audio_froms, &audio_tos, - scenes, tracks, track_width), - _ => todo!() - })?) - } -} -#[derive(Default, Debug)] pub struct Tek { - /// Must not be dropped for the duration of the process - pub jack: Arc>, - /// Source of time - pub clock: Clock, - /// Theme - pub color: ItemPalette, - pub pool: Option, - pub editor: Option, - pub player: Option, - pub sampler: Option, - pub midi_buf: Vec>>, - pub midi_ins: Vec>, - pub midi_outs: Vec>, - pub audio_ins: Vec>, - pub audio_outs: Vec>, - pub note_buf: Vec, - pub tracks: Vec, - pub scenes: Vec, - pub selected: Selection, - pub splits: Vec, - pub size: Measure, - pub perf: PerfModel, - pub editing: AtomicBool, - pub history: Vec, - - /// View definition - pub view: SourceIter<'static>, - // Input definitions - pub keys: SourceIter<'static>, - pub keys_clip: SourceIter<'static>, - pub keys_track: SourceIter<'static>, - pub keys_scene: SourceIter<'static>, - pub keys_mix: SourceIter<'static>, - - pub fmtd_beat: Arc>, - pub fmtd_time: Arc>, - pub fmtd_bpm: Arc>, - pub fmtd_sr: Arc>, - pub fmtd_buf: Arc>, - pub fmtd_lat: Arc>, - pub fmtd_stop: Arc, -} -has_size!(|self: Tek|&self.size); -has_clock!(|self: Tek|self.clock); -has_clips!(|self: Tek|self.pool.as_ref().expect("no clip pool").clips); -has_jack!(|self: Tek|&self.jack); -has_sampler!(|self: Tek|{ - sampler = self.sampler; - index = self.editor.as_ref().map(|e|e.note_pos()).unwrap_or(0); }); -has_editor!(|self: Tek|{ - editor = self.editor; - editor_w = { - let size = self.size.w(); - let editor = self.editor.as_ref().expect("missing editor"); - let time_len = editor.time_len().get(); - let time_zoom = editor.time_zoom().get().max(1); - (5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) - }; - editor_h = 15; - is_editing = self.editing.load(Relaxed); }); -view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); { - ":editor" => (&self.editor).boxed(), - ":pool" => self.view_pool().boxed(), - ":sample" => self.view_sample(self.is_editing()).boxed(), - ":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(), - ":status" => self.view_editor().boxed(), - ":toolbar" => self.view_clock().boxed(), - ":tracks" => self.view_tracks().boxed(), - ":inputs" => self.view_inputs().boxed(), - ":outputs" => self.view_outputs().boxed(), - ":scenes" => self.view_scenes().boxed(), - ":scene-add" => Fill::x(Align::x(Fixed::x(23, button(" C-a ", format!(" add scene ({}/{})", - self.selected().scene().unwrap_or(0), - self.scenes().len()))))).boxed(), -}); -provide_num!(usize: |self: Tek| { - ":scene" => self.selected.scene().unwrap_or(0), - ":scene-next" => (self.selected.scene().unwrap_or(0) + 1).min(self.scenes.len()), - ":scene-prev" => self.selected.scene().unwrap_or(0).saturating_sub(1), - ":track" => self.selected.track().unwrap_or(0), - ":track-next" => (self.selected.track().unwrap_or(0) + 1).min(self.tracks.len()), - ":track-prev" => self.selected.track().unwrap_or(0).saturating_sub(1) }); -provide!(Color: |self: Tek| {}); -provide!(Selection: |self: Tek| {}); -provide!(Arc>: |self: Tek| {}); -provide!(Option>>: |self: Tek| {}); -provide_bool!(bool: |self: Tek| {}); -provide_num!(isize: |self: Tek| {}); -provide_num!(u16: |self: Tek| { - ":sidebar-w" => self.sidebar_w(), - ":sample-h" => if self.is_editing() { 0 } else { 5 }, - ":samples-w" => if self.is_editing() { 4 } else { 11 }, - ":samples-y" => if self.is_editing() { 1 } else { 0 }, - ":pool-w" => if self.is_editing() { 5 } else { - let w = self.size.w(); - if w > 60 { 20 } else if w > 40 { 15 } else { 10 } - } -}); -impl Tek { - fn new_clock ( - jack: &Arc>, - bpm: Option, sync_lead: bool, sync_follow: bool, - midi_froms: &[PortConnection], midi_tos: &[PortConnection], - ) -> Usually { - let tek = Self { - view: SourceIter(include_str!("./view_transport.edn")), - jack: jack.clone(), - color: ItemPalette::random(), - clock: Clock::new(jack, bpm), - midi_ins: vec![JackPort::::new(jack, "GlobalI", midi_froms)?], - midi_outs: vec![JackPort::::new(jack, "GlobalO", midi_tos)?], - keys: SourceIter(KEYS_APP), - keys_clip: SourceIter(KEYS_CLIP), - keys_track: SourceIter(KEYS_TRACK), - keys_scene: SourceIter(KEYS_SCENE), - keys_mix: SourceIter(KEYS_MIX), - fmtd_beat: Arc::new(RwLock::new(String::with_capacity(16))), - fmtd_time: Arc::new(RwLock::new(String::with_capacity(16))), - fmtd_bpm: Arc::new(RwLock::new(String::with_capacity(16))), - fmtd_sr: Arc::new(RwLock::new(String::with_capacity(16))), - fmtd_buf: Arc::new(RwLock::new(String::with_capacity(16))), - fmtd_lat: Arc::new(RwLock::new(String::with_capacity(16))), - fmtd_stop: "⏹".into(), - ..Default::default() - }; - tek.sync_lead(sync_lead); - tek.sync_follow(sync_follow); - Ok(tek) - } - fn new_sequencer ( - jack: &Arc>, - bpm: Option, sync_lead: bool, sync_follow: bool, - midi_froms: &[PortConnection], midi_tos: &[PortConnection], - ) -> Usually { - let clip = MidiClip::new("Clip", true, 384usize, None, Some(ItemColor::random().into())); - let clip = Arc::new(RwLock::new(clip)); - Ok(Self { - view: SourceIter(include_str!("./view_sequencer.edn")), - pool: Some((&clip).into()), - editor: Some((&clip).into()), - editing: false.into(), - midi_buf: vec![vec![];65536], - player: Some(MidiPlayer::new(&jack, "sequencer", Some(&clip), &midi_froms, &midi_tos)?), - ..Self::new_clock(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)? - }) - } - fn new_groovebox ( - jack: &Arc>, - bpm: Option, sync_lead: bool, sync_follow: bool, - midi_froms: &[PortConnection], midi_tos: &[PortConnection], - audio_froms: &[&[PortConnection];2], audio_tos: &[&[PortConnection];2], - ) -> Usually { - let app = Self { - view: SourceIter(include_str!("./view_groovebox.edn")), - sampler: Some(Sampler::new(jack, &"sampler", midi_froms, audio_froms, audio_tos)?), - ..Self::new_sequencer(jack, bpm, sync_lead, sync_follow, midi_froms, midi_tos)? - }; - if let Some(sampler) = app.sampler.as_ref().unwrap().midi_in.as_ref() { - jack.connect_ports(&app.player.as_ref().unwrap().midi_outs[0].port, &sampler.port)?; - } - Ok(app) - } - fn new_arranger ( - jack: &Arc>, - bpm: Option, sync_lead: bool, sync_follow: bool, - midi_froms: &[PortConnection], midi_tos: &[PortConnection], - audio_froms: &[&[PortConnection];2], audio_tos: &[&[PortConnection];2], - scenes: usize, tracks: usize, track_width: usize, - ) -> Usually { - let mut arranger = Self { - view: SourceIter(include_str!("./view_arranger.edn")), - ..Self::new_groovebox( - jack, bpm, sync_lead, sync_follow, - midi_froms, midi_tos, audio_froms, audio_tos, - )? - }; - arranger.scenes_add(scenes); - arranger.tracks_add(tracks, track_width, midi_froms, midi_tos); - arranger.selected = Selection::Clip(1, 1); - Ok(arranger) - } - fn scenes_add (&mut self, n: usize) -> Usually<()> { - let scene_color_1 = ItemColor::random(); - let scene_color_2 = ItemColor::random(); - for i in 0..n { - let _ = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } - fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<(usize, &mut Scene)> - { - let scene = Scene { - name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), - clips: vec![None;self.tracks().len()], - color: color.unwrap_or_else(ItemPalette::random), - }; - self.scenes_mut().push(scene); - let index = self.scenes().len() - 1; - Ok((index, &mut self.scenes_mut()[index])) - } - fn scene_default_name (&self) -> Arc { - format!("Sc{:3>}", self.scenes().len() + 1).into() - } - fn tracks_add ( - &mut self, count: usize, width: usize, - midi_from: &[PortConnection], midi_to: &[PortConnection], - ) -> Usually<()> { - let jack = self.jack().clone(); - let track_color_1 = ItemColor::random(); - let track_color_2 = ItemColor::random(); - for i in 0..count { - let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); - let mut track = self.track_add(None, Some(color), midi_from, midi_to)?.1; - track.width = width; - } - Ok(()) - } - fn track_add ( - &mut self, name: Option<&str>, color: Option, - midi_from: &[PortConnection], midi_to: &[PortConnection], - ) -> Usually<(usize, &mut Track)> { - let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); - let mut track = Track { - width: (name.len() + 2).max(9), - color: color.unwrap_or_else(ItemPalette::random), - player: MidiPlayer::from(self.clock()), - name, - ..Default::default() - }; - track.player.midi_ins.push(JackPort::::new( - &self.jack, &format!("{}I", &track.name), midi_from - )?); - track.player.midi_outs.push(JackPort::::new( - &self.jack, &format!("{}O", &track.name), midi_to - )?); - self.tracks_mut().push(track); - let len = self.tracks().len(); - let index = len - 1; - for scene in self.scenes_mut().iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok((index, &mut self.tracks_mut()[index])) - } - fn sync_lead (&self, enable: bool) -> Usually<()> { - if enable { - self.jack.read().unwrap().client().register_timebase_callback(false, |mut state|{ - let clock = self.clock(); - clock.playhead.update_from_sample(state.position.frame() as f64); - state.position.bbt = Some(clock.bbt()); - state.position - })?; - } - Ok(()) - } - fn sync_follow (&self, enable: bool) -> Usually<()> { - // TODO: sync follow - Ok(()) - } - fn view_clock (&self) -> impl Content + use<'_> { - Outer(false, Style::default().fg(Tui::g(0))).enclose(row!( - self.view_engine_stats(), " ", - self.view_play_pause(), " ", - self.view_beat_stats(), - )) - } - fn view_beat_stats (&self) -> impl Content + use<'_> { - let compact = self.size.w() > 80; - let clock = self.clock(); - let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); - let mut fmtd_beat = self.fmtd_beat.write().unwrap(); - let mut fmtd_time = self.fmtd_time.write().unwrap(); - let mut fmtd_bpm = self.fmtd_bpm.write().unwrap(); - fmtd_beat.clear(); - fmtd_time.clear(); - fmtd_bpm.clear(); - if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { - clock.timebase.format_beats_1_to(&mut*fmtd_beat, clock.timebase.usecs_to_pulse(now)); - write!(&mut fmtd_time, "{:.3}s", now/1000000.); - write!(&mut fmtd_bpm, "{:.3}", clock.timebase.bpm.get()); - } else { - write!(&mut fmtd_beat, "-.-.--"); - write!(&mut fmtd_time, "-.---s"); - write!(&mut fmtd_bpm, "---.---"); - } - let theme = ItemPalette::G[128]; - Thunk::new(move||Either::new(compact, - row!(FieldH(theme, "BPM", self.fmtd_bpm.clone()), - FieldH(theme, "Beat", self.fmtd_beat.clone()), - FieldH(theme, "Time", self.fmtd_time.clone())), - row!(FieldV(theme, "BPM", self.fmtd_bpm.clone()), - FieldV(theme, "Beat", self.fmtd_beat.clone()), - FieldV(theme, "Time", self.fmtd_time.clone())))) - } - fn view_engine_stats (&self) -> impl Content + use<'_> { - let compact = self.size.w() > 80; - let clock = self.clock(); - let rate = clock.timebase.sr.get(); - let chunk = clock.chunk.load(Relaxed); - let mut fmtd_sr = self.fmtd_sr.write().unwrap(); - let mut fmtd_buf = self.fmtd_buf.write().unwrap(); - let mut fmtd_lat = self.fmtd_lat.write().unwrap(); - fmtd_sr.clear(); - write!(&mut fmtd_sr, "{}", if compact {format!("{:.1}kHz", rate / 1000.)} else {format!("{:.0}Hz", rate)}); - fmtd_buf.clear(); - write!(&mut fmtd_buf, "{chunk}"); - fmtd_lat.clear(); - write!(&mut fmtd_lat, "{:.1}ms", chunk as f64 / rate * 1000.); - let theme = ItemPalette::G[128]; - Either::new(compact, - row!(FieldH(theme, "SR", self.fmtd_sr.clone()), - FieldH(theme, "Buf", self.fmtd_buf.clone()), - FieldH(theme, "Lat", self.fmtd_lat.clone())), - row!(FieldV(theme, "SR", self.fmtd_sr.clone()), - FieldV(theme, "Buf", self.fmtd_buf.clone()), - FieldV(theme, "Lat", self.fmtd_lat.clone()))) - } - fn view_meter <'a> (&'a self, label: &'a str, value: f32) -> impl Content + 'a { - col!( - FieldH(ItemPalette::G[128], label, format!("{:>+9.3}", value)), - Fixed::xy(if value >= 0.0 { 13 } - else if value >= -1.0 { 12 } - else if value >= -2.0 { 11 } - else if value >= -3.0 { 10 } - else if value >= -4.0 { 9 } - else if value >= -6.0 { 8 } - else if value >= -9.0 { 7 } - else if value >= -12.0 { 6 } - else if value >= -15.0 { 5 } - else if value >= -20.0 { 4 } - else if value >= -25.0 { 3 } - else if value >= -30.0 { 2 } - else if value >= -40.0 { 1 } - else { 0 }, 1, Tui::bg(if value >= 0.0 { Red } - else if value >= -3.0 { Yellow } - else { Green }, ()))) - } - fn view_meters (&self, values: &[f32;2]) -> impl Content + use<'_> { - col!( - format!("L/{:>+9.3}", values[0]), - format!("R/{:>+9.3}", values[1]), - ) - } - fn view_play_pause (&self) -> impl Content + use<'_> { - let playing = self.clock.is_rolling(); - let compact = self.is_editing(); - Tui::bg( - if playing{Rgb(0,128,0)}else{Rgb(128,64,0)}, - Either::new(compact, - Thunk::new(move||Fixed::x(9, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), " PLAYING "), - Tui::fg(Rgb(255, 128, 0), " STOPPED ")))), - Thunk::new(move||Fixed::x(5, Either::new(playing, - Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), - Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))))) - } - fn view_editor (&self) -> impl Content + use<'_> { - self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())) - } - fn view_pool (&self) -> impl Content + use<'_> { - self.pool.as_ref().map(|pool|PoolView(self.is_editing(), pool)) - } - fn view_scene_add (&self) -> impl Content + use<'_> { - button(" C-a ", format!(" add scene ({}/{})", - self.selected().scene().unwrap_or(0), - self.scenes().len())) - } - fn view_scenes (&self) -> impl Content + use<'_> { - Outer(false, Style::default().fg(Tui::g(0))).enclose_bg({ - let w = self.w(); - let h = self.size.h().saturating_sub(6 + self.midi_ins.len() + self.midi_outs.len()) as u16; - self.view_row(w, h, self.scene_header(), self.clip_columns()) - }) - } - fn view_tracks (&self) -> impl Content + use<'_> { - let h = 1; - self.view_row(self.w(), 1, self.track_header(), self.track_cells()) - } - fn view_inputs (&self) -> impl Content + use<'_> { - let h = 1 + self.midi_ins.len() as u16; - self.view_row(self.w(), h, self.input_header(), self.input_cells()) - } - fn view_outputs (&self) -> impl Content + use<'_> { - let h = 1 + self.midi_outs.len(); - self.view_row(self.w(), h as u16, - self.output_header(), - self.output_cells()) - } - fn view_row <'a> ( - &'a self, w: u16, h: u16, a: impl Content + 'a, b: impl Content + 'a - ) -> impl Content + 'a { - Fixed::y(h, Bsp::e( - Fixed::x(self.sidebar_w() as u16, a), - Fill::x(Align::c(Fixed::xy(w, h, b))) - )) - } - fn w (&self) -> u16 { - self.tracks_sizes(self.is_editing(), self.editor_w()) - .last() - .map(|x|x.3 as u16) - .unwrap_or(0) - } - fn sidebar_w (&self) -> u16 { - let w = self.size.w(); - let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; - let w = if self.is_editing() { 8 } else { w }; - w - } - fn clip (&self) -> Option>> { - self.scene()?.clips.get(self.selected().track()?)?.clone() - } - fn toggle_loop (&mut self) { - if let Some(clip) = self.clip() { - clip.write().unwrap().toggle_loop() - } - } - fn track_del (&mut self, index: usize) { - self.tracks_mut().remove(index); - for scene in self.scenes_mut().iter_mut() { - scene.clips.remove(index); - } - } - fn clip_columns <'a> (&'a self) -> ThunkBox<'a, TuiOut> { - let editing = self.is_editing(); - let tracks = move||self.tracks_sizes(editing, self.editor_w()); - let scenes = move||self.scenes_sizes(editing, 2, 15); - let selected_track = self.selected().track(); - let selected_scene = self.selected().scene(); - let border = |x|Outer(false, Style::default().fg(Tui::g(0))).enclose(x); - let area = self.size.w().saturating_sub(self.sidebar_w() as usize * 2); - (move||Align::c(Map::new(tracks, { - let last_color = Arc::new(RwLock::new(ItemPalette::default())); - move|(_, track, x1, x2), t| { - let last_color = last_color.clone(); - let same_track = selected_track == Some(t+1); - let w = (x2 - x1) as u16; - map_east(x1 as u16, w, border(Map::new(scenes, move|(_, scene, y1, y2), s|{ - let last_color = last_color.clone(); - Either(x2 >= area, (), Thunk::new(move||{ - let last_color = last_color.clone(); - let mut fg = Tui::g(64); - let mut bg = ItemPalette::G[32]; - if let Some(clip) = &scene.clips[t] { - let clip = clip.read().unwrap(); - fg = clip.color.lightest.rgb; - bg = clip.color - }; - - // weird offsetting: - let selected = same_track && selected_scene == Some(s+1); - let neighbor = same_track && selected_scene == Some(s); - let active = editing && selected; - - //let top = if neighbor { None } else { Some(last_color.read().unwrap().base.rgb) }; - let top = if s == 0 { - Some(Reset) - } else if neighbor { - Some(last_color.read().unwrap().light.rgb) - } else { - Some(last_color.read().unwrap().base.rgb) - }; - let mid = if selected { bg.light } else { bg.base }.rgb; - let low = Some(Reset); - let h = (1 + y2 - y1) as u16; - *last_color.write().unwrap() = bg; - let tab = " Tab "; - let name = if active { - self.editor.as_ref() - .map(|e|e.clip().as_ref().map(|c|c.clone())) - .flatten() - .map(|c|c.read().unwrap().name.clone()) - .unwrap_or_else(||"".into()) - } else { - "edit".into() - }; - let label = move||{ - let clip = scene.clips[t].clone(); - let icon = " ⏹ "; - let name = clip.map(|c|c.read().unwrap().name.clone()); - Align::nw(Tui::fg(fg, Bsp::e(icon, Bsp::e(Tui::bold(true, name), " ")))) - }; - map_south(y1 as u16, h, Push::y(1, Fixed::y(h, Either::new(active, - Thunk::new(move||Bsp::a( - Fill::xy(Align::nw(button(tab, label()))), - &self.editor)), - Thunk::new(move||Bsp::a( - When::new(selected, Fill::y(Align::n(button(tab, "edit")))), - phat_sel_3( - selected, - Fill::xy(label()), - Fill::xy(label()), - top, mid, low - ) - )), - )))) - - })) - }))).boxed() - } - })).boxed()).into() - } - fn activate (&mut self) -> Usually<()> { - let selected = self.selected().clone(); - match selected { - Selection::Scene(s) => { - let mut clips = vec![]; - for (t, _) in self.tracks().iter().enumerate() { - clips.push(self.scenes()[s].clips[t].clone()); - } - for (t, track) in self.tracks_mut().iter_mut().enumerate() { - if track.player.play_clip.is_some() || clips[t].is_some() { - track.player.enqueue_next(clips[t].as_ref()); - } - } - if self.clock().is_stopped() { - self.clock().play_from(Some(0))?; - } - }, - Selection::Clip(t, s) => { - let clip = self.scenes()[s].clips[t].clone(); - self.tracks_mut()[t].player.enqueue_next(clip.as_ref()); - }, - _ => {} - } - Ok(()) - } - fn input_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> { - let fg = Tui::g(224); - let bg = Tui::g(64); - (move||Bsp::s(Fill::x(Align::w(self.button(" I ", format!(" midi ins ({})", self.midi_ins().len())))), self.midi_ins().get(0).map(|inp|Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(inp.name.clone())))), - inp.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, - Tui::fg_bg(fg, bg, connect.info()))))), - ))).boxed()).into() - } - fn output_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> { - let fg = Tui::g(224); - let bg = Tui::g(64); - (move||Bsp::s(Fill::x(Align::w(self.button(" O ", format!(" midi outs ({}) ", self.midi_outs().len())))), self.midi_outs().get(0).map(|out|Bsp::s( - Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))), - out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, - Tui::fg_bg(fg, bg, connect.info()))))), - ))).boxed()).into() - } - fn track_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> { - let add_track = ||self.button(" C-t ", format!(" add track ({}/{})", - self.selected.track().unwrap_or(0), - self.tracks().len())); - (move||Tui::bg(Tui::g(32), Fill::x(Align::w(add_track()))).boxed()).into() - } - fn button <'a> ( - &'a self, key: impl Content + 'a, label: impl Content + 'a - ) -> impl Content + 'a { - let compact = !self.is_editing(); - Tui::bold(true, Bsp::e( - Margin::x(1, Tui::fg_bg(Tui::g(0), Tui::orange(), key)), - When::new(compact, Margin::x(1, Tui::fg_bg(Tui::g(255), Tui::g(96), label))), - )) - } -} -const KEYS_APP: &str = include_str!("keys.edn"); -const KEYS_CLIP: &str = include_str!("keys_clip.edn"); -const KEYS_TRACK: &str = include_str!("keys_track.edn"); -const KEYS_SCENE: &str = include_str!("keys_scene.edn"); -const KEYS_MIX: &str = include_str!("keys_mix.edn"); -handle!(TuiIn: |self: Tek, input|Ok({ - // If editing, editor keys take priority - if self.is_editing() { - if self.editor.handle(input)? == Some(true) { - return Ok(Some(true)) - } - } - // Handle from root keymap - if let Some(command) = self.keys.command::<_, TekCommand, _>(self, input) { - if let Some(undo) = command.execute(self)? { self.history.push(undo); } - return Ok(Some(true)) - } - // Handle from selection-dependent keymaps - if let Some(command) = match self.selected() { - Selection::Clip(_, _) => self.keys_clip, - Selection::Track(_) => self.keys_track, - Selection::Scene(_) => self.keys_scene, - Selection::Mix => self.keys_mix, - }.command::<_, TekCommand, _>(self, input) { - if let Some(undo) = command.execute(self)? { self.history.push(undo); } - return Ok(Some(true)) - } - None -})); -#[derive(Clone, Debug)] pub enum TekCommand { - Clip(ClipCommand), - Clock(ClockCommand), - Color(ItemPalette), - Edit(Option), - Editor(MidiEditCommand), - Enqueue(Option>>), - History(isize), - Pool(PoolCommand), - Sampler(SamplerCommand), - Scene(SceneCommand), - Select(Selection), - StopAll, - Track(TrackCommand), - Zoom(Option), -} -atom_command!(TekCommand: |app: Tek| { - ("stop" [] Self::StopAll) - ("undo" [d: usize] Self::History(-(d.unwrap_or(0)as isize))) - ("redo" [d: usize] Self::History(d.unwrap_or(0) as isize)) - ("zoom" [z: usize] Self::Zoom(z)) - ("edit" [] Self::Edit(None)) - ("edit" [c: bool] Self::Edit(c)) - ("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default())) - ("enqueue" [c: Arc>] Self::Enqueue(c)) - ("select" [t: usize, s: usize] match (t.expect("no track"), s.expect("no scene")) { - (0, 0) => Self::Select(Selection::Mix), - (t, 0) => Self::Select(Selection::Track(t)), - (0, s) => Self::Select(Selection::Scene(s)), - (t, s) => Self::Select(Selection::Clip(t, s)), - }) - ("clip" [,..a] Self::Clip( - ClipCommand::try_from_expr(app, a).expect("invalid command"))) - ("clock" [,..a] Self::Clock( - ClockCommand::try_from_expr(app.clock(), a).expect("invalid command"))) - ("editor" [,..a] Self::Editor( - MidiEditCommand::try_from_expr(app.editor.as_ref().expect("no editor"), a).expect("invalid command"))) - ("pool" [,..a] Self::Pool( - PoolCommand::try_from_expr(app.pool.as_ref().expect("no pool"), a).expect("invalid command"))) - ("sampler" [,..a] Self::Sampler( - SamplerCommand::try_from_expr(app.sampler.as_ref().expect("no sampler"), a).expect("invalid command"))) - ("scene" [,..a] Self::Scene( - SceneCommand::try_from_expr(app, a).expect("invalid command"))) - ("track" [,..a] Self::Track( - TrackCommand::try_from_expr(app, a).expect("invalid command"))) -}); -command!(|self: TekCommand, app: Tek|match self { - Self::Zoom(_) => { println!("\n\rtodo: global zoom"); None }, - Self::History(delta) => { println!("\n\rtodo: undo/redo"); None }, - Self::Select(s) => { - app.selected = s; - // autoedit: load focused clip in editor. - if let Some(ref mut editor) = app.editor { - editor.set_clip(match app.selected { - Selection::Clip(t, s) if let Some(Some(Some(clip))) = app - .scenes.get(s).map(|s|s.clips.get(t)) => Some(clip), - _ => None - }); - } - None - }, - Self::Edit(value) => { - if let Some(value) = value { - if app.is_editing() != value { - app.editing.store(value, Relaxed); - } - } else { - app.editing.store(!app.is_editing(), Relaxed); - }; - // autocreate: create new clip from pool when entering empty cell - if let Some(ref pool) = app.pool { - if app.is_editing() { - if let Selection::Clip(t, s) = app.selected { - if let Some(scene) = app.scenes.get_mut(s.saturating_sub(1)) { - if let Some(slot) = scene.clips.get_mut(t.saturating_sub(1)) { - if slot.is_none() { - let (index, mut clip) = pool.add_new_clip(); - // autocolor: new clip colors from scene and track color - clip.write().unwrap().color = ItemColor::random_near( - app.tracks[t.saturating_sub(1)].color.base.mix( - scene.color.base, - 0.5 - ), - 0.2 - ).into(); - if let Some(ref mut editor) = app.editor { - editor.set_clip(Some(&clip)); - } - *slot = Some(clip); - } - } - } - } - } - } - None - }, - Self::Clock(cmd) => cmd.delegate(app, Self::Clock)?, - Self::Scene(cmd) => cmd.delegate(app, Self::Scene)?, - Self::Track(cmd) => cmd.delegate(app, Self::Track)?, - Self::Clip(cmd) => cmd.delegate(app, Self::Clip)?, - Self::Editor(cmd) => app.editor.as_mut() - .map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(), - Self::Sampler(cmd) => app.sampler.as_mut() - .map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(), - Self::Enqueue(clip) => app.player.as_mut() - .map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(), - Self::Color(palette) => { - use Selection::*; - Some(Self::Color(match app.selected { - Mix => { - let old = app.color; - app.color = palette; - old - }, - Track(t) => { - let t = t.saturating_sub(1); - let old = app.tracks[t].color; - app.tracks[t].color = palette; - old - } - Scene(s) => { - let s = s.saturating_sub(1); - let old = app.scenes[s].color; - app.scenes[s].color = palette; - old - } - Clip(t, s) => { - let t = t.saturating_sub(1); - let s = s.saturating_sub(1); - if let Some(ref clip) = app.scenes[s].clips[t] { - let mut clip = clip.write().unwrap(); - let old = clip.color; - clip.color = palette; - old - } else { - return Ok(None) - } - } - })) - }, - Self::StopAll => { - for track in 0..app.tracks.len(){app.tracks[track].player.enqueue_next(None);} - None - }, - Self::Pool(cmd) => if let Some(pool) = app.pool.as_mut() { - let undo = cmd.clone().delegate(pool, Self::Pool)?; - if let Some(editor) = app.editor.as_mut() { - match cmd { - // autoselect: automatically load selected clip in editor - // autocolor: update color in all places simultaneously - PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) => - editor.set_clip(pool.clip().as_ref()), - _ => {} - } - }; - undo - } else { - None - }, -}); -/// Represents the current user selection in the arranger -#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { - /// The whole mix is selected - #[default] Mix, - /// A track is selected. - Track(usize), - /// A scene is selected. - Scene(usize), - /// A clip (track × scene) is selected. - Clip(usize, usize), -} -/// Focus identification methods -impl Selection { - fn is_mix (&self) -> bool { matches!(self, Self::Mix) } - fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } - fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } - fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } - fn track (&self) -> Option { - use Selection::*; - match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None } - } - fn scene (&self) -> Option { - use Selection::*; - match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None } - } - fn description (&self, tracks: &[Track], scenes: &[Scene]) -> Arc { - format!("Selected: {}", match self { - Self::Mix => "Everything".to_string(), - Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) - .unwrap_or_else(||"T??".into()), - Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) - .unwrap_or_else(||"S??".into()), - Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { - (Some(_), Some(scene)) => match scene.clip(*t) { - Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), - None => format!("T{t} S{s}: Empty") - }, - _ => format!("T{t} S{s}: Empty"), - } - }).into() - } -} -impl HasSelection for Tek { - fn selected (&self) -> &Selection { &self.selected } - fn selected_mut (&mut self) -> &mut Selection { &mut self.selected } -} -trait HasSelection { - fn selected (&self) -> &Selection; - fn selected_mut (&mut self) -> &mut Selection; -} -#[derive(Debug, Default)] pub struct Track { - /// Name of track - pub name: Arc, - /// Preferred width of track column - pub width: usize, - /// Identifying color of track - pub color: ItemPalette, - /// MIDI player state - pub player: MidiPlayer, - /// Device chain - pub devices: Vec>, - /// Inputs of 1st device - pub audio_ins: Vec>, - /// Outputs of last device - pub audio_outs: Vec>, -} -has_clock!(|self: Track|self.player.clock); -has_player!(|self: Track|self.player); -impl Track { - const MIN_WIDTH: usize = 9; - fn width_inc (&mut self) { self.width += 1; } - fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } -} -#[derive(Clone, Debug)] pub enum TrackCommand { - Add, - Del(usize), - Stop(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), - SetColor(usize, ItemPalette), -} -atom_command!(TrackCommand: |app: Tek| { - ("add" [] Self::Add) - ("size" [a: usize] Self::SetSize(a.unwrap())) - ("zoom" [a: usize] Self::SetZoom(a.unwrap())) - ("color" [a: usize] Self::SetColor(a.unwrap().saturating_sub(1), ItemPalette::random())) - ("del" [a: usize] Self::Del(a.unwrap().saturating_sub(1))) - ("stop" [a: usize] Self::Stop(a.unwrap().saturating_sub(1))) - ("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap())) -}); -command!(|self: TrackCommand, app: Tek|match self { - Self::Add => { - use Selection::*; - let index = app.track_add(None, None, &[], &[])?.0 + 1; - app.selected = match app.selected { - Track(t) => Track(index), - Clip(t, s) => Clip(index, s), - _ => app.selected - }; - Some(Self::Del(index)) - }, - Self::Del(index) => { app.track_del(index); None }, - Self::Stop(track) => { app.tracks[track].player.enqueue_next(None); None }, - Self::SetColor(index, color) => { - let old = app.tracks[index].color; - app.tracks[index].color = color; - Some(Self::SetColor(index, old)) - }, - _ => None -}); -impl HasTracks for Tek { - fn midi_ins (&self) -> &Vec> { &self.midi_ins } - fn midi_outs (&self) -> &Vec> { &self.midi_outs } - fn tracks (&self) -> &Vec { &self.tracks } - fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } -} -macro_rules! per_track { - (|$self:ident,$track:ident|$content:expr) => {{ - let tracks = ||$self.tracks_sizes($self.is_editing(), $self.editor_w()); - Box::new(move||Align::x(Map::new(tracks, move|(_, $track, x1, x2), i| { - let width = (x2 - x1) as u16; - let content = Fixed::y(1, $content); - let styled = Tui::fg_bg($track.color.lightest.rgb, $track.color.base.rgb, content); - map_east(x1 as u16, width, Fixed::x(width, styled)) - }))).into() - }} -} -trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { - fn midi_ins (&self) -> &Vec>; - fn midi_outs (&self) -> &Vec>; - fn tracks (&self) -> &Vec; - fn tracks_mut (&mut self) -> &mut Vec; - fn track_longest (&self) -> usize { - self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) - } - fn tracks_sizes <'a> (&'a self, editing: bool, bigger: usize) - -> impl Iterator + Send + Sync + 'a - { - let mut x = 0; - let active = match self.selected() { - Selection::Track(t) if editing => Some(t.saturating_sub(1)), - Selection::Clip(t, _) if editing => Some(t.saturating_sub(1)), - _ => None - }; - self.tracks().iter().enumerate().map(move |(index, track)|{ - let width = if Some(index) == active { bigger } else { track.width.max(8) }; - let data = (index, track, x, x + width); - x += width; - data - }) - } - fn track_next_name (&self) -> Arc { - format!("Track{:02}", self.tracks().len() + 1).into() - } - fn track (&self) -> Option<&Track> { - self.selected().track().and_then(|s|self.tracks().get(s)) - } - fn track_mut (&mut self) -> Option<&mut Track> { - self.selected().track().and_then(|s|self.tracks_mut().get_mut(s)) - } - fn track_cells <'a> (&'a self) -> ThunkBox<'a, TuiOut> { - let iter = ||self.tracks_sizes(self.is_editing(), self.editor_w()); - (move||Align::x(Map::new(iter, move|(_, track, x1, x2), i| { - let active = self.selected().track() == Some(i+1); - let name = &track.name; - let color = track.color; - let fg = color.lightest.rgb; - let bg = if active { color.light.rgb } else { color.base.rgb }; - let bfg = if active { Rgb(255,255,255) } else { Rgb(0,0,0) }; - let border = Style::default().fg(bfg).bg(bg); - let content = Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::nw(Bsp::e(" ", name))))); - Tui::bg(bg, map_east(x1 as u16, (x2 - x1) as u16, Outer(false, border).enclose(content))) - })).boxed()).into() - } - fn cell > (theme: ItemPalette, field: T) -> impl Content { - Tui::fg_bg(theme.lightest.rgb, theme.base.rgb, Fixed::y(1, field)) - } - fn input_cells <'a> (&'a self) -> ThunkBox<'a, TuiOut> { - let rec = false; - let mon = false; - per_track!(|self, track|row!( - Tui::fg_bg(if rec { White } else { track.color.light.rgb }, track.color.dark.rgb, "Rcrd"), - Tui::fg_bg(if rec { White } else { track.color.dark.rgb }, track.color.dark.rgb, "▐"), - Tui::fg_bg(if mon { White } else { track.color.light.rgb }, track.color.dark.rgb, "Mntr"), - )) - } - fn output_cells <'a> (&'a self) -> ThunkBox<'a, TuiOut> { - let mute = false; - let solo = false; - per_track!(|self, track|row!( - Tui::fg_bg(if mute { White } else { track.color.light.rgb }, track.color.dark.rgb, "Mute"), - Tui::fg_bg(if mute { White } else { track.color.dark.rgb }, track.color.dark.rgb, "▐"), - Tui::fg_bg(if solo { White } else { track.color.light.rgb }, track.color.dark.rgb, "Solo"), - )) - } -} -pub trait Device: Send + Sync + std::fmt::Debug {} -impl Device for Sampler {} -impl Device for Plugin {} -#[derive(Debug, Default)] pub struct Scene { - /// Name of scene - pub name: Arc, - /// Clips in scene, one per track - pub clips: Vec>>>, - /// Identifying color of scene - pub color: ItemPalette, -} -impl Scene { - /// Returns the pulse length of the longest clip in the scene - fn pulses (&self) -> usize { - self.clips.iter().fold(0, |a, p|{ - a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) - }) - } - /// Returns true if all clips in the scene are - /// currently playing on the given collection of tracks. - fn is_playing (&self, tracks: &[Track]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(c) => tracks - .get(track_index) - .map(|track|{ - if let Some((_, Some(clip))) = track.player().play_clip() { - *clip.read().unwrap() == *c.read().unwrap() - } else { - false - } - }) - .unwrap_or(false), - None => true - }) - } - fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } -} -#[derive(Clone, Debug)] pub enum SceneCommand { - Add, - Del(usize), - Swap(usize, usize), - SetSize(usize), - SetZoom(usize), - SetColor(usize, ItemPalette), - Enqueue(usize), -} -atom_command!(SceneCommand: |app: Tek| { - ("add" [] Self::Add) - ("del" [a: usize] Self::Del(0)) - ("zoom" [a: usize] Self::SetZoom(a.unwrap())) - ("color" [a: usize] Self::SetColor(a.unwrap().saturating_sub(1), ItemPalette::random())) - ("enqueue" [a: usize] Self::Enqueue(a.unwrap().saturating_sub(1))) - ("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap())) -}); -command!(|self: SceneCommand, app: Tek|match self { - Self::Add => { - use Selection::*; - let index = app.scene_add(None, None)?.0 + 1; - app.selected = match app.selected { - Scene(s) => Scene(index), - Clip(t, s) => Clip(t, index), - _ => app.selected - }; - Some(Self::Del(index)) - }, - Self::Del(index) => { app.scene_del(index); None }, - Self::SetColor(index, color) => { - let old = app.scenes[index].color; - app.scenes[index].color = color; - Some(Self::SetColor(index, old)) - }, - Self::Enqueue(scene) => { - for track in 0..app.tracks.len() { - app.tracks[track].player.enqueue_next(app.scenes[scene].clips[track].as_ref()); - } - None - }, - _ => None -}); -impl HasScenes for Tek { - fn scenes (&self) -> &Vec { &self.scenes } - fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } -} -trait HasScenes: HasSelection + HasEditor + Send + Sync { - fn scenes (&self) -> &Vec; - fn scenes_mut (&mut self) -> &mut Vec; - fn scene_longest (&self) -> usize { - self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) - } - fn scenes_sizes (&self, editing: bool, height: usize, larger: usize,) - -> impl Iterator + Send + Sync - { - let mut y = 0; - let (selected_track, selected_scene) = match self.selected() { - Selection::Clip(t, s) => (Some(t.saturating_sub(1)), Some(s.saturating_sub(1))), - _ => (None, None) - }; - self.scenes().iter().enumerate().map(move|(s, scene)|{ - let active = editing && selected_track.is_some() && selected_scene == Some(s); - let height = if active { larger } else { height }; - let data = (s, scene, y, y + height); - y += height; - data - }) - } - fn scene (&self) -> Option<&Scene> { - self.selected().scene().and_then(|s|self.scenes().get(s)) - } - fn scene_mut (&mut self) -> Option<&mut Scene> { - self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s)) - } - fn scene_del (&mut self, index: usize) { - self.selected().scene().and_then(|s|Some(self.scenes_mut().remove(index))); - } - fn scene_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> { - (move||{ - let last_color = Arc::new(RwLock::new(ItemPalette::G[0])); - let iter = ||self.scenes_sizes(self.is_editing(), 2, 15); - Map::new(iter, move|(_, scene, y1, y2), i| { - let cell = phat_sel_3( - self.selected().scene() == Some(i), - Tui::bold(true, Bsp::e("🭬", &scene.name)), - Tui::bold(true, Bsp::e("🭬", &scene.name)), - if i == 0 { Some(Reset) } - else if self.selected().scene() == Some(i) { None } - else { Some(last_color.read().unwrap().base.rgb) }, - if self.selected().scene() == Some(i+1) { scene.color.light } else { scene.color.base }.rgb, - Some(Reset) - ); - let h = (1 + y2 - y1) as u16; - *last_color.write().unwrap() = scene.color; - map_south(y1 as u16, h, Push::y(1, Fixed::y(h, - Outer(false, Style::default().fg(Tui::g(0))).enclose(cell)))) - }).boxed() - }).into() - } -} -#[derive(Clone, Debug)] pub enum ClipCommand { - Get(usize, usize), - Put(usize, usize, Option>>), - Enqueue(usize, usize), - Edit(Option>>), - SetLoop(usize, usize, bool), - SetColor(usize, usize, ItemPalette), -} -atom_command!(ClipCommand: |app: Tek| { - ("get" [a: usize ,b: usize] - Self::Get(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1))) - ("put" [a: usize, b: usize, c: Option>>] - Self::Put(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1), c.unwrap())) - ("enqueue" [a: usize, b: usize] - Self::Enqueue(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1))) - ("edit" [a: Option>>] - Self::Edit(a.unwrap())) - ("loop" [a: usize, b: usize, c: bool] - Self::SetLoop(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1), c.unwrap())) - ("color" [a: usize, b: usize] - Self::SetColor(a.unwrap().saturating_sub(1), b.unwrap().saturating_sub(1), ItemPalette::random())) -}); -command!(|self: ClipCommand, app: Tek|match self { - Self::Get(track, scene) => { todo!() }, - Self::Put(track, scene, clip) => { - let old = app.scenes[scene].clips[track].clone(); - app.scenes[scene].clips[track] = clip; - Some(Self::Put(track, scene, old)) - }, - Self::Enqueue(track, scene) => { - app.tracks[track].player.enqueue_next(app.scenes[scene].clips[track].as_ref()); - None - }, - _ => None -}); -audio!(|self: Tek, client, scope|{ - // Start profiling cycle - let t0 = self.perf.get_t0(); - // Update transport clock - self.clock().update_from_scope(scope).unwrap(); - // Collect MIDI input (TODO preallocate) - let midi_in = self.midi_ins.iter() - .map(|port|port.port.iter(scope) - .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) - .collect::>()) - .collect::>(); - // Update standalone MIDI sequencer - if let Some(player) = self.player.as_mut() { - if Control::Quit == PlayerAudio( - player, - &mut self.note_buf, - &mut self.midi_buf, - ).process(client, scope) { - return Control::Quit - } - } - // Update standalone sampler - if let Some(sampler) = self.sampler.as_mut() { - if Control::Quit == SamplerAudio(sampler).process(client, scope) { - return Control::Quit - } - //for port in midi_in.iter() { - //for message in port.iter() { - //match message { - //Ok(M - //} - //} - //} - } - // TODO move these to editor and sampler?: - for port in midi_in.iter() { - for event in port.iter() { - match event { - (time, Ok(LiveEvent::Midi {message, ..})) => match message { - MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => { - editor.set_note_pos(key.as_int() as usize); - }, - MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = ( - self.editor.as_ref(), - self.sampler.as_ref(), - ) => { - // TODO: give sampler its own cursor - if let Some(sample) = &sampler.mapped[editor.note_pos()] { - sample.write().unwrap().handle_cc(*controller, *value) - } - } - _ =>{} - }, - _ =>{} - } - } - } - // Update track sequencers - for track in self.tracks.iter_mut() { - if PlayerAudio( - track.player_mut(), &mut self.note_buf, &mut self.midi_buf - ).process(client, scope) == Control::Quit { - return Control::Quit - } - } - // End profiling cycle - self.perf.update(t0, scope); - Control::Continue -}); -fn button <'a> ( - key: impl Content + 'a, - label: impl Content + 'a -) -> impl Content + 'a { - Tui::bold(true, Bsp::e( - Margin::x(1, Tui::fg_bg(Tui::g(0), Tui::orange(), key)), - Margin::x(1, Tui::fg_bg(Tui::g(255), Tui::g(96), label)), - )) -} -#[cfg(test)] fn test_tek () { - use clap::CommandFactory; - TekCli::command().debug_assert(); -} diff --git a/tek/src/model.rs b/tek/src/model.rs new file mode 100644 index 00000000..27c79890 --- /dev/null +++ b/tek/src/model.rs @@ -0,0 +1,390 @@ +use crate::*; +#[derive(Default, Debug)] pub struct Tek { + /// Must not be dropped for the duration of the process + pub jack: Arc>, + /// Source of time + pub clock: Clock, + /// Theme + pub color: ItemPalette, + pub pool: Option, + pub editor: Option, + pub player: Option, + pub sampler: Option, + pub midi_buf: Vec>>, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, + pub note_buf: Vec, + pub tracks: Vec, + pub scenes: Vec, + pub selected: Selection, + pub splits: Vec, + pub size: Measure, + pub perf: PerfModel, + pub editing: AtomicBool, + pub history: Vec, + + /// View definition + pub view: SourceIter<'static>, + // Input definitions + pub keys: SourceIter<'static>, + pub keys_clip: SourceIter<'static>, + pub keys_track: SourceIter<'static>, + pub keys_scene: SourceIter<'static>, + pub keys_mix: SourceIter<'static>, + + pub fmtd_beat: Arc>, + pub fmtd_time: Arc>, + pub fmtd_bpm: Arc>, + pub fmtd_sr: Arc>, + pub fmtd_buf: Arc>, + pub fmtd_lat: Arc>, + pub fmtd_stop: Arc, +} +has_size!(|self: Tek|&self.size); +has_clock!(|self: Tek|self.clock); +has_clips!(|self: Tek|self.pool.as_ref().expect("no clip pool").clips); +has_sampler!(|self: Tek|{ + sampler = self.sampler; + index = self.editor.as_ref().map(|e|e.note_pos()).unwrap_or(0); }); +has_editor!(|self: Tek|{ + editor = self.editor; + editor_w = { + let size = self.size.w(); + let editor = self.editor.as_ref().expect("missing editor"); + let time_len = editor.time_len().get(); + let time_zoom = editor.time_zoom().get().max(1); + (5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16) + }; + editor_h = 15; + is_editing = self.editing.load(Relaxed); }); +provide!(Color: |self: Tek| {}); +provide!(Selection: |self: Tek| {}); +provide!(Arc>: |self: Tek| {}); +provide!(Option>>: |self: Tek| {}); +provide_bool!(bool: |self: Tek| {}); +provide_num!(isize: |self: Tek| {}); +provide_num!(usize: |self: Tek| { + ":scene" => self.selected.scene().unwrap_or(0), + ":scene-next" => (self.selected.scene().unwrap_or(0) + 1).min(self.scenes.len()), + ":scene-prev" => self.selected.scene().unwrap_or(0).saturating_sub(1), + ":track" => self.selected.track().unwrap_or(0), + ":track-next" => (self.selected.track().unwrap_or(0) + 1).min(self.tracks.len()), + ":track-prev" => self.selected.track().unwrap_or(0).saturating_sub(1) }); +impl Tek { + pub fn scenes_add (&mut self, n: usize) -> Usually<()> { + let scene_color_1 = ItemColor::random(); + let scene_color_2 = ItemColor::random(); + for i in 0..n { + let _ = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } + pub fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<(usize, &mut Scene)> + { + let scene = Scene { + name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()), + clips: vec![None;self.tracks().len()], + color: color.unwrap_or_else(ItemPalette::random), + }; + self.scenes_mut().push(scene); + let index = self.scenes().len() - 1; + Ok((index, &mut self.scenes_mut()[index])) + } + pub fn scene_default_name (&self) -> Arc { + format!("Sc{:3>}", self.scenes().len() + 1).into() + } + pub fn tracks_add ( + &mut self, count: usize, width: usize, + midi_from: &[PortConnection], midi_to: &[PortConnection], + ) -> Usually<()> { + let jack = self.jack().clone(); + let track_color_1 = ItemColor::random(); + let track_color_2 = ItemColor::random(); + for i in 0..count { + let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into(); + let mut track = self.track_add(None, Some(color), midi_from, midi_to)?.1; + track.width = width; + } + Ok(()) + } + pub fn track_add ( + &mut self, name: Option<&str>, color: Option, + midi_from: &[PortConnection], midi_to: &[PortConnection], + ) -> Usually<(usize, &mut Track)> { + let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); + let mut track = Track { + width: (name.len() + 2).max(9), + color: color.unwrap_or_else(ItemPalette::random), + player: MidiPlayer::from(self.clock()), + name, + ..Default::default() + }; + track.player.midi_ins.push(JackPort::::new( + &self.jack, &format!("{}I", &track.name), midi_from + )?); + track.player.midi_outs.push(JackPort::::new( + &self.jack, &format!("{}O", &track.name), midi_to + )?); + self.tracks_mut().push(track); + let len = self.tracks().len(); + let index = len - 1; + for scene in self.scenes_mut().iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok((index, &mut self.tracks_mut()[index])) + } + pub fn sync_lead (&self, enable: bool) -> Usually<()> { + if enable { + self.jack.read().unwrap().client().register_timebase_callback(false, |mut state|{ + let clock = self.clock(); + clock.playhead.update_from_sample(state.position.frame() as f64); + state.position.bbt = Some(clock.bbt()); + state.position + })?; + } + Ok(()) + } + pub fn sync_follow (&self, enable: bool) -> Usually<()> { + // TODO: sync follow + Ok(()) + } + fn clip (&self) -> Option>> { + self.scene()?.clips.get(self.selected().track()?)?.clone() + } + fn toggle_loop (&mut self) { + if let Some(clip) = self.clip() { + clip.write().unwrap().toggle_loop() + } + } + pub fn track_del (&mut self, index: usize) { + self.tracks_mut().remove(index); + for scene in self.scenes_mut().iter_mut() { + scene.clips.remove(index); + } + } + fn activate (&mut self) -> Usually<()> { + let selected = self.selected().clone(); + match selected { + Selection::Scene(s) => { + let mut clips = vec![]; + for (t, _) in self.tracks().iter().enumerate() { + clips.push(self.scenes()[s].clips[t].clone()); + } + for (t, track) in self.tracks_mut().iter_mut().enumerate() { + if track.player.play_clip.is_some() || clips[t].is_some() { + track.player.enqueue_next(clips[t].as_ref()); + } + } + if self.clock().is_stopped() { + self.clock().play_from(Some(0))?; + } + }, + Selection::Clip(t, s) => { + let clip = self.scenes()[s].clips[t].clone(); + self.tracks_mut()[t].player.enqueue_next(clip.as_ref()); + }, + _ => {} + } + Ok(()) + } + fn cell > (theme: ItemPalette, field: T) -> impl Content { + Tui::fg_bg(theme.lightest.rgb, theme.base.rgb, Fixed::y(1, field)) + } +} +/// Represents the current user selection in the arranger +#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection { + /// The whole mix is selected + #[default] Mix, + /// A track is selected. + Track(usize), + /// A scene is selected. + Scene(usize), + /// A clip (track × scene) is selected. + Clip(usize, usize), +} +/// Focus identification methods +impl Selection { + fn is_mix (&self) -> bool { matches!(self, Self::Mix) } + fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } + fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } + fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } + pub fn track (&self) -> Option { + use Selection::*; + match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None } + } + pub fn scene (&self) -> Option { + use Selection::*; + match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None } + } + fn description (&self, tracks: &[Track], scenes: &[Scene]) -> Arc { + format!("Selected: {}", match self { + Self::Mix => "Everything".to_string(), + Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name)) + .unwrap_or_else(||"T??".into()), + Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name)) + .unwrap_or_else(||"S??".into()), + Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { + (Some(_), Some(scene)) => match scene.clip(*t) { + Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), + None => format!("T{t} S{s}: Empty") + }, + _ => format!("T{t} S{s}: Empty"), + } + }).into() + } +} +impl HasSelection for Tek { + fn selected (&self) -> &Selection { &self.selected } + fn selected_mut (&mut self) -> &mut Selection { &mut self.selected } +} +pub trait HasSelection { + fn selected (&self) -> &Selection; + fn selected_mut (&mut self) -> &mut Selection; +} +#[derive(Debug, Default)] pub struct Track { + /// Name of track + pub name: Arc, + /// Preferred width of track column + pub width: usize, + /// Identifying color of track + pub color: ItemPalette, + /// MIDI player state + pub player: MidiPlayer, + /// Device chain + pub devices: Vec>, + /// Inputs of 1st device + pub audio_ins: Vec>, + /// Outputs of last device + pub audio_outs: Vec>, +} +has_clock!(|self: Track|self.player.clock); +has_player!(|self: Track|self.player); +impl Track { + const MIN_WIDTH: usize = 9; + fn width_inc (&mut self) { self.width += 1; } + fn width_dec (&mut self) { if self.width > Track::MIN_WIDTH { self.width -= 1; } } +} +impl HasTracks for Tek { + fn midi_ins (&self) -> &Vec> { &self.midi_ins } + fn midi_outs (&self) -> &Vec> { &self.midi_outs } + fn tracks (&self) -> &Vec { &self.tracks } + fn tracks_mut (&mut self) -> &mut Vec { &mut self.tracks } +} +pub trait HasTracks: HasSelection + HasClock + HasJack + HasEditor + Send + Sync { + fn midi_ins (&self) -> &Vec>; + fn midi_outs (&self) -> &Vec>; + fn tracks (&self) -> &Vec; + fn tracks_mut (&mut self) -> &mut Vec; + fn track_longest (&self) -> usize { + self.tracks().iter().map(|s|s.name.len()).fold(0, usize::max) + } + const WIDTH_OFFSET: usize = 1; + fn tracks_sizes <'a> (&'a self, editing: bool, bigger: usize) + -> impl Iterator + Send + Sync + 'a + { + let mut x = 0; + let active = match self.selected() { + Selection::Track(t) if editing => Some(t.saturating_sub(1)), + Selection::Clip(t, _) if editing => Some(t.saturating_sub(1)), + _ => None + }; + self.tracks().iter().enumerate().map(move |(index, track)|{ + let width = if Some(index) == active { bigger } else { track.width.max(8) }; + let data = (index, track, x, x + width); + x += width + Self::WIDTH_OFFSET; + data + }) + } + fn track_next_name (&self) -> Arc { + format!("Track{:02}", self.tracks().len() + 1).into() + } + fn track (&self) -> Option<&Track> { + self.selected().track().and_then(|s|self.tracks().get(s)) + } + fn track_mut (&mut self) -> Option<&mut Track> { + self.selected().track().and_then(|s|self.tracks_mut().get_mut(s)) + } +} +pub trait Device: Send + Sync + std::fmt::Debug {} +impl Device for Sampler {} +impl Device for Plugin {} +#[derive(Debug, Default)] pub struct Scene { + /// Name of scene + pub name: Arc, + /// Clips in scene, one per track + pub clips: Vec>>>, + /// Identifying color of scene + pub color: ItemPalette, +} +impl Scene { + /// Returns the pulse length of the longest clip in the scene + fn pulses (&self) -> usize { + self.clips.iter().fold(0, |a, p|{ + a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0)) + }) + } + /// Returns true if all clips in the scene are + /// currently playing on the given collection of tracks. + fn is_playing (&self, tracks: &[Track]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(c) => tracks + .get(track_index) + .map(|track|{ + if let Some((_, Some(clip))) = track.player().play_clip() { + *clip.read().unwrap() == *c.read().unwrap() + } else { + false + } + }) + .unwrap_or(false), + None => true + }) + } + fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } +} +impl HasScenes for Tek { + fn scenes (&self) -> &Vec { &self.scenes } + fn scenes_mut (&mut self) -> &mut Vec { &mut self.scenes } +} +pub trait HasScenes: HasSelection + HasEditor + Send + Sync { + fn scenes (&self) -> &Vec; + fn scenes_mut (&mut self) -> &mut Vec; + fn scene_longest (&self) -> usize { + self.scenes().iter().map(|s|s.name.len()).fold(0, usize::max) + } + fn scenes_sizes (&self, editing: bool, height: usize, larger: usize,) + -> impl Iterator + Send + Sync + { + let mut y = 0; + let (selected_track, selected_scene) = match self.selected() { + Selection::Clip(t, s) => (Some(t.saturating_sub(1)), Some(s.saturating_sub(1))), + _ => (None, None) + }; + self.scenes().iter().enumerate().map(move|(s, scene)|{ + let active = editing && selected_track.is_some() && selected_scene == Some(s); + let height = if active { larger } else { height }; + let data = (s, scene, y, y + height); + y += height; + data + }) + } + fn scene (&self) -> Option<&Scene> { + self.selected().scene().and_then(|s|self.scenes().get(s)) + } + fn scene_mut (&mut self) -> Option<&mut Scene> { + self.selected().scene().and_then(|s|self.scenes_mut().get_mut(s)) + } + fn scene_del (&mut self, index: usize) { + self.selected().scene().and_then(|s|Some(self.scenes_mut().remove(index))); + } +} diff --git a/tek/src/view.rs b/tek/src/view.rs new file mode 100644 index 00000000..1b9886af --- /dev/null +++ b/tek/src/view.rs @@ -0,0 +1,377 @@ +use crate::*; +view!(TuiOut: |self: Tek| self.size.of(View(self, self.view)); { + ":editor" => (&self.editor).boxed(), + ":pool" => self.view_pool().boxed(), + ":sample" => self.view_sample(self.is_editing()).boxed(), + ":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(), + ":status" => self.view_editor().boxed(), + ":toolbar" => self.view_clock().boxed(), + ":tracks" => self.view_tracks().boxed(), + ":inputs" => self.view_inputs().boxed(), + ":outputs" => self.view_outputs().boxed(), + ":scenes" => self.view_scenes().boxed(), + ":scene-add" => Fill::x(Align::x(Fixed::x(23, button(" C-a ", format!(" add scene ({}/{})", + self.selected().scene().unwrap_or(0), + self.scenes().len()))))).boxed(), +}); +provide_num!(u16: |self: Tek| { + ":sidebar-w" => self.sidebar_w(), + ":sample-h" => if self.is_editing() { 0 } else { 5 }, + ":samples-w" => if self.is_editing() { 4 } else { 11 }, + ":samples-y" => if self.is_editing() { 1 } else { 0 }, + ":pool-w" => if self.is_editing() { 5 } else { + let w = self.size.w(); + if w > 60 { 20 } else if w > 40 { 15 } else { 10 } + } }); +macro_rules! per_track { + (|$self:ident,$track:ident,$index:ident|$content:expr) => {{ + let tracks = ||$self.tracks_sizes($self.is_editing(), $self.editor_w()); + Box::new(move||Align::x(Map::new(tracks, move|(_, $track, x1, x2), $index| { + let width = (x2 - x1) as u16; + let content = Fixed::y(1, $content); + let styled = Tui::fg_bg($track.color.lightest.rgb, $track.color.base.rgb, content); + map_east(x1 as u16, width, Fixed::x(width, styled)) + }))).into() + }} +} +impl Tek { + fn view_clock (&self) -> impl Content + use<'_> { + Outer(false, Style::default().fg(Tui::g(0))).enclose(row!( + self.view_engine_stats(), " ", + self.view_play_pause(), " ", + self.view_beat_stats(), + )) + } + fn view_beat_stats (&self) -> impl Content + use<'_> { + let compact = self.size.w() > 80; + let clock = self.clock(); + let delta = |start: &Moment|clock.global.usec.get() - start.usec.get(); + let mut fmtd_beat = self.fmtd_beat.write().unwrap(); + let mut fmtd_time = self.fmtd_time.write().unwrap(); + let mut fmtd_bpm = self.fmtd_bpm.write().unwrap(); + fmtd_beat.clear(); + fmtd_time.clear(); + fmtd_bpm.clear(); + if let Some(now) = clock.started.read().unwrap().as_ref().map(delta) { + clock.timebase.format_beats_1_to(&mut*fmtd_beat, clock.timebase.usecs_to_pulse(now)); + write!(&mut fmtd_time, "{:.3}s", now/1000000.); + write!(&mut fmtd_bpm, "{:.3}", clock.timebase.bpm.get()); + } else { + write!(&mut fmtd_beat, "-.-.--"); + write!(&mut fmtd_time, "-.---s"); + write!(&mut fmtd_bpm, "---.---"); + } + let theme = ItemPalette::G[128]; + Thunk::new(move||Either::new(compact, + row!(FieldH(theme, "BPM", self.fmtd_bpm.clone()), + FieldH(theme, "Beat", self.fmtd_beat.clone()), + FieldH(theme, "Time", self.fmtd_time.clone())), + row!(FieldV(theme, "BPM", self.fmtd_bpm.clone()), + FieldV(theme, "Beat", self.fmtd_beat.clone()), + FieldV(theme, "Time", self.fmtd_time.clone())))) + } + fn view_engine_stats (&self) -> impl Content + use<'_> { + let compact = self.size.w() > 80; + let clock = self.clock(); + let rate = clock.timebase.sr.get(); + let chunk = clock.chunk.load(Relaxed); + let mut fmtd_sr = self.fmtd_sr.write().unwrap(); + let mut fmtd_buf = self.fmtd_buf.write().unwrap(); + let mut fmtd_lat = self.fmtd_lat.write().unwrap(); + fmtd_sr.clear(); + write!(&mut fmtd_sr, "{}", if compact {format!("{:.1}kHz", rate / 1000.)} else {format!("{:.0}Hz", rate)}); + fmtd_buf.clear(); + write!(&mut fmtd_buf, "{chunk}"); + fmtd_lat.clear(); + write!(&mut fmtd_lat, "{:.1}ms", chunk as f64 / rate * 1000.); + let theme = ItemPalette::G[128]; + Either::new(compact, + row!(FieldH(theme, "SR", self.fmtd_sr.clone()), + FieldH(theme, "Buf", self.fmtd_buf.clone()), + FieldH(theme, "Lat", self.fmtd_lat.clone())), + row!(FieldV(theme, "SR", self.fmtd_sr.clone()), + FieldV(theme, "Buf", self.fmtd_buf.clone()), + FieldV(theme, "Lat", self.fmtd_lat.clone()))) + } + fn view_meter <'a> (&'a self, label: &'a str, value: f32) -> impl Content + 'a { + col!( + FieldH(ItemPalette::G[128], label, format!("{:>+9.3}", value)), + Fixed::xy(if value >= 0.0 { 13 } + else if value >= -1.0 { 12 } + else if value >= -2.0 { 11 } + else if value >= -3.0 { 10 } + else if value >= -4.0 { 9 } + else if value >= -6.0 { 8 } + else if value >= -9.0 { 7 } + else if value >= -12.0 { 6 } + else if value >= -15.0 { 5 } + else if value >= -20.0 { 4 } + else if value >= -25.0 { 3 } + else if value >= -30.0 { 2 } + else if value >= -40.0 { 1 } + else { 0 }, 1, Tui::bg(if value >= 0.0 { Red } + else if value >= -3.0 { Yellow } + else { Green }, ()))) + } + fn view_meters (&self, values: &[f32;2]) -> impl Content + use<'_> { + col!( + format!("L/{:>+9.3}", values[0]), + format!("R/{:>+9.3}", values[1]), + ) + } + fn view_play_pause (&self) -> impl Content + use<'_> { + let playing = self.clock.is_rolling(); + let compact = self.is_editing(); + Tui::bg( + if playing{Rgb(0,128,0)}else{Rgb(128,64,0)}, + Either::new(compact, + Thunk::new(move||Fixed::x(9, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), " PLAYING "), + Tui::fg(Rgb(255, 128, 0), " STOPPED ")))), + Thunk::new(move||Fixed::x(5, Either::new(playing, + Tui::fg(Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), + Tui::fg(Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))))) + } + fn view_editor (&self) -> impl Content + use<'_> { + self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())) + } + fn view_pool (&self) -> impl Content + use<'_> { + self.pool.as_ref().map(|pool|PoolView(self.is_editing(), pool)) + } + fn view_scene_add (&self) -> impl Content + use<'_> { + button(" C-a ", format!(" add scene ({}/{})", + self.selected().scene().unwrap_or(0), + self.scenes().len())) + } + fn view_tracks (&self) -> impl Content + use<'_> { + let h = 1; + self.view_row(self.w(), 1, self.track_header(), self.track_cells()) + } + fn view_inputs (&self) -> impl Content + use<'_> { + let h = 1 + self.midi_ins.len() as u16; + self.view_row(self.w(), h, self.input_header(), self.input_cells()) + } + fn view_outputs (&self) -> impl Content + use<'_> { + let h = 1 + self.midi_outs.len(); + self.view_row(self.w(), h as u16, + self.output_header(), + self.output_cells()) + } + fn view_scenes (&self) -> impl Content + use<'_> { + Outer(false, Style::default().fg(Tui::g(0))).enclose_bg({ + let w = self.w(); + let d = 6 + self.midi_ins.len() + self.midi_outs.len(); + let h = self.size.h().saturating_sub(d) as u16; + self.view_row(w, h, self.scene_header(), self.clip_columns()) + }) + } + fn scene_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> { + (move||{ + let last_color = Arc::new(RwLock::new(ItemPalette::G[0])); + let iter = ||self.scenes_sizes(self.is_editing(), 2, 15); + Map::new(iter, move|(_, scene, y1, y2), i| { + let cell = phat_sel_3( + self.selected().scene() == Some(i), + Tui::bold(true, Bsp::e("🭬", &scene.name)), + Tui::bold(true, Bsp::e("🭬", &scene.name)), + if i == 0 { Some(Reset) } + else if self.selected().scene() == Some(i) { None } + else { Some(last_color.read().unwrap().base.rgb) }, + if self.selected().scene() == Some(i+1) { scene.color.light } else { scene.color.base }.rgb, + Some(Reset) + ); + let h = (1 + y2 - y1) as u16; + *last_color.write().unwrap() = scene.color; + map_south(y1 as u16, h, Push::y(1, Fixed::y(h, + Outer(false, Style::default().fg(Tui::g(0))).enclose(cell)))) + }).boxed() + }).into() + } + fn clip_columns <'a> (&'a self) -> ThunkBox<'a, TuiOut> { + let editing = self.is_editing(); + let tracks = move||self.tracks_sizes(editing, self.editor_w()); + let scenes = move||self.scenes_sizes(editing, 2, 15); + let selected_track = self.selected().track(); + let selected_scene = self.selected().scene(); + let border = |x|Outer(false, Style::default().fg(Tui::g(0))).enclose(x); + let d = 6 + self.midi_ins.len() + self.midi_outs.len(); + (move||Align::c(Map::new(tracks, { + let last_color = Arc::new(RwLock::new(ItemPalette::default())); + let area = self.size.w().saturating_sub(self.sidebar_w() as usize * 2); + move|(_, track, x1, x2), t| { + let last_color = last_color.clone(); + let same_track = selected_track == Some(t+1); + let w = (x2 - x1) as u16; + map_east(x1 as u16, w, border(Map::new(scenes, move|(_, scene, y1, y2), s|{ + let last_color = last_color.clone(); + Either(x2 >= area, (), Thunk::new(move||{ + let last_color = last_color.clone(); + let mut fg = Tui::g(64); + let mut bg = ItemPalette::G[32]; + if let Some(clip) = &scene.clips[t] { + let clip = clip.read().unwrap(); + fg = clip.color.lightest.rgb; + bg = clip.color + }; + + // weird offsetting: + let selected = same_track && selected_scene == Some(s+1); + let neighbor = same_track && selected_scene == Some(s); + let active = editing && selected; + + //let top = if neighbor { None } else { Some(last_color.read().unwrap().base.rgb) }; + let top = if s == 0 { + Some(Reset) + } else if neighbor { + Some(last_color.read().unwrap().light.rgb) + } else { + Some(last_color.read().unwrap().base.rgb) + }; + let mid = if selected { bg.light } else { bg.base }.rgb; + let low = Some(Reset); + let h = (1 + y2 - y1) as u16; + *last_color.write().unwrap() = bg; + let tab = " Tab "; + let name = if active { + self.editor.as_ref() + .map(|e|e.clip().as_ref().map(|c|c.clone())) + .flatten() + .map(|c|c.read().unwrap().name.clone()) + .unwrap_or_else(||"".into()) + } else { + "edit".into() + }; + let label = move||{ + let clip = scene.clips[t].clone(); + let icon = " ⏹ "; + let name = clip.map(|c|c.read().unwrap().name.clone()); + Align::nw(Tui::fg(fg, Bsp::e(icon, Bsp::e(Tui::bold(true, name), " ")))) + }; + let area = self.size.h().saturating_sub(d);//self.sidebar_w() as usize * 2); + Either(y2 > area, (), map_south(y1 as u16, h, Push::y(1, Fixed::y(h, Either::new(active, + Thunk::new(move||Bsp::a( + Fill::xy(Align::nw(button(tab, label()))), + &self.editor)), + Thunk::new(move||Bsp::a( + When::new(selected, Fill::y(Align::n(button(tab, "edit")))), + phat_sel_3( + selected, + Fill::xy(label()), + Fill::xy(label()), + top, mid, low + ) + )), + ))))) + + })) + }))).boxed() + } + })).boxed()).into() + } + fn view_row <'a> ( + &'a self, w: u16, h: u16, a: impl Content + 'a, b: impl Content + 'a + ) -> impl Content + 'a { + Fixed::y(h, Bsp::e( + Fixed::x(self.sidebar_w() as u16, a), + Fill::x(Align::c(Fixed::xy(w, h, b))) + )) + } + fn w (&self) -> u16 { + self.tracks_sizes(self.is_editing(), self.editor_w()) + .last() + .map(|x|x.3 as u16) + .unwrap_or(0) + } + fn sidebar_w (&self) -> u16 { + let w = self.size.w(); + let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; + let w = if self.is_editing() { 8 } else { w }; + w + } + fn input_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> { + let fg = Tui::g(224); + let bg = Tui::g(64); + let input = move|input: &JackPort|Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(input.name.clone())))), + input.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, + Tui::fg_bg(fg, bg, connect.info())))))); + (move||{ + let label = format!(" midi ins ({})", self.midi_ins().len()); + let button = Fill::x(Align::w(self.button(" I ", label))); + Bsp::s(button, self.midi_ins().get(0).map(input)).boxed() + }).into() + } + fn input_cells <'a> (&'a self) -> ThunkBox<'a, TuiOut> { + let rec = false; + let mon = false; + per_track!(|self, track, _t|Bsp::s(Tui::bold(true, row!( + Tui::fg_bg(if rec { White } else { track.color.light.rgb }, track.color.dark.rgb, "Rcrd"), + Tui::fg_bg(if rec { White } else { track.color.dark.rgb }, track.color.dark.rgb, "▐"), + Tui::fg_bg(if mon { White } else { track.color.light.rgb }, track.color.dark.rgb, "Mntr"), + )), row!( + Tui::fg_bg(if rec { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"), + Tui::fg_bg(if rec { White } else { track.color.dark.rgb }, track.color.darker.rgb, "▐"), + Tui::fg_bg(if mon { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"), + ))) + } + fn output_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> { + let fg = Tui::g(224); + let bg = Tui::g(64); + (move||Bsp::s(Fill::x(Align::w(self.button(" O ", format!(" midi outs ({}) ", self.midi_outs().len())))), self.midi_outs().get(0).map(|out|Bsp::s( + Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(out.name.clone())))), + out.connect.get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, connect.info()))))), + ))).boxed()).into() + } + fn output_cells <'a> (&'a self) -> ThunkBox<'a, TuiOut> { + let mute = false; + let solo = false; + per_track!(|self, track, _t|Bsp::s(Tui::bold(true, row!( + Tui::fg_bg(if mute { White } else { track.color.light.rgb }, track.color.dark.rgb, "Mute"), + Tui::fg_bg(if mute { White } else { track.color.dark.rgb }, track.color.dark.rgb, "▐"), + Tui::fg_bg(if solo { White } else { track.color.light.rgb }, track.color.dark.rgb, "Solo"), + )), row!( + Tui::fg_bg(if mute { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"), + Tui::fg_bg(if mute { White } else { track.color.darker.rgb }, track.color.darker.rgb, "▐"), + Tui::fg_bg(if solo { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"), + ))) + } + fn track_header <'a> (&'a self) -> ThunkBox<'a, TuiOut> { + let add_track = ||self.button(" C-t ", format!(" add track ({}/{})", + self.selected.track().unwrap_or(0), + self.tracks().len())); + (move||Tui::bg(Tui::g(32), Fill::x(Align::w(add_track()))).boxed()).into() + } + fn track_cells <'a> (&'a self) -> ThunkBox<'a, TuiOut> { + per_track!(|self, track, t|{ + let active = self.selected().track() == Some(t+1); + let name = &track.name; + let fg = track.color.lightest.rgb; + let bg = if active { track.color.light.rgb } else { track.color.base.rgb }; + let bg2 = if t > 0 { self.tracks()[t - 1].color.base.rgb } else { Reset }; + let bfg = if active { Rgb(255,255,255) } else { Rgb(0,0,0) }; + let bs = Style::default().fg(bfg).bg(bg); + let cell = Bsp::e( + Tui::fg_bg(bg, bg2, "▐"), + Tui::fg_bg(fg, bg, Tui::bold(true, Fill::x(Align::nw(Bsp::e(" ", name)))))); + Outer(active, bs).enclose(cell) + }) + } + fn button <'a> ( + &'a self, key: impl Content + 'a, label: impl Content + 'a + ) -> impl Content + 'a { + let compact = !self.is_editing(); + Tui::bold(true, Bsp::e( + Margin::x(1, Tui::fg_bg(Tui::g(0), Tui::orange(), key)), + When::new(compact, Margin::x(1, Tui::fg_bg(Tui::g(255), Tui::g(96), label))), + )) + } +} +fn button <'a> ( + key: impl Content + 'a, + label: impl Content + 'a +) -> impl Content + 'a { + Tui::bold(true, Bsp::e( + Margin::x(1, Tui::fg_bg(Tui::g(0), Tui::orange(), key)), + Margin::x(1, Tui::fg_bg(Tui::g(255), Tui::g(96), label)), + )) +}