diff --git a/crates/app/src/api.rs b/crates/app/src/api.rs index 7d5d0851..b0db4c6f 100644 --- a/crates/app/src/api.rs +++ b/crates/app/src/api.rs @@ -256,111 +256,142 @@ impl MidiEditor { } } -//#[tengri_proc::input(TuiIn)] -//impl Tek { - //#[tengri::command("sampler", TekCommand::Sampler)] - //fn cmd_sampler (&mut self, cmd: SamplerCommand) -> Perhaps { - //self.sampler_mut().map(|s|cmd.delegate(s, Self::Sampler)).transpose()?.flatten()) - //} - //#[tengri::command("scene", TekCommand::Scene)] - //fn cmd_scene (&mut self, cmd: SceneCommand) -> Perhaps { - //cmd.delegate(self, scene) - //} -//} - #[tengri_proc::command(Tek)] impl TekCommand { - fn toggle_help (&self, state: &mut Tek, value: Option) -> Option { - //(ToggleHelp [] cmd!(app.toggle_dialog(Some(Dialog::Help)))) - None + fn toggle_help (&self, tek: &mut Tek, value: bool) -> Perhaps { + tek.toggle_dialog(Some(Dialog::Help)); + Ok(None) } - fn toggle_menu (&self, state: &mut Tek, value: Option) -> Option { - //(ToggleMenu [] cmd!(app.toggle_dialog(Some(Dialog::Menu)))) - None + fn toggle_menu (&self, tek: &mut Tek, value: bool) -> Perhaps { + tek.toggle_dialog(Some(Dialog::Menu)); + Ok(None) } - fn toggle_edit (&self, state: &mut Tek, value: Option) -> Option { - //(Edit [value: Option] cmd!(app.toggle_editor(value))) - None + fn toggle_edit (&self, tek: &mut Tek, value: bool) -> Perhaps { + tek.toggle_editor(Some(value)); + Ok(None) } - //(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)?) - //(Device [cmd: DeviceCommand] cmd.delegate(app, Self::Device)?) - //(Message [cmd: MessageCommand] cmd.delegate(app, Self::Message)?) - //(Editor [cmd: MidiEditCommand] delegate_to_editor(app, cmd)?) - //(Pool [cmd: PoolCommand] delegate_to_pool(app, cmd)?) - //(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:?}")) - //(Launch [] cmd!(app.launch())) - //(Select [s: Selection] cmd!(app.select(s))) - //(StopAll [] cmd!(app.stop_all()))) - //("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)) - //("device" [,..a] ns!(DeviceCommand, app, a, Self::Device)) - //("message" [,..a] ns!(MessageCommand, app, a, Self::Message)) - //("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()) + fn editor (&self, tek: &mut Tek, command: MidiEditCommand) -> Perhaps { + Ok(tek.editor.as_mut().map(|editor|command.execute(editor)) + .transpose()? + .flatten() + .map(|undo|Self::Editor { command: undo })) + } + fn pool (&self, tek: &mut Tek, command: PoolCommand) -> Perhaps { + Ok(if let Some(pool) = tek.pool.as_mut() { + let undo = command.clone().delegate(pool, |command|TekCommand::Pool{command})?; + // update linked editor after pool action + tek.editor.as_mut().map(|editor|match command { + // autoselect: automatically load selected clip in editor + PoolCommand::Select { .. } | + // autocolor: update color in all places simultaneously + PoolCommand::Clip { command: PoolClipCommand::SetColor { .. } } => + editor.set_clip(pool.clip().as_ref()), + _ => {} + }); + undo + } else { + None + }) + } + fn sampler (&self, tek: &mut Tek, ref command: SamplerCommand) -> Perhaps { + Ok(tek.sampler_mut() + .map(|s|command.delegate(s, |command|Self::Sampler{command})) + .transpose()? + .flatten()) + } + fn scene (&self, tek: &mut Tek, command: SceneCommand) -> Perhaps { + Ok(command.delegate(tek, |command|Self::Scene{command})?) + } + fn track (&self, tek: &mut Tek, command: TrackCommand) -> Perhaps { + Ok(command.delegate(tek, |command|Self::Track{command})?) + } + fn input (&self, tek: &mut Tek, command: InputCommand) -> Perhaps { + Ok(command.delegate(tek, |command|Self::Input{command})?) + } + fn output (&self, tek: &mut Tek, command: OutputCommand) -> Perhaps { + Ok(command.delegate(tek, |command|Self::Output{command})?) + } + fn clip (&self, tek: &mut Tek, command: ClipCommand) -> Perhaps { + Ok(command.delegate(tek, |command|Self::Clip{command})?) + } + fn clock (&self, tek: &mut Tek, command: ClockCommand) -> Perhaps { + Ok(command.delegate(tek, |command|Self::Clock{command})?) + } + fn device (&self, tek: &mut Tek, command: DeviceCommand) -> Perhaps { + Ok(command.delegate(tek, |command|Self::Device{command})?) + } + fn message (&self, tek: &mut Tek, command: MessageCommand) -> Perhaps { + Ok(command.delegate(tek, |command|Self::Message{command})?) + } + fn color (&self, tek: &mut Tek, theme: ItemTheme) -> Perhaps { + Ok(tek.set_color(Some(theme)).map(|theme|Self::Color{theme})) + } + fn enqueue (&self, tek: &mut Tek, clip: Option>>) -> Perhaps { + todo!() + } + fn history (&self, tek: &mut Tek, delta: isize) -> Perhaps { + todo!() + } + fn zoom (&self, tek: &mut Tek, zoom: usize) -> Perhaps { + todo!() + } + fn launch (&self, tek: &mut Tek) -> Perhaps { + tek.launch(); + Ok(None) + } + fn select (&self, tek: &mut Tek, selection: Selection) -> Perhaps { + tek.select(selection); + Ok(None) //("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 }) }))) + } + fn stop_all (&self, tek: &mut Tek) -> Perhaps { + tek.stop_all(); + Ok(None) + } } #[tengri_proc::command(Tek)] impl InputCommand { - fn add (&self, state: &mut Tek) -> Option { - state.midi_in_add()?; - None + fn add (&self, tek: &mut Tek) -> Perhaps { + tek.midi_in_add()?; + Ok(None) } } #[tengri_proc::command(Tek)] impl OutputCommand { - fn add (&self, state: &mut Tek) -> Option { - state.midi_out_add()?; - None + fn add (&self, tek: &mut Tek) -> Perhaps { + tek.midi_out_add()?; + Ok(None) } } #[tengri_proc::command(Tek)] impl DeviceCommand { - //(Picker [] cmd!(app.device_picker_show())) - //(Pick [i: usize] cmd!(app.device_pick(i))) - //(Add [i: usize] cmd!(app.device_add(i)))) - //("picker" [] Some(Self::Picker)) - //("pick" [index: usize] Some(Self::Pick(index.unwrap()))) - //("add" [index: usize] Some(Self::Add(index.unwrap())))) + fn picker (&self, tek: &mut Tek) -> Perhaps { + tek.device_picker_show(); + Ok(None) + } + fn pick (&self, tek: &mut Tek, i: usize) -> Perhaps { + tek.device_pick(i); + Ok(None) + } + fn add (&self, tek: &mut Tek, i: usize) -> Perhaps { + tek.device_add(i); + Ok(None) + } } #[tengri_proc::command(Tek)] impl MessageCommand { - //(Dismiss [] cmd!(app.message_dismiss()))) - //("dismiss" [] Some(Self::Dismiss))) + fn dismiss (&self, tek: &mut Tek) -> Perhaps { + tek.message_dismiss(); + Ok(None) + } } #[tengri_proc::command(Tek)] @@ -426,332 +457,323 @@ impl ClipCommand { //("delete" [a: usize, b: usize] Some(Self::Put(a.unwrap(), b.unwrap(), None)))) } -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 { +#[tengri_proc::command(MidiPool)] +impl PoolCommand { /// Toggle visibility of pool - Show(bool), + fn show (&self, pool: &mut MidiPool, visible: bool) -> Perhaps { + pool.visible = visible; + Ok(Some(Self::Show(!visible))) + } /// Select a clip from the clip pool - Select(usize), + fn select (&self, pool: &mut MidiPool, index: usize) -> Perhaps { + pool.set_clip_index(index); + Ok(None) + } /// Rename a clip - Rename(ClipRenameCommand), + fn rename (&self, pool: &mut MidiPool, command: ClipRenameCommand) -> Perhaps { + Ok(match command { + ClipRenameCommand::Begin => { + pool.begin_clip_rename(); + None + }, + _ => command.delegate(pool, |command|Self::Rename{command})? + }) + } /// Change the length of a clip - Length(ClipLengthCommand), + fn length (&self, pool: &mut MidiPool, command: ClipLengthCommand) -> Perhaps { + Ok(match command { + ClipLengthCommand::Begin => { + pool.begin_clip_length(); + None + }, + _ => command.delegate(pool, |command|Self::Length{command})? + }) + } /// Import from file - Import(FileBrowserCommand), + fn import (&self, pool: &mut MidiPool, command: FileBrowserCommand) -> Perhaps { + Ok(match command { + ClipImportCommand::Begin => { + pool.begin_import(); + None + }, + _ => command.delegate(pool, |command|Self::Import{command})? + }) + } /// Export to file - Export(FileBrowserCommand), + fn export (&self, pool: &mut MidiPool, command: FileBrowserCommand) -> Perhaps { + Ok(match command { + ClipExportCommand::Begin => { + pool.begin_export(); + None + }, + _ => command.delegate(pool, |command|Self::Export{command})? + }) + } /// 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)) - }, - }) + fn clip (&self, pool: &mut MidiPool, command: PoolClipCommand) -> Perhaps { + command.execute(pool)?.map(|command|Self::Clip{command}) } } -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!() +#[tengri_proc::command(MidiPool)] +impl PoolClipCommand { + fn add (&self, pool: &mut MidiPool, index: usize, clip: MidiClip) -> Perhaps { + let mut index = index; + let clip = Arc::new(RwLock::new(clip)); + let mut clips = pool.clips_mut(); + if index >= clips.len() { + index = clips.len(); + clips.push(clip) + } else { + clips.insert(index, clip); } - } else { + Ok(Some(Self::Delete(index))) + } + fn delete (&self, pool: &mut MidiPool, index: usize) -> Perhaps { + let clip = pool.clips_mut().remove(index).read().unwrap().clone(); + Ok(Some(Self::Add(index, clip))) + } + fn swap (&self, pool: &mut MidiPool, index: usize, other: usize) -> Perhaps { + pool.clips_mut().swap(index, other); + Ok(Some(Self::Swap(index, other))) + } + fn import (&self, pool: &mut MidiPool, index: usize, path: PathBuf) -> Perhaps { + 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(pool)? + } + fn export (&self, pool: &mut MidiPool, index: usize, path: PathBuf) -> Perhaps { + todo!("export clip to midi file"); + } + fn set_name (&self, pool: &mut MidiPool, index: usize, name: Arc) -> Perhaps { + let clip = &mut pool.clips_mut()[index]; + let old_name = clip.read().unwrap().name.clone(); + clip.write().unwrap().name = name; + Ok(Some(Self::SetName(index, old_name))) + } + fn set_length (&self, pool: &mut MidiPool, index: usize, length: usize) -> Perhaps { + let clip = &mut pool.clips_mut()[index]; + let old_len = clip.read().unwrap().length; + clip.write().unwrap().length = length; + Ok(Some(Self::SetLength(index, old_len))) + } + fn set_color (&self, pool: &mut MidiPool, index: usize, color: ItemColor) -> Perhaps { + let mut color = ItemTheme::from(color); + std::mem::swap(&mut color, &mut pool.clips()[index].write().unwrap().color); + Ok(Some(Self::SetColor(index, color.base))) + } +} + +#[tengri_proc::command(MidiPool)] +impl ClipRenameCommand { + fn begin (&self, pool: &mut MidiPool) -> Perhaps { unreachable!(); } - None -}); + fn cancel (&self, pool: &mut MidiPool) -> Perhaps { + if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = old_name.clone().into(); + } + return Ok(None) + } + fn confirm (&self, pool: &mut MidiPool) -> Perhaps { + if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { + let old_name = old_name.clone(); + pool.mode_mut() = None; + return Ok(Some(Self::Set(old_name))) + } + return Ok(None) + } + fn set (&self, pool: &mut MidiPool, value: Arc) -> Perhaps { + if let Some(PoolMode::Rename(clip, ref mut old_name)) = pool.mode_mut().clone() { + pool.clips()[clip].write().unwrap().name = value; + } + return Ok(None) + } +} + +#[tengri_proc::command(MidiPool)] +impl ClipLengthCommand { + fn begin (&self, pool: &mut MidiPool) -> Perhaps { + unreachable!() + } + fn cancel (&self, pool: &mut MidiPool) -> Perhaps { + if let Some(PoolMode::Length(..)) = pool.mode_mut().clone() { + *pool.mode_mut() = None; + } + Ok(None) + } + fn set (&self, pool: &mut MidiPool, length: usize) -> Perhaps { + if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) + = pool.mode_mut().clone() + { + let old_length; + { + let clip = pool.clips()[clip].clone();//.write().unwrap(); + old_length = Some(clip.read().unwrap().length); + clip.write().unwrap().length = length; + } + pool.mode_mut() = None; + return Ok(old_length.map(|length|Self::Set { length })) + } + Ok(None) + } + fn next (&self, pool: &mut MidiPool) -> Perhaps { + if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) + = pool.mode_mut().clone() + { + focus.next() + } + Ok(None) + } + fn prev (&self, pool: &mut MidiPool) -> Perhaps { + if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) + = pool.mode_mut().clone() + { + focus.prev() + } + Ok(None) + } + fn inc (&self, pool: &mut MidiPool) -> Perhaps { + if let Some(PoolMode::Length(clip, ref mut length, ref mut focus)) + = pool.mode_mut().clone() + { + match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + } + } + Ok(None) + } + fn dec (&self, pool: &mut MidiPool) -> Perhaps { + if let Some( + PoolMode::Length(clip, ref mut length, ref mut focus) + ) = pool.mode_mut().clone() { + match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + } + } + Ok(None) + } +} + +#[tengri_proc::command(MidiPool)] +impl FileBrowserCommand { + fn begin (&self, pool: &mut MidiPool) -> Perhaps { + unreachable!(); + } + fn cancel (&self, pool: &mut MidiPool) -> Perhaps { + pool.mode = None; + Ok(None) + } + fn confirm (&self, pool: &mut MidiPool) -> Perhaps { + Ok(match pool.mode { + Some(PoolMode::Import(index, ref mut browser)) => { + if browser.is_file() { + let index = *index; + let path = browser.path(); + pool.mode = None; + PoolClipCommand::Import(index, path).execute(pool)?; + } else if browser.is_dir() { + pool.mode = Some(PoolMode::Import(*index, browser.chdir()?)); + } + }, + Some(PoolMode::Export(index, ref mut browser)) => match self { + Cancel => { pool.mode = None; }, + _ => unreachable!() + }, + _ => unreachable!(), + }) + } + fn select (&self, pool: &mut MidiPool, index: usize) -> Perhaps { + Ok(match pool.mode { + Some(PoolMode::Import(index, ref mut browser)) => { browser.index = index; }, + Some(PoolMode::Export(index, ref mut browser)) => { browser.index = index; }, + _ => unreachable!(), + }) + } + fn chdir (&self, pool: &mut MidiPool, dir: PathBuf) -> Perhaps { + Ok(match pool.mode { + Some(PoolMode::Import(index, ref mut browser)) => { + pool.mode = Some(PoolMode::Import(*index, FileBrowser::new(Some(dir))?)); + }, + Some(PoolMode::Export(index, ref mut browser)) => { + pool.mode = Some(PoolMode::Export(*index, FileBrowser::new(Some(dir))?)); + }, + _ => unreachable!(), + }) + } + fn filter (&self, pool: &mut MidiPool, filter: Arc) -> Perhaps { + todo!() + } +} + +#[tengri_proc::command(MidiEditor)] +impl MidiEditCommand { + // TODO: 1-9 seek markers that by default start every 8th of the clip + fn note_append (&self, editor: &mut MidiEditor) -> Perhaps { + editor.put_note(true); + Ok(None) + } + fn note_put (&self, editor: &mut MidiEditor) -> Perhaps { + editor.put_note(false); + Ok(None) + } + fn note_del (&self, editor: &mut MidiEditor) -> Perhaps { + todo!() + } + fn note_pos (&self, editor: &mut MidiEditor, pos: usize) -> Perhaps { + editor.set_note_pos(pos.min(127)); + Ok(None) + } + fn note_len (&self, editor: &mut MidiEditor, value: usize) -> Perhaps { + //let note_len = editor.get_note_len(); + //let time_zoom = editor.get_time_zoom(); + editor.set_note_len(value); + //if note_len / time_zoom != x / time_zoom { + editor.redraw(); + //} + Ok(None) + } + fn note_scroll (&self, editor: &mut MidiEditor, value: usize) { + editor.set_note_lo(value.min(127)); + Ok(None) + } + fn time_pos (&self, editor: &mut MidiEditor, value: usize) -> Perhaps { + editor.set_time_pos(value); + Ok(None) + } + fn time_scroll (&self, editor: &mut MidiEditor, value: usize) -> Perhaps { + editor.set_time_start(value); + Ok(None) + } + fn time_zoom (&self, editor: &mut MidiEditor, value: usize) -> Perhaps { + editor.time_zoom().set(value); + editor.redraw(); + Ok(None) + } + fn time_lock (&self, editor: &mut MidiEditor, value: bool) -> Perhaps { + editor.set_time_lock(value); + Ok(None) + } + fn show (&self, editor: &mut MidiEditor, clip: Option>>) -> Perhaps { + editor.set_clip(clip.as_ref()); + Ok(None) + } +} diff --git a/crates/device/src/clock/clock_api.rs b/crates/device/src/clock/clock_api.rs index a2e5927f..097867fc 100644 --- a/crates/device/src/clock/clock_api.rs +++ b/crates/device/src/clock/clock_api.rs @@ -1,55 +1,52 @@ use crate::*; -#[derive(Clone, Debug, PartialEq)] -pub enum ClockCommand { - Play(Option), - Pause(Option), - SeekUsec(f64), - SeekSample(f64), - SeekPulse(f64), - SetBpm(f64), - SetQuant(f64), - SetSync(f64), -} - provide_num!(u32: |self: Clock| {}); - +provide!(Option: |self: Clock| {}); provide!(f64: |self: Clock| {}); -atom_command!(ClockCommand: |state: Clock| { - ("play" [] Some(Self::Play(None))) - ("play" [t: u32] Some(Self::Play(t))) - ("pause" [] Some(Self::Pause(None))) - ("pause" [t: u32] Some(Self::Pause(t))) - ("toggle" [] Some(if state.is_rolling() { Self::Pause(None) } else { Self::Play(None) })) - ("toggle" [t: u32] Some(if state.is_rolling() { Self::Pause(t) } else { Self::Play(t) })) - ("seek/usec" [t: f64] Some(Self::SeekUsec(t.expect("no usec")))) - ("seek/pulse" [t: f64] Some(Self::SeekPulse(t.expect("no pulse")))) - ("seek/sample" [t: f64] Some(Self::SeekSample(t.expect("no sample")))) - ("set/bpm" [t: f64] Some(Self::SetBpm(t.expect("no bpm")))) - ("set/sync" [t: f64] Some(Self::SetSync(t.expect("no sync")))) - ("set/quant" [t: f64] Some(Self::SetQuant(t.expect("no quant")))) -}); - impl Command for ClockCommand { fn execute (self, state: &mut T) -> Perhaps { - self.execute(state.clock_mut()) + self.execute(state.clock_mut()) // awesome } } -impl Command for ClockCommand { - fn execute (self, state: &mut Clock) -> Perhaps { - use ClockCommand::*; - match self { - Play(start) => state.play_from(start)?, - Pause(pause) => state.pause_at(pause)?, - SeekUsec(usec) => state.playhead.update_from_usec(usec), - SeekSample(sample) => state.playhead.update_from_sample(sample), - SeekPulse(pulse) => state.playhead.update_from_pulse(pulse), - SetBpm(bpm) => return Ok(Some(SetBpm(state.timebase().bpm.set(bpm)))), - SetQuant(quant) => return Ok(Some(SetQuant(state.quant.set(quant)))), - SetSync(sync) => return Ok(Some(SetSync(state.sync.set(sync)))), - }; +#[tengri_proc::command(Clock)] +impl ClockCommand { + fn play (self, state: &mut Clock, position: Option) -> Perhaps { + state.play_from(position)?; + Ok(None) // TODO Some(Pause(previousPosition)) + } + fn pause (self, state: &mut Clock, position: Option) -> Perhaps { + state.pause_at(position)?; Ok(None) } + fn toggle_playback (self, state: &mut Clock, position: Option) -> Perhaps { + if state.is_rolling() { + state.pause_at(position)?; + } else { + state.play_from(position)?; + } + Ok(None) + } + fn seek_usec (self, state: &mut Clock, usec: f64) -> Perhaps { + state.playhead.update_from_usec(usec); + Ok(None) + } + fn seek_sample (self, state: &mut Clock, sample: f64) -> Perhaps { + state.playhead.update_from_sample(sample); + Ok(None) + } + fn seek_pulse (self, state: &mut Clock, pulse: f64) -> Perhaps { + state.playhead.update_from_pulse(pulse); + Ok(None) + } + fn set_bpm (self, state: &mut Clock, bpm: f64) -> Perhaps { + Ok(Some(Self::SetBpm { bpm: state.timebase().bpm.set(bpm) })) + } + fn set_quant (self, state: &mut Clock, quant: f64) -> Perhaps { + Ok(Some(Self::SetQuant { quant: state.quant.set(quant) })) + } + fn set_sync (self, state: &mut Clock, sync: f64) -> Perhaps { + Ok(Some(Self::SetSync { sync: state.sync.set(sync) })) + } } diff --git a/deps/tengri b/deps/tengri index fa10f7d4..2a6087e1 160000 --- a/deps/tengri +++ b/deps/tengri @@ -1 +1 @@ -Subproject commit fa10f7d4d36924beae83d2136d180c2006508ae9 +Subproject commit 2a6087e1c7086f09b1ade22c84ff62642df7c723