use crate::*; pub type ClipPool = Vec>>; pub trait HasClips { fn clips <'a> (&'a self) -> std::sync::RwLockReadGuard<'a, ClipPool>; fn clips_mut <'a> (&'a self) -> std::sync::RwLockWriteGuard<'a, ClipPool>; fn add_clip (&self) -> (usize, Arc>) { let clip = Arc::new(RwLock::new(MidiClip::new("Clip", true, 384, None, None))); self.clips_mut().push(clip.clone()); (self.clips().len() - 1, clip) } } #[macro_export] macro_rules! has_clips { (|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => { impl $(<$($L),*$($T $(: $U)?),*>)? HasClips for $Struct $(<$($L),*$($T),*>)? { fn clips <'a> (&'a $self) -> std::sync::RwLockReadGuard<'a, ClipPool> { $cb.read().unwrap() } fn clips_mut <'a> (&'a $self) -> std::sync::RwLockWriteGuard<'a, ClipPool> { $cb.write().unwrap() } } } } #[derive(Debug)] pub struct MidiPool { pub visible: bool, /// Collection of clips pub clips: Arc>>>>, /// Selected clip pub clip: AtomicUsize, /// Mode switch pub mode: Option, } /// Modes for clip pool #[derive(Debug, Clone)] pub enum PoolMode { /// Renaming a pattern Rename(usize, Arc), /// Editing the length of a pattern Length(usize, usize, ClipLengthFocus), /// Load clip from disk Import(usize, FileBrowser), /// Save clip to disk Export(usize, FileBrowser), } impl Default for MidiPool { fn default () -> Self { Self { visible: true, clips: Arc::from(RwLock::from(vec![])), clip: 0.into(), mode: None, } } } from!(|clip:&Arc>|MidiPool = { let model = Self::default(); model.clips.write().unwrap().push(clip.clone()); model.clip.store(1, Relaxed); model }); has_clips!(|self: MidiPool|self.clips); has_clip!(|self: MidiPool|self.clips().get(self.clip_index()).map(|c|c.clone())); impl MidiPool { fn clip_index (&self) -> usize { self.clip.load(Relaxed) } fn set_clip_index (&self, value: usize) { self.clip.store(value, Relaxed); } fn mode (&self) -> &Option { &self.mode } fn mode_mut (&mut self) -> &mut Option { &mut self.mode } fn begin_clip_length (&mut self) { let length = self.clips()[self.clip_index()].read().unwrap().length; *self.mode_mut() = Some(PoolMode::Length( self.clip_index(), length, ClipLengthFocus::Bar )); } fn begin_clip_rename (&mut self) { let name = self.clips()[self.clip_index()].read().unwrap().name.clone(); *self.mode_mut() = Some(PoolMode::Rename( self.clip_index(), name )); } fn begin_import (&mut self) -> Usually<()> { *self.mode_mut() = Some(PoolMode::Import( self.clip_index(), FileBrowser::new(None)? )); Ok(()) } fn begin_export (&mut self) -> Usually<()> { *self.mode_mut() = Some(PoolMode::Export( self.clip_index(), FileBrowser::new(None)? )); Ok(()) } } /// Displays and edits clip length. #[derive(Clone)] pub struct ClipLength { /// Pulses per beat (quaver) ppq: usize, /// Beats per bar bpb: usize, /// Length of clip in pulses pulses: usize, /// Selected subdivision focus: Option, } impl ClipLength { fn new (pulses: usize, focus: Option) -> Self { Self { ppq: PPQ, bpb: 4, pulses, focus } } fn bars (&self) -> usize { self.pulses / (self.bpb * self.ppq) } fn beats (&self) -> usize { (self.pulses % (self.bpb * self.ppq)) / self.ppq } fn ticks (&self) -> usize { self.pulses % self.ppq } fn bars_string (&self) -> Arc { format!("{}", self.bars()).into() } fn beats_string (&self) -> Arc { format!("{}", self.beats()).into() } fn ticks_string (&self) -> Arc { format!("{:>02}", self.ticks()).into() } } /// Focused field of `ClipLength` #[derive(Copy, Clone, Debug)] pub enum ClipLengthFocus { /// Editing the number of bars Bar, /// Editing the number of beats Beat, /// Editing the number of ticks Tick, } impl ClipLengthFocus { fn next (&mut self) { *self = match self { Self::Bar => Self::Beat, Self::Beat => Self::Tick, Self::Tick => Self::Bar, } } fn prev (&mut self) { *self = match self { Self::Bar => Self::Tick, Self::Beat => Self::Bar, Self::Tick => Self::Beat, } } } pub struct PoolView<'a>(pub bool, pub &'a MidiPool); content!(TuiOut: |self: PoolView<'a>| { let Self(compact, model) = self; let MidiPool { clips, .. } = self.1; //let color = self.1.clip().map(|c|c.read().unwrap().color).unwrap_or_else(||TuiTheme::g(32).into()); let on_bg = |x|x;//Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, x)); let border = |x|x;//Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)).enclose(x); let iter = | |model.clips().clone().into_iter(); Tui::bg(Color::Reset, Fixed::y(clips.read().unwrap().len() as u16, on_bg(border(Map::new(iter, move|clip, i|{ let item_height = 1; let item_offset = i as u16 * item_height; let selected = i == model.clip_index(); let MidiClip { ref name, color, length, .. } = *clip.read().unwrap(); let bg = if selected { color.light.rgb } else { color.base.rgb }; let fg = color.lightest.rgb; let name = if *compact { format!(" {i:>3}") } else { format!(" {i:>3} {name}") }; let length = if *compact { String::default() } else { format!("{length} ") }; Fixed::y(1, map_south(item_offset, item_height, Tui::bg(bg, lay!( Fill::x(Align::w(Tui::fg(fg, Tui::bold(selected, name)))), Fill::x(Align::e(Tui::fg(fg, Tui::bold(selected, length)))), Fill::x(Align::w(When::new(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶"))))), Fill::x(Align::e(When::new(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀"))))), )))) }))))) }); content!(TuiOut: |self: ClipLength| { let bars = ||self.bars_string(); let beats = ||self.beats_string(); let ticks = ||self.ticks_string(); match self.focus { None => row!(" ", bars(), ".", beats(), ".", ticks()), Some(ClipLengthFocus::Bar) => row!("[", bars(), "]", beats(), ".", ticks()), Some(ClipLengthFocus::Beat) => row!(" ", bars(), "[", beats(), "]", ticks()), Some(ClipLengthFocus::Tick) => row!(" ", bars(), ".", beats(), "[", ticks()), } }); impl PoolCommand { pub fn from_tui_event (state: &MidiPool, input: &impl EdnInput) -> Usually> { use EdnItem::*; let edns: Vec> = EdnItem::read_all(match state.mode() { Some(PoolMode::Rename(..)) => KEYS_RENAME, Some(PoolMode::Length(..)) => KEYS_LENGTH, Some(PoolMode::Import(..)) | Some(PoolMode::Export(..)) => KEYS_FILE, _ => KEYS_POOL })?; for item in edns { match item { Exp(e) => match e.as_slice() { [Sym(key), command, args @ ..] if input.matches_edn(key) => { return Ok(PoolCommand::from_edn(state, command, args)) } _ => {} }, _ => panic!("invalid config") } } Ok(None) } } handle!(TuiIn: |self: MidiPool, input|{ Ok(if let Some(command) = PoolCommand::from_tui_event(self, input)? { let _undo = command.execute(self)?; Some(true) } else { None }) }); edn_provide!(bool: |self: MidiPool| {}); impl MidiPool { pub fn new_clip (&self) -> MidiClip { MidiClip::new("Clip", true, 4 * PPQ, None, Some(ItemPalette::random())) } pub fn cloned_clip (&self) -> MidiClip { let index = self.clip_index(); let mut clip = self.clips()[index].read().unwrap().duplicate(); clip.color = ItemPalette::random_near(clip.color, 0.25); clip } pub fn add_new_clip (&self) -> (usize, Arc>) { let clip = Arc::new(RwLock::new(self.new_clip())); let index = { let mut clips = self.clips.write().unwrap(); clips.push(clip.clone()); clips.len().saturating_sub(1) }; self.clip.store(index, Relaxed); (index, clip) } } edn_provide!(MidiClip: |self: MidiPool| { ":new-clip" => self.new_clip(), ":cloned-clip" => self.cloned_clip(), }); edn_provide!(PathBuf: |self: MidiPool| {}); edn_provide!(Arc: |self: MidiPool| {}); edn_provide!(usize: |self: MidiPool| { ":current" => 0, ":after" => 0, ":previous" => 0, ":next" => 0 }); edn_provide!(ItemColor: |self: MidiPool| { ":random-color" => ItemColor::random() }); #[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), } edn_command!(PoolCommand: |state: MidiPool| { ("show" [a: bool] Self::Show(a.expect("no flag"))) ("select" [i: usize] Self::Select(i.expect("no index"))) ("rename" [a, ..b] ClipRenameCommand::from_edn(state, &a.to_ref(), b).map(Self::Rename).expect("invalid command")) ("length" [a, ..b] ClipLengthCommand::from_edn(state, &a.to_ref(), b).map(Self::Length).expect("invalid command")) ("import" [a, ..b] FileBrowserCommand::from_edn(state, &a.to_ref(), b).map(Self::Import).expect("invalid command")) ("export" [a, ..b] FileBrowserCommand::from_edn(state, &a.to_ref(), b).map(Self::Export).expect("invalid command")) ("clip" [a, ..b] PoolClipCommand::from_edn(state, &a.to_ref(), b).map(Self::Clip).expect("invalid command")) }); 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), } edn_command!(PoolClipCommand: |state: MidiPool| { ("add" [i: usize, c: MidiClip] Self::Add(i.expect("no index"), c.expect("no clip"))) ("delete" [i: usize] Self::Delete(i.expect("no index"))) ("swap" [a: usize, b: usize] Self::Swap(a.expect("no index"), b.expect("no index"))) ("import" [i: usize, p: PathBuf] Self::Import(i.expect("no index"), p.expect("no path"))) ("export" [i: usize, p: PathBuf] Self::Export(i.expect("no index"), p.expect("no path"))) ("set-name" [i: usize, n: Arc] Self::SetName(i.expect("no index"), n.expect("no name"))) ("set-length" [i: usize, l: usize] Self::SetLength(i.expect("no index"), l.expect("no length"))) ("set-color" [i: usize, c: ItemColor] Self::SetColor(i.expect("no index"), c.expect("no color"))) }); 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 = ItemPalette::from(color); std::mem::swap(&mut color, &mut model.clips()[index].write().unwrap().color); Some(Self::SetColor(index, color.base)) }, }) } } #[derive(Clone, Debug, PartialEq)] pub enum ClipRenameCommand { Begin, Cancel, Confirm, Set(Arc), } edn_command!(ClipRenameCommand: |state: MidiPool| { ("begin" [] Self::Begin) ("cancel" [] Self::Cancel) ("confirm" [] Self::Confirm) ("set" [n: Arc] Self::Set(n.expect("no name"))) }); command!(|self: ClipRenameCommand, state: MidiPool|{ use ClipRenameCommand::*; if let Some( PoolMode::Rename(clip, ref mut old_name) ) = state.mode_mut().clone() { match self { Set(s) => { state.clips()[clip].write().unwrap().name = s; return Ok(Some(Self::Set(old_name.clone().into()))) }, Confirm => { let old_name = old_name.clone(); *state.mode_mut() = None; return Ok(Some(Self::Set(old_name))) }, Cancel => { state.clips()[clip].write().unwrap().name = old_name.clone().into(); return Ok(None) }, _ => unreachable!() } } else { unreachable!() } }); #[derive(Copy, Clone, Debug, PartialEq)] pub enum ClipLengthCommand { Begin, Cancel, Set(usize), Next, Prev, Inc, Dec, } edn_command!(ClipLengthCommand: |state: MidiPool| { ("begin" [] Self::Begin) ("cancel" [] Self::Cancel) ("next" [] Self::Next) ("prev" [] Self::Prev) ("inc" [] Self::Inc) ("dec" [] Self::Dec) ("set" [l: usize] Self::Set(l.expect("no length"))) }); 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 mut old_length = None; { 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 }); edn_command!(FileBrowserCommand: |state: MidiPool| { ("begin" [] Self::Begin) ("cancel" [] Self::Cancel) ("confirm" [] Self::Confirm) ("select" [i: usize] Self::Select(i.expect("no index"))) ("chdir" [p: PathBuf] Self::Chdir(p.expect("no path"))) ("filter" [f: Arc] Self::Filter(f.expect("no filter"))) }); 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 }); /////////////////////////////////////////////////////////////////////////////////////////////////// //fn to_clips_command (state: &MidiPool, input: &Event) -> Option { //use KeyCode::{Up, Down, Delete, Char}; //use PoolCommand as Cmd; //let index = state.clip_index(); //let count = state.clips().len(); //Some(match input { //kpat!(Char('n')) => Cmd::Rename(ClipRenameCommand::Begin), //kpat!(Char('t')) => Cmd::Length(ClipLengthCommand::Begin), //kpat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin), //kpat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin), //kpat!(Char('c')) => Cmd::Clip(PoolClipCommand::SetColor(index, ItemColor::random())), //kpat!(Char('[')) | kpat!(Up) => Cmd::Select( //index.overflowing_sub(1).0.min(state.clips().len() - 1) //), //kpat!(Char(']')) | kpat!(Down) => Cmd::Select( //index.saturating_add(1) % state.clips().len() //), //kpat!(Char('<')) => if index > 1 { //state.set_clip_index(state.clip_index().saturating_sub(1)); //Cmd::Clip(PoolClipCommand::Swap(index - 1, index)) //} else { //return None //}, //kpat!(Char('>')) => if index < count.saturating_sub(1) { //state.set_clip_index(state.clip_index() + 1); //Cmd::Clip(PoolClipCommand::Swap(index + 1, index)) //} else { //return None //}, //kpat!(Delete) => if index > 0 { //state.set_clip_index(index.min(count.saturating_sub(1))); //Cmd::Clip(PoolClipCommand::Delete(index)) //} else { //return None //}, //kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(PoolClipCommand::Add(count, MidiClip::new( //"Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) //))), //kpat!(Char('i')) => Cmd::Clip(PoolClipCommand::Add(index + 1, MidiClip::new( //"Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) //))), //kpat!(Char('d')) | kpat!(Shift-Char('D')) => { //let mut clip = state.clips()[index].read().unwrap().duplicate(); //clip.color = ItemPalette::random_near(clip.color, 0.25); //Cmd::Clip(PoolClipCommand::Add(index + 1, clip)) //}, //_ => return None //}) //}