use crate::*; use std::path::PathBuf; type MaybeClip = Option>>; macro_rules! ns { ($C:ty, $s:expr, $a:expr, $W:expr) => { <$C>::try_from_expr($s, $a).map($W) } } macro_rules! cmd { ($cmd:expr) => {{ $cmd; None }}; } macro_rules! cmd_todo { ($msg:literal) => {{ println!($msg); None }}; } expose!([self: Tek] ([bool] (":mode-editor" self.is_editing()) (":mode-clip" !self.is_editing() && self.selected.is_clip()) (":mode-track" !self.is_editing() && self.selected.is_track()) (":mode-scene" !self.is_editing() && self.selected.is_scene()) (":mode-mix" !self.is_editing() && self.selected.is_mix()) (":mode-pool-import" matches!( self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Import(..)))) (":mode-pool-export" matches!( self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Export(..)))) (":mode-pool-rename" matches!( self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Rename(..)))) (":mode-pool-length" matches!( self.pool.as_ref().map(|p|p.mode.as_ref()).flatten(), Some(PoolMode::Length(..))))) ([isize]) ([Color]) ([Arc>]) ([u7] (":pitch" (self.editor().map(|e|e.note_pos()).unwrap() as u8).into())) ([u16] (":w-sidebar" self.w_sidebar())) ([usize] (":scene-last" self.scenes.len()) (":track-last" self.tracks.len())) ([Option] (":scene" self.selected.scene()) (":track" self.selected.track())) ([MaybeClip] (":clip" match self.selected { Selection::TrackClip { track, scene } => self.scenes[scene].clips[track].clone(), _ => None })) ([Selection] (":scene-next" self.selected.scene_next(self.scenes.len())) (":scene-prev" self.selected.scene_prev()) (":track-next" self.selected.track_next(self.tracks.len())) (":track-prev" self.selected.track_prev()))); provide!(bool: |self: MidiPool| {}); provide!(MidiClip: |self: MidiPool| { ":new-clip" => self.new_clip(), ":cloned-clip" => self.cloned_clip(), }); provide!(PathBuf: |self: MidiPool| {}); provide!(Arc: |self: MidiPool| {}); provide!(usize: |self: MidiPool| { ":current" => 0, ":after" => 0, ":previous" => 0, ":next" => 0 }); provide!(ItemColor: |self: MidiPool| { ":random-color" => ItemColor::random() }); provide!(bool: |self: MidiEditor| { ":true" => true, ":false" => false, ":time-lock" => self.time_lock().get(), ":time-lock-toggle" => !self.time_lock().get(), }); provide!(usize: |self: MidiEditor| { ":note-length" => self.note_len(), ":note-pos" => self.note_pos(), ":note-pos-next" => self.note_pos() + 1, ":note-pos-prev" => self.note_pos().saturating_sub(1), ":note-pos-next-octave" => self.note_pos() + 12, ":note-pos-prev-octave" => self.note_pos().saturating_sub(12), ":note-len" => self.note_len(), ":note-len-next" => self.note_len() + 1, ":note-len-prev" => self.note_len().saturating_sub(1), ":note-range" => self.note_axis().get(), ":note-range-prev" => self.note_axis().get() + 1, ":note-range-next" => self.note_axis().get().saturating_sub(1), ":time-pos" => self.time_pos(), ":time-pos-next" => self.time_pos() + self.time_zoom().get(), ":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()), ":time-zoom" => self.time_zoom().get(), ":time-zoom-next" => self.time_zoom().get() + 1, ":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1), }); handle!(TuiIn: |self: Tek, input|Ok(if let Some(command) = self.config.keys.command(self, input) { let undo = command.execute(self)?; if let Some(undo) = undo { self.history.push(undo); } Some(true) } else { None })); impose!([app: Tek] (TekCommand: ("menu" [] Some(Self::ToggleMenu)) ("help" [] Some(Self::ToggleHelp)) ("stop" [] Some(Self::StopAll)) ("undo" [d: usize] Some(Self::History(-(d.unwrap_or(0) as isize)))) ("redo" [d: usize] Some(Self::History(d.unwrap_or(0) as isize))) ("zoom" [z: usize] Some(Self::Zoom(z))) ("edit" [] Some(Self::Edit(None))) ("edit" [c: bool] Some(Self::Edit(c))) ("color" [] Some(Self::Color(ItemTheme::random()))) ("color" [c: Color] Some(Self::Color(c.map(ItemTheme::from).expect("no color")))) ("enqueue" [c: Arc>] Some(Self::Enqueue(c))) ("launch" [] Some(Self::Launch)) ("select" [t: Selection] Some(t.map(Self::Select).expect("no selection"))) ("clock" [,..a] ns!(ClockCommand, app.clock(), a, Self::Clock)) ("scene" [,..a] ns!(SceneCommand, app, a, Self::Scene)) ("track" [,..a] ns!(TrackCommand, app, a, Self::Track)) ("input" [,..a] ns!(InputCommand, app, a, Self::Input)) ("output" [,..a] ns!(OutputCommand, app, a, Self::Output)) ("clip" [,..a] ns!(ClipCommand, app, a, Self::Clip)) ("pool" [,..a] app.pool.as_ref().map(|p|ns!(PoolCommand, p, a, Self::Pool)).flatten()) ("editor" [,..a] app.editor().map(|e|ns!(MidiEditCommand, e, a, Self::Editor)).flatten()) ("sampler" [,..a] app.sampler().map(|s|ns!(SamplerCommand, s, a, Self::Sampler)).flatten()) ("select" [t: usize, s: usize] Some(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::TrackClip { track: t, scene: s }) }))) (ClipCommand: ("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap()))) ("color" [a: usize, b: usize] Some(Self::SetColor(a.unwrap(), b.unwrap(), ItemTheme::random()))) ("enqueue" [a: usize, b: usize] Some(Self::Enqueue(a.unwrap(), b.unwrap()))) ("get" [a: usize, b: usize] Some(Self::Get(a.unwrap(), b.unwrap()))) ("loop" [a: usize, b: usize, c: bool] Some(Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))) ("put" [a: usize, b: usize, c: MaybeClip] Some(Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))) ("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) (InputCommand: ("add" [] Some(Self::Add))) (OutputCommand: ("add" [] Some(Self::Add))) (SceneCommand: ("add" [] Some(Self::Add)) ("delete" [a: Option] Some(Self::Del(a.flatten().unwrap()))) ("zoom" [a: usize] Some(Self::SetZoom(a.unwrap()))) ("color" [a: usize] Some(Self::SetColor(a.unwrap(), ItemTheme::G[128]))) ("enqueue" [a: usize] Some(Self::Enqueue(a.unwrap()))) ("swap" [a: usize, b: usize] Some(Self::Swap(a.unwrap(), b.unwrap())))) (TrackCommand: ("add" [] Some(Self::Add)) ("size" [a: usize] Some(Self::SetSize(a.unwrap()))) ("zoom" [a: usize] Some(Self::SetZoom(a.unwrap()))) ("color" [a: usize] Some(Self::SetColor(a.unwrap(), ItemTheme::random()))) ("delete" [a: Option] Some(Self::Del(a.flatten().unwrap()))) ("stop" [a: usize] Some(Self::Stop(a.unwrap()))) ("swap" [a: usize, b: usize] Some(Self::Swap(a.unwrap(), b.unwrap()))) ("play" [] Some(Self::TogglePlay)) ("solo" [] Some(Self::ToggleSolo)) ("rec" [] Some(Self::ToggleRec)) ("mon" [] Some(Self::ToggleMon)))); defcom!([self, app: Tek] (TekCommand (Sampler [cmd: SamplerCommand] app.sampler_mut().map(|s|cmd.delegate(s, Self::Sampler)).transpose()?.flatten()) (Scene [cmd: SceneCommand] cmd.delegate(app, Self::Scene)?) (Track [cmd: TrackCommand] cmd.delegate(app, Self::Track)?) (Output [cmd: OutputCommand] cmd.delegate(app, Self::Output)?) (Input [cmd: InputCommand] cmd.delegate(app, Self::Input)?) (Clip [cmd: ClipCommand] cmd.delegate(app, Self::Clip)?) (Clock [cmd: ClockCommand] cmd.delegate(app, Self::Clock)?) (Editor [cmd: MidiEditCommand] delegate_to_editor(app, cmd)?) (Pool [cmd: PoolCommand] delegate_to_pool(app, cmd)?) (ToggleHelp [] cmd!(app.toggle_modal(Some(Modal::Help)))) (ToggleMenu [] cmd!(app.toggle_modal(Some(Modal::Menu)))) (Color [p: ItemTheme] app.set_color(Some(p)).map(Self::Color)) (Enqueue [c: MaybeClip] cmd_todo!("\n\rtodo: enqueue {c:?}")) (History [d: isize] cmd_todo!("\n\rtodo: history {d:?}")) (Zoom [z: Option] cmd_todo!("\n\rtodo: zoom {z:?}")) (Edit [value: Option] cmd!(app.toggle_editor(value))) (Launch [] cmd!(app.launch())) (Select [s: Selection] cmd!(app.select(s))) (StopAll [] cmd!(app.stop_all()))) (InputCommand (Add [] cmd!(app.add_midi_in()?))) (OutputCommand (Add [] cmd!(app.add_midi_out()?))) (TrackCommand (TogglePlay [] Some(Self::TogglePlay)) (ToggleSolo [] Some(Self::ToggleSolo)) (SetSize [t: usize] cmd_todo!("\n\rtodo: {self:?}")) (SetZoom [z: usize] cmd_todo!("\n\rtodo: {self:?}")) (Swap [a: usize, b: usize] cmd_todo!("\n\rtodo: {self:?}")) (Del [index: usize] cmd!(app.track_del(index))) (Stop [index: usize] cmd!(app.tracks[index].player.enqueue_next(None))) (Add [] Some(Self::Del(app.track_add_focus()?))) (SetColor [i: usize, c: ItemTheme] Some(Self::SetColor(i, app.track_set_color(i, c)))) (ToggleRec [] { app.track_toggle_record(); Some(Self::ToggleRec) }) (ToggleMon [] { app.track_toggle_monitor(); Some(Self::ToggleMon) })) (SceneCommand (Swap [a: usize, b: usize] cmd_todo!("\n\rtodo: {self:?}")) (SetSize [index: usize] cmd_todo!("\n\rtodo: {self:?}")) (SetZoom [zoom: usize] cmd_todo!("\n\rtodo: {self:?}")) (Enqueue [scene: usize] cmd!(app.scene_enqueue(scene))) (Del [index: usize] cmd!(app.scene_del(index))) (Add [] Some(Self::Del(app.scene_add_focus()?))) (SetColor [i: usize, c: ItemTheme] Some(Self::SetColor(i, app.scene_set_color(i, c))))) (ClipCommand (Get [a: usize, b: usize] cmd_todo!("\n\rtodo: clip: get: {a} {b}")) (Edit [clip: MaybeClip] cmd_todo!("\n\rtodo: clip: edit: {clip:?}")) (SetLoop [t: usize, s: usize, l: bool] cmd_todo!("\n\rtodo: {self:?}")) (Put [t: usize, s: usize, c: MaybeClip] Some(Self::Put(t, s, app.clip_put(t, s, c)))) (Enqueue [t: usize, s: usize] cmd!(app.tracks[t].player.enqueue_next(app.scenes[s].clips[t].as_ref()))) (SetColor [t: usize, s: usize, c: ItemTheme] app.clip_set_color(t, s, c).map(|o|Self::SetColor(t, s, o))))); fn delegate_to_editor (app: &mut Tek, cmd: MidiEditCommand) -> Perhaps { Ok(app.editor.as_mut().map(|editor|cmd.delegate(editor, TekCommand::Editor)) .transpose()? .flatten()) } fn delegate_to_pool (app: &mut Tek, cmd: PoolCommand) -> Perhaps { Ok(if let Some(pool) = app.pool.as_mut() { let undo = cmd.clone().delegate(pool, TekCommand::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, PartialEq, Debug)] pub enum PoolCommand { /// Toggle visibility of pool Show(bool), /// Select a clip from the clip pool Select(usize), /// Rename a clip Rename(ClipRenameCommand), /// Change the length of a clip Length(ClipLengthCommand), /// Import from file Import(FileBrowserCommand), /// Export to file Export(FileBrowserCommand), /// Update the contents of the clip pool Clip(PoolClipCommand), } atom_command!(PoolCommand: |state: MidiPool| { ("show" [a: bool] Some(Self::Show(a.expect("no flag")))) ("select" [i: usize] Some(Self::Select(i.expect("no index")))) ("rename" [,..a] ClipRenameCommand::try_from_expr(state, a).map(Self::Rename)) ("length" [,..a] ClipLengthCommand::try_from_expr(state, a).map(Self::Length)) ("import" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Import)) ("export" [,..a] FileBrowserCommand::try_from_expr(state, a).map(Self::Export)) ("clip" [,..a] PoolClipCommand::try_from_expr(state, a).map(Self::Clip)) }); command!(|self: PoolCommand, state: MidiPool|{ use PoolCommand::*; match self { Rename(ClipRenameCommand::Begin) => { state.begin_clip_rename(); None } Rename(command) => command.delegate(state, Rename)?, Length(ClipLengthCommand::Begin) => { state.begin_clip_length(); None }, Length(command) => command.delegate(state, Length)?, Import(FileBrowserCommand::Begin) => { state.begin_import()?; None }, Import(command) => command.delegate(state, Import)?, Export(FileBrowserCommand::Begin) => { state.begin_export()?; None }, Export(command) => command.delegate(state, Export)?, Clip(command) => command.execute(state)?.map(Clip), Show(visible) => { state.visible = visible; Some(Self::Show(!visible)) }, Select(clip) => { state.set_clip_index(clip); None }, } }); #[derive(Clone, Debug, PartialEq)] pub enum PoolClipCommand { Add(usize, MidiClip), Delete(usize), Swap(usize, usize), Import(usize, PathBuf), Export(usize, PathBuf), SetName(usize, Arc), SetLength(usize, usize), SetColor(usize, ItemColor), } impl Command for PoolClipCommand { fn execute (self, model: &mut T) -> Perhaps { use PoolClipCommand::*; Ok(match self { Add(mut index, clip) => { let clip = Arc::new(RwLock::new(clip)); let mut clips = model.clips_mut(); if index >= clips.len() { index = clips.len(); clips.push(clip) } else { clips.insert(index, clip); } Some(Self::Delete(index)) }, Delete(index) => { let clip = model.clips_mut().remove(index).read().unwrap().clone(); Some(Self::Add(index, clip)) }, Swap(index, other) => { model.clips_mut().swap(index, other); Some(Self::Swap(index, other)) }, Import(index, path) => { let bytes = std::fs::read(&path)?; let smf = Smf::parse(bytes.as_slice())?; let mut t = 0u32; let mut events = vec![]; for track in smf.tracks.iter() { for event in track.iter() { t += event.delta.as_int(); if let TrackEventKind::Midi { channel, message } = event.kind { events.push((t, channel.as_int(), message)); } } } let mut clip = MidiClip::new("imported", true, t as usize + 1, None, None); for event in events.iter() { clip.notes[event.0 as usize].push(event.2); } Self::Add(index, clip).execute(model)? }, Export(_index, _path) => { todo!("export clip to midi file"); }, SetName(index, name) => { let clip = &mut model.clips_mut()[index]; let old_name = clip.read().unwrap().name.clone(); clip.write().unwrap().name = name; Some(Self::SetName(index, old_name)) }, SetLength(index, length) => { let clip = &mut model.clips_mut()[index]; let old_len = clip.read().unwrap().length; clip.write().unwrap().length = length; Some(Self::SetLength(index, old_len)) }, SetColor(index, color) => { let mut color = ItemTheme::from(color); std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color); Some(Self::SetColor(index, color.base)) }, }) } } atom_command!(ClipRenameCommand: |state: MidiPool| { ("begin" [] Some(Self::Begin)) ("cancel" [] Some(Self::Cancel)) ("confirm" [] Some(Self::Confirm)) ("set" [n: Arc] Some(Self::Set(n.expect("no name")))) }); atom_command!(ClipLengthCommand: |state: MidiPool| { ("begin" [] Some(Self::Begin)) ("cancel" [] Some(Self::Cancel)) ("next" [] Some(Self::Next)) ("prev" [] Some(Self::Prev)) ("inc" [] Some(Self::Inc)) ("dec" [] Some(Self::Dec)) ("set" [l: usize] Some(Self::Set(l.expect("no length")))) }); atom_command!(FileBrowserCommand: |state: MidiPool| { ("begin" [] Some(Self::Begin)) ("cancel" [] Some(Self::Cancel)) ("confirm" [] Some(Self::Confirm)) ("select" [i: usize] Some(Self::Select(i.expect("no index")))) ("chdir" [p: PathBuf] Some(Self::Chdir(p.expect("no path")))) ("filter" [f: Arc] Some(Self::Filter(f.expect("no filter")))) }); atom_command!(MidiEditCommand: |state: MidiEditor| { ("note/append" [] Some(Self::AppendNote)) ("note/put" [] Some(Self::PutNote)) ("note/del" [] Some(Self::DelNote)) ("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor")))) ("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length")))) ("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor")))) ("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom")))) ("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock")))) ("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get()))) }); atom_command!(PoolClipCommand: |state: MidiPool| { ("add" [i: usize, c: MidiClip] Some(Self::Add(i.expect("no index"), c.expect("no clip")))) ("delete" [i: usize] Some(Self::Delete(i.expect("no index")))) ("swap" [a: usize, b: usize] Some(Self::Swap(a.expect("no index"), b.expect("no index")))) ("import" [i: usize, p: PathBuf] Some(Self::Import(i.expect("no index"), p.expect("no path")))) ("export" [i: usize, p: PathBuf] Some(Self::Export(i.expect("no index"), p.expect("no path")))) ("set-name" [i: usize, n: Arc] Some(Self::SetName(i.expect("no index"), n.expect("no name")))) ("set-length" [i: usize, l: usize] Some(Self::SetLength(i.expect("no index"), l.expect("no length")))) ("set-color" [i: usize, c: ItemColor] Some(Self::SetColor(i.expect("no index"), c.expect("no color")))) }); // TODO: 1-9 seek markers that by default start every 8th of the clip defcom!([self, state: MidiEditor] (MidiEditCommand (AppendNote [] { state.put_note(true); None }) (PutNote [] { state.put_note(false); None }) (DelNote [] { None }) (SetNoteCursor [x: usize] { state.set_note_pos(x.min(127)); None }) (SetNoteLength [x: usize] { let note_len = state.note_len(); let time_zoom = state.time_zoom().get(); state.set_note_len(x); //if note_len / time_zoom != x / time_zoom { state.redraw(); //} None }) (SetNoteScroll [x: usize] { state.note_lo().set(x.min(127)); None }) (SetTimeCursor [x: usize] { state.set_time_pos(x); None }) (SetTimeScroll [x: usize] { state.time_start().set(x); None }) (SetTimeZoom [x: usize] { state.time_zoom().set(x); state.redraw(); None }) (SetTimeLock [x: bool] { state.time_lock().set(x); None }) (Show [x: MaybeClip] { state.set_clip(x.as_ref()); None }))); #[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand { Begin, Cancel, Confirm, Set(Arc), } command!(|self: ClipRenameCommand, state: MidiPool|if let Some( PoolMode::Rename(clip, ref mut old_name) ) = state.mode_mut().clone() { match self { Self::Set(s) => { state.clips()[clip].write().unwrap().name = s; return Ok(Some(Self::Set(old_name.clone().into()))) }, Self::Confirm => { let old_name = old_name.clone(); *state.mode_mut() = None; return Ok(Some(Self::Set(old_name))) }, Self::Cancel => { state.clips()[clip].write().unwrap().name = old_name.clone().into(); return Ok(None) }, _ => unreachable!() } } else { unreachable!() }); command!(|self: FileBrowserCommand, state: MidiPool|{ use PoolMode::*; use FileBrowserCommand::*; let mode = &mut state.mode; match mode { Some(Import(index, ref mut browser)) => match self { Cancel => { *mode = None; }, Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); }, Select(index) => { browser.index = index; }, Confirm => if browser.is_file() { let index = *index; let path = browser.path(); *mode = None; PoolClipCommand::Import(index, path).execute(state)?; } else if browser.is_dir() { *mode = Some(Import(*index, browser.chdir()?)); }, _ => todo!(), }, Some(Export(index, ref mut browser)) => match self { Cancel => { *mode = None; }, Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); }, Select(index) => { browser.index = index; }, _ => unreachable!() }, _ => unreachable!(), }; None }); #[derive(Copy, Clone, Debug, PartialEq)] pub enum ClipLengthCommand { Begin, Cancel, Set(usize), Next, Prev, Inc, Dec, } command!(|self: ClipLengthCommand, state: MidiPool|{ use ClipLengthCommand::*; use ClipLengthFocus::*; if let Some( PoolMode::Length(clip, ref mut length, ref mut focus) ) = state.mode_mut().clone() { match self { Cancel => { *state.mode_mut() = None; }, Prev => { focus.prev() }, Next => { focus.next() }, Inc => match focus { Bar => { *length += 4 * PPQ }, Beat => { *length += PPQ }, Tick => { *length += 1 }, }, Dec => match focus { Bar => { *length = length.saturating_sub(4 * PPQ) }, Beat => { *length = length.saturating_sub(PPQ) }, Tick => { *length = length.saturating_sub(1) }, }, Set(length) => { let old_length; { let clip = state.clips()[clip].clone();//.write().unwrap(); old_length = Some(clip.read().unwrap().length); clip.write().unwrap().length = length; } *state.mode_mut() = None; return Ok(old_length.map(Self::Set)) }, _ => unreachable!() } } else { unreachable!(); } None });