diff --git a/cli/tek.rs b/cli/tek.rs index 750a5a30..3de6d8cd 100644 --- a/cli/tek.rs +++ b/cli/tek.rs @@ -141,7 +141,7 @@ pub fn main () -> Usually<()> { let mut player = default_player(jack, Some(&clip))?; player.clock = default_bpm(player.clock); let sampler = default_sampler(jack)?; - jack.connect_ports(&player.midi_outs[0].port, &sampler.midi_in.port)?; + jack.connect_ports(&player.midi_outs[0].port, &sampler.midi_in.as_ref().unwrap().port)?; App::groovebox( jack, (&clip).into(), (&clip).into(), Some(player), &midi_froms, &midi_tos, diff --git a/midi/src/lib.rs b/midi/src/lib.rs index f856bc58..b3d0171a 100644 --- a/midi/src/lib.rs +++ b/midi/src/lib.rs @@ -20,7 +20,7 @@ pub(crate) use ::tek_tui::{ *, tek_input::*, tek_output::*, - crossterm::event::KeyCode, + crossterm::event::*, ratatui::style::{Style, Stylize, Color} }; diff --git a/midi/src/midi_player.rs b/midi/src/midi_player.rs index 9a526ace..eeb1c96e 100644 --- a/midi/src/midi_player.rs +++ b/midi/src/midi_player.rs @@ -23,9 +23,9 @@ pub struct MidiPlayer { /// State of clock and playhead pub clock: Clock, /// Start time and clip being played - pub play_clip: Option<(Moment, Option>>)>, + pub play_clip: Option<(Moment, Option>>)>, /// Start time and next clip - pub next_clip: Option<(Moment, Option>>)>, + pub next_clip: Option<(Moment, Option>>)>, /// Play input through output. pub monitoring: bool, /// Write input to sequence. @@ -45,6 +45,26 @@ pub struct MidiPlayer { /// MIDI output buffer pub note_buf: Vec, } +impl Default for MidiPlayer { + fn default () -> Self { + Self { + play_clip: None, + next_clip: None, + recording: false, + monitoring: false, + overdub: false, + + notes_in: RwLock::new([false;128]).into(), + notes_out: RwLock::new([false;128]).into(), + note_buf: vec![0;8], + reset: true, + + midi_ins: vec![], + midi_outs: vec![], + clock: Clock::default(), + } + } +} impl MidiPlayer { pub fn new ( jack: &Arc>, @@ -56,20 +76,11 @@ impl MidiPlayer { let name = name.as_ref(); let clock = Clock::from(jack); Ok(Self { - play_clip: Some((Moment::zero(&clock.timebase), clip.cloned())), - next_clip: None, - recording: false, - monitoring: false, - overdub: false, - - notes_in: RwLock::new([false;128]).into(), - notes_out: RwLock::new([false;128]).into(), - note_buf: vec![0;8], - reset: true, - midi_ins: vec![JackPort::::new(jack, format!("M/{name}"), midi_from)?,], midi_outs: vec![JackPort::::new(jack, format!("{name}/M"), midi_to)?, ], + play_clip: Some((Moment::zero(&clock.timebase), clip.cloned())), clock, + ..Default::default() }) } pub fn play_status (&self) -> impl Content { diff --git a/midi/src/midi_pool.rs b/midi/src/midi_pool.rs index 627ef941..596fbe85 100644 --- a/midi/src/midi_pool.rs +++ b/midi/src/midi_pool.rs @@ -102,3 +102,493 @@ impl Command for MidiPoolCommand { }) } } + +#[derive(Debug)] +pub struct PoolModel { + pub visible: bool, + /// Collection of clips + pub clips: Arc>>>>, + /// Selected clip + pub clip: AtomicUsize, + /// Mode switch + pub mode: Option, + /// Rendered size + size: Measure, + /// Scroll offset + scroll: usize, +} +impl Default for PoolModel { + fn default () -> Self { + Self { + visible: true, + clips: Arc::from(RwLock::from(vec![])), + clip: 0.into(), + scroll: 0, + mode: None, + size: Measure::new(), + } + } +} +from!(|clip:&Arc>|PoolModel = { + let mut model = Self::default(); + model.clips.write().unwrap().push(clip.clone()); + model.clip.store(1, Relaxed); + model +}); + +pub struct PoolView<'a>(pub bool, pub &'a PoolModel); +render!(TuiOut: (self: PoolView<'a>) => { + let Self(compact, model) = self; + let PoolModel { clips, mode, .. } = 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(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶"))))), + Fill::x(Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀"))))), + )))) + }))))) +}); + +/// 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), +} + +#[derive(Clone, PartialEq, Debug)] +pub enum PoolCommand { + Show(bool), + /// Update the contents of the clip pool + Clip(MidiPoolCommand), + /// 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), +} + +command!(|self:PoolCommand, state: PoolModel|{ + use PoolCommand::*; + match self { + Show(visible) => { + state.visible = visible; + Some(Self::Show(!visible)) + } + Rename(command) => match command { + ClipRenameCommand::Begin => { + let length = state.clips()[state.clip_index()].read().unwrap().length; + *state.clips_mode_mut() = Some( + PoolMode::Length(state.clip_index(), length, ClipLengthFocus::Bar) + ); + None + }, + _ => command.execute(state)?.map(Rename) + }, + Length(command) => match command { + ClipLengthCommand::Begin => { + let name = state.clips()[state.clip_index()].read().unwrap().name.clone(); + *state.clips_mode_mut() = Some( + PoolMode::Rename(state.clip_index(), name) + ); + None + }, + _ => command.execute(state)?.map(Length) + }, + Import(command) => match command { + FileBrowserCommand::Begin => { + *state.clips_mode_mut() = Some( + PoolMode::Import(state.clip_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Import) + }, + Export(command) => match command { + FileBrowserCommand::Begin => { + *state.clips_mode_mut() = Some( + PoolMode::Export(state.clip_index(), FileBrowser::new(None)?) + ); + None + }, + _ => command.execute(state)?.map(Export) + }, + Select(clip) => { + state.set_clip_index(clip); + None + }, + Clip(command) => command.execute(state)?.map(Clip), + } +}); + +input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() { + Some(PoolMode::Rename(..)) => Self::Rename(ClipRenameCommand::input_to_command(state, input)?), + Some(PoolMode::Length(..)) => Self::Length(ClipLengthCommand::input_to_command(state, input)?), + Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), + Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?), + _ => to_clips_command(state, input)? +}); + +fn to_clips_command (state: &PoolModel, 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(MidiPoolCommand::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(MidiPoolCommand::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(MidiPoolCommand::Swap(index + 1, index)) + } else { + return None + }, + kpat!(Delete) => if index > 0 { + state.set_clip_index(index.min(count.saturating_sub(1))); + Cmd::Clip(MidiPoolCommand::Delete(index)) + } else { + return None + }, + kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(MidiPoolCommand::Add(count, MidiClip::new( + "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) + ))), + kpat!(Char('i')) => Cmd::Clip(MidiPoolCommand::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(MidiPoolCommand::Add(index + 1, clip)) + }, + _ => return None + }) +} +has_clips!(|self: PoolModel|self.clips); +has_clip!(|self: PoolModel|self.clips().get(self.clip_index()).map(|c|c.clone())); +impl PoolModel { + pub(crate) fn clip_index (&self) -> usize { + self.clip.load(Relaxed) + } + pub(crate) fn set_clip_index (&self, value: usize) { + self.clip.store(value, Relaxed); + } + pub(crate) fn clips_mode (&self) -> &Option { + &self.mode + } + pub(crate) fn clips_mode_mut (&mut self) -> &mut Option { + &mut self.mode + } + pub fn file_picker (&self) -> Option<&FileBrowser> { + match self.mode { + Some(PoolMode::Import(_, ref file_picker)) => Some(file_picker), + Some(PoolMode::Export(_, ref file_picker)) => Some(file_picker), + _ => None + } + } +} +command!(|self: FileBrowserCommand, state: PoolModel|{ + 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; + MidiPoolCommand::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 +}); +input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{ + use FileBrowserCommand::*; + use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; + if let Some(PoolMode::Import(_index, browser)) = &state.mode { + match input { + kpat!(Up) => Select(browser.index.overflowing_sub(1).0 + .min(browser.len().saturating_sub(1))), + kpat!(Down) => Select(browser.index.saturating_add(1) + % browser.len()), + kpat!(Right) => Chdir(browser.cwd.clone()), + kpat!(Left) => Chdir(browser.cwd.clone()), + kpat!(Enter) => Confirm, + kpat!(Char(_)) => { todo!() }, + kpat!(Backspace) => { todo!() }, + kpat!(Esc) => Cancel, + _ => return None + } + } else if let Some(PoolMode::Export(_index, browser)) = &state.mode { + match input { + kpat!(Up) => Select(browser.index.overflowing_sub(1).0 + .min(browser.len())), + kpat!(Down) => Select(browser.index.saturating_add(1) + % browser.len()), + kpat!(Right) => Chdir(browser.cwd.clone()), + kpat!(Left) => Chdir(browser.cwd.clone()), + kpat!(Enter) => Confirm, + kpat!(Char(_)) => { todo!() }, + kpat!(Backspace) => { todo!() }, + kpat!(Esc) => Cancel, + _ => return None + } + } else { + unreachable!() + } +}); + +/// Displays and edits clip length. +#[derive(Clone)] +pub struct ClipLength { + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of clip in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, +} + +impl ClipLength { + pub fn new (pulses: usize, focus: Option) -> Self { + Self { ppq: PPQ, bpb: 4, pulses, focus } + } + pub fn bars (&self) -> usize { + self.pulses / (self.bpb * self.ppq) + } + pub fn beats (&self) -> usize { + (self.pulses % (self.bpb * self.ppq)) / self.ppq + } + pub fn ticks (&self) -> usize { + self.pulses % self.ppq + } + pub fn bars_string (&self) -> Arc { + format!("{}", self.bars()).into() + } + pub fn beats_string (&self) -> Arc { + format!("{}", self.beats()).into() + } + pub 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 { + pub fn next (&mut self) { + *self = match self { + Self::Bar => Self::Beat, + Self::Beat => Self::Tick, + Self::Tick => Self::Bar, + } + } + pub fn prev (&mut self) { + *self = match self { + Self::Bar => Self::Tick, + Self::Beat => Self::Bar, + Self::Tick => Self::Beat, + } + } +} + +render!(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()), + } +}); + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ClipLengthCommand { + Begin, + Cancel, + Set(usize), + Next, + Prev, + Inc, + Dec, +} + +command!(|self: ClipLengthCommand,state:PoolModel|{ + use ClipLengthCommand::*; + use ClipLengthFocus::*; + match state.clips_mode_mut().clone() { + Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self { + Cancel => { *state.clips_mode_mut() = None; }, + Self::Prev => { focus.prev() }, + Self::Next => { focus.next() }, + Self::Inc => match focus { + Bar => { *length += 4 * PPQ }, + Beat => { *length += PPQ }, + Tick => { *length += 1 }, + }, + Self::Dec => match focus { + Bar => { *length = length.saturating_sub(4 * PPQ) }, + Beat => { *length = length.saturating_sub(PPQ) }, + Tick => { *length = length.saturating_sub(1) }, + }, + Self::Set(length) => { + let mut old_length = None; + { + let mut clip = state.clips()[clip].clone();//.write().unwrap(); + old_length = Some(clip.read().unwrap().length); + clip.write().unwrap().length = length; + } + *state.clips_mode_mut() = None; + return Ok(old_length.map(Self::Set)) + }, + _ => unreachable!() + }, + _ => unreachable!() + }; + None +}); + +input_to_command!(ClipLengthCommand: |state: PoolModel, input: Event|{ + if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() { + match input { + kpat!(Up) => Self::Inc, + kpat!(Down) => Self::Dec, + kpat!(Right) => Self::Next, + kpat!(Left) => Self::Prev, + kpat!(Enter) => Self::Set(*length), + kpat!(Esc) => Self::Cancel, + _ => return None + } + } else { + unreachable!() + } +}); +use crate::*; +use super::*; + +#[derive(Clone, Debug, PartialEq)] +pub enum ClipRenameCommand { + Begin, + Cancel, + Confirm, + Set(Arc), +} + +impl Command for ClipRenameCommand { + fn execute (self, state: &mut PoolModel) -> Perhaps { + use ClipRenameCommand::*; + match state.clips_mode_mut().clone() { + Some(PoolMode::Rename(clip, ref mut old_name)) => 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.clips_mode_mut() = None; + return Ok(Some(Self::Set(old_name))) + }, + Cancel => { + state.clips()[clip].write().unwrap().name = old_name.clone().into(); + }, + _ => unreachable!() + }, + _ => unreachable!() + }; + Ok(None) + } +} + +impl InputToCommand for ClipRenameCommand { + fn input_to_command (state: &PoolModel, input: &Event) -> Option { + use KeyCode::{Char, Backspace, Enter, Esc}; + if let Some(PoolMode::Rename(_, ref old_name)) = state.clips_mode() { + Some(match input { + kpat!(Char(c)) => { + let mut new_name = old_name.clone().to_string(); + new_name.push(*c); + Self::Set(new_name.into()) + }, + kpat!(Backspace) => { + let mut new_name = old_name.clone().to_string(); + new_name.pop(); + Self::Set(new_name.into()) + }, + kpat!(Enter) => Self::Confirm, + kpat!(Esc) => Self::Cancel, + _ => return None + }) + } else { + unreachable!() + } + } +} diff --git a/sampler/src/lib.rs b/sampler/src/lib.rs index ab5aaecc..ba29ef8f 100644 --- a/sampler/src/lib.rs +++ b/sampler/src/lib.rs @@ -14,7 +14,6 @@ pub(crate) use ::tek_tui::{ pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}}; pub(crate) use std::fs::File; -pub(crate) use std::ops::Deref; pub(crate) use std::path::PathBuf; pub(crate) use std::error::Error; pub(crate) use std::ffi::OsString; diff --git a/sampler/src/sampler.rs b/sampler/src/sampler.rs index 3eda1b03..e1c29ddc 100644 --- a/sampler/src/sampler.rs +++ b/sampler/src/sampler.rs @@ -9,13 +9,31 @@ pub struct Sampler { pub recording: Option<(usize, Arc>)>, pub unmapped: Vec>>, pub voices: Arc>>, - pub midi_in: JackPort, + pub midi_in: Option>, pub audio_ins: Vec>, pub input_meter: Vec, pub audio_outs: Vec>, pub buffer: Vec>, pub output_gain: f32 } +impl Default for Sampler { + fn default () -> Self { + Self { + midi_in: None, + audio_ins: vec![], + input_meter: vec![0.0;2], + audio_outs: vec![], + jack: Default::default(), + name: "tek_sampler".to_string(), + mapped: [const { None };128], + unmapped: vec![], + voices: Arc::new(RwLock::new(vec![])), + buffer: vec![vec![0.0;16384];2], + output_gain: 1., + recording: None, + } + } +} impl Sampler { pub fn new ( jack: &Arc>, @@ -26,24 +44,16 @@ impl Sampler { ) -> Usually { let name = name.as_ref(); Ok(Self { - midi_in: JackPort::::new(jack, format!("M/{name}"), midi_from)?, + midi_in: Some(JackPort::::new(jack, format!("M/{name}"), midi_from)?), audio_ins: vec![ JackPort::::new(jack, &format!("L/{name}"), audio_from[0])?, JackPort::::new(jack, &format!("R/{name}"), audio_from[1])?, ], - input_meter: vec![0.0;2], audio_outs: vec![ JackPort::::new(jack, &format!("{name}/L"), audio_to[0])?, JackPort::::new(jack, &format!("{name}/R"), audio_to[1])?, ], - jack: jack.clone(), - name: name.into(), - mapped: [const { None };128], - unmapped: vec![], - voices: Arc::new(RwLock::new(vec![])), - buffer: vec![vec![0.0;16384];2], - output_gain: 1., - recording: None, + ..Default::default() }) } pub fn cancel_recording (&mut self) { @@ -112,18 +122,20 @@ impl Sampler { /// Create [Voice]s from [Sample]s in response to MIDI input. pub fn process_midi_in (&mut self, scope: &ProcessScope) { let Sampler { midi_in, mapped, voices, .. } = self; - for RawMidi { time, bytes } in midi_in.port.iter(scope) { - if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { - match message { - MidiMessage::NoteOn { ref key, ref vel } => { - if let Some(ref sample) = mapped[key.as_int() as usize] { - voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + if let Some(ref midi_in) = midi_in { + for RawMidi { time, bytes } in midi_in.port.iter(scope) { + if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { + match message { + MidiMessage::NoteOn { ref key, ref vel } => { + if let Some(ref sample) = mapped[key.as_int() as usize] { + voices.write().unwrap().push(Sample::play(sample, time as usize, vel)); + } + }, + MidiMessage::Controller { controller, value } => { + // TODO } - }, - MidiMessage::Controller { controller, value } => { - // TODO + _ => {} } - _ => {} } } } diff --git a/tek/src/arranger.edn b/tek/edn/arranger.edn similarity index 100% rename from tek/src/arranger.edn rename to tek/edn/arranger.edn diff --git a/tek/src/arranger_keys.edn b/tek/edn/arranger_keys.edn similarity index 100% rename from tek/src/arranger_keys.edn rename to tek/edn/arranger_keys.edn diff --git a/tek/src/groovebox.edn b/tek/edn/groovebox.edn similarity index 100% rename from tek/src/groovebox.edn rename to tek/edn/groovebox.edn diff --git a/tek/src/sequencer.edn b/tek/edn/sequencer.edn similarity index 100% rename from tek/src/sequencer.edn rename to tek/edn/sequencer.edn diff --git a/tek/src/arranger.rs b/tek/src/arranger.rs index 3c12a750..ba5b1486 100644 --- a/tek/src/arranger.rs +++ b/tek/src/arranger.rs @@ -1,9 +1,6 @@ use crate::*; use ClockCommand::{Play, Pause}; use self::ArrangerCommand as Cmd; -has_clock!(|self: Arranger|&self.clock); -has_clips!(|self: Arranger|self.pool.clips); -has_editor!(|self: Arranger|self.editor); impl Arranger { pub fn activate (&mut self) -> Usually<()> { if let ArrangerSelection::Scene(s) = self.selected { @@ -41,6 +38,139 @@ impl Arranger { } } } +impl Arranger { + pub fn track_next_name (&self) -> Arc { + format!("Trk{:02}", self.tracks.len() + 1).into() + } + pub fn track_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerTrack> + { + let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); + let track = ArrangerTrack { + width: (name.len() + 2).max(9), + color: color.unwrap_or_else(ItemPalette::random), + player: MidiPlayer::from(&self.clock), + name, + }; + self.tracks.push(track); + let len = self.tracks.len(); + let index = len - 1; + for scene in self.scenes.iter_mut() { + while scene.clips.len() < len { + scene.clips.push(None); + } + } + Ok(&mut self.tracks[index]) + } + pub fn track_del (&mut self, index: usize) { + self.tracks.remove(index); + for scene in self.scenes.iter_mut() { + scene.clips.remove(index); + } + } + 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))?; + track.width = width; + let port = JackPort::::new(&jack, &format!("{}I", &track.name), midi_from)?; + track.player.midi_ins.push(port); + let port = JackPort::::new(&jack, &format!("{}O", &track.name), midi_to)?; + track.player.midi_outs.push(port); + } + Ok(()) + } +} +impl Arranger { + pub fn scene_add (&mut self, name: Option<&str>, color: Option) + -> Usually<&mut ArrangerScene> + { + let scene = ArrangerScene { + 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.push(scene); + let index = self.scenes.len() - 1; + Ok(&mut self.scenes[index]) + } + pub fn scene_del (&mut self, index: usize) { + todo!("delete scene"); + } + fn scene_default_name (&self) -> Arc { + format!("Sc{:3>}", self.scenes.len() + 1).into() + } + pub fn selected_scene (&self) -> Option<&ArrangerScene> { + self.selected.scene().and_then(|s|self.scenes.get(s)) + } + pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { + self.selected.scene().and_then(|s|self.scenes.get_mut(s)) + } + 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 _scene = self.scene_add(None, Some( + scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() + ))?; + } + Ok(()) + } +} +impl ArrangerTrack { + fn longest_name (tracks: &[Self]) -> usize { + tracks.iter().map(|s|s.name.len()).fold(0, usize::max) + } + fn width_inc (&mut self) { + self.width += 1; + } + fn width_dec (&mut self) { + if self.width > Arranger::TRACK_MIN_WIDTH { + self.width -= 1; + } + } +} +impl ArrangerScene { + pub fn longest_name (scenes: &[Self]) -> usize { + scenes.iter().map(|s|s.name.len()).fold(0, usize::max) + } + /// Returns the pulse length of the longest clip in the scene + pub 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. + pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> 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 + }) + } + pub fn clip (&self, index: usize) -> Option<&Arc>> { + match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } + } +} //pub struct ArrangerVCursor { //cols: Vec<(usize, usize)>, @@ -181,110 +311,6 @@ impl Arranger { //TuiTheme::g(32).into(), //TuiTheme::g(32).into(), //); -#[derive(PartialEq, Clone, Copy, Debug, Default)] -/// Represents the current user selection in the arranger -pub enum ArrangerSelection { - /// 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 ArrangerSelection { - pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) } - pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } - pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } - pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } - pub fn description ( - &self, - tracks: &[ArrangerTrack], - scenes: &[ArrangerScene], - ) -> 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() - } - pub fn track (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(t, _) => Some(*t), - Track(t) => Some(*t), - _ => None - } - } - pub fn scene (&self) -> Option { - use ArrangerSelection::*; - match self { - Clip(_, s) => Some(*s), - Scene(s) => Some(*s), - _ => None - } - } -} -impl Arranger { - pub fn track_next_name (&self) -> Arc { - format!("Trk{:02}", self.tracks.len() + 1).into() - } - pub fn track_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerTrack> - { - let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into()); - let track = ArrangerTrack { - width: (name.len() + 2).max(9), - color: color.unwrap_or_else(ItemPalette::random), - player: MidiPlayer::from(&self.clock), - name, - }; - self.tracks.push(track); - let len = self.tracks.len(); - let index = len - 1; - for scene in self.scenes.iter_mut() { - while scene.clips.len() < len { - scene.clips.push(None); - } - } - Ok(&mut self.tracks[index]) - } - pub fn track_del (&mut self, index: usize) { - self.tracks.remove(index); - for scene in self.scenes.iter_mut() { - scene.clips.remove(index); - } - } - 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))?; - track.width = width; - let port = JackPort::::new(&jack, &format!("{}I", &track.name), midi_from)?; - track.player.midi_ins.push(port); - let port = JackPort::::new(&jack, &format!("{}O", &track.name), midi_to)?; - track.player.midi_outs.push(port); - } // TODO: port per track: //for connection in midi_from.iter() { //let mut split = connection.as_ref().split("="); @@ -332,107 +358,3 @@ impl Arranger { //panic!("Failed to parse track number: {number}") //} //} - Ok(()) - } -} -#[derive(Debug)] pub struct ArrangerTrack { - /// 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, -} -has_clock!(|self:ArrangerTrack|self.player.clock()); -has_player!(|self:ArrangerTrack|self.player); -impl ArrangerTrack { - fn longest_name (tracks: &[Self]) -> usize { - tracks.iter().map(|s|s.name.len()).fold(0, usize::max) - } - fn width_inc (&mut self) { - self.width += 1; - } - fn width_dec (&mut self) { - if self.width > Arranger::TRACK_MIN_WIDTH { - self.width -= 1; - } - } -} -impl Arranger { - pub fn scene_add (&mut self, name: Option<&str>, color: Option) - -> Usually<&mut ArrangerScene> - { - let scene = ArrangerScene { - 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.push(scene); - let index = self.scenes.len() - 1; - Ok(&mut self.scenes[index]) - } - pub fn scene_del (&mut self, index: usize) { - todo!("delete scene"); - } - fn scene_default_name (&self) -> Arc { - format!("Sc{:3>}", self.scenes.len() + 1).into() - } - pub fn selected_scene (&self) -> Option<&ArrangerScene> { - self.selected.scene().and_then(|s|self.scenes.get(s)) - } - pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> { - self.selected.scene().and_then(|s|self.scenes.get_mut(s)) - } - 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 _scene = self.scene_add(None, Some( - scene_color_1.mix(scene_color_2, i as f32 / n as f32).into() - ))?; - } - Ok(()) - } -} -#[derive(Default, Debug, Clone)] pub struct ArrangerScene { - /// Name of scene - pub(crate) name: Arc, - /// Clips in scene, one per track - pub(crate) clips: Vec>>>, - /// Identifying color of scene - pub(crate) color: ItemPalette, -} -impl ArrangerScene { - pub fn longest_name (scenes: &[Self]) -> usize { - scenes.iter().map(|s|s.name.len()).fold(0, usize::max) - } - /// Returns the pulse length of the longest clip in the scene - pub 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. - pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> 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 - }) - } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } -} diff --git a/tek/src/groovebox.rs b/tek/src/groovebox.rs deleted file mode 100644 index 8d9650d2..00000000 --- a/tek/src/groovebox.rs +++ /dev/null @@ -1,5 +0,0 @@ -use crate::*; -use super::*; -use self::GrooveboxCommand as Cmd; -use EdnItem::*; -use std::marker::ConstParamTy; diff --git a/tek/src/lib.rs b/tek/src/lib.rs index b1ffe52f..d1055b9b 100644 --- a/tek/src/lib.rs +++ b/tek/src/lib.rs @@ -15,11 +15,8 @@ pub mod view; pub use self::view::*; pub mod control; pub use self::control::*; pub mod audio; pub use self::audio::*; -pub mod arranger; pub use self::arranger::*; -pub mod groovebox; pub use self::groovebox::*; -pub mod mixer; pub use self::mixer::*; -pub mod pool; pub use self::pool::*; -pub mod sequencer; pub use self::sequencer::*; +pub mod arranger; pub use self::arranger::*; +pub mod mixer; pub use self::mixer::*; pub use ::tek_time; pub use ::tek_time::*; pub use ::tek_jack; pub use ::tek_jack::{*, jack::{*, contrib::*}}; diff --git a/tek/src/model.rs b/tek/src/model.rs index 260a8601..73ae6db3 100644 --- a/tek/src/model.rs +++ b/tek/src/model.rs @@ -1,6 +1,5 @@ use crate::*; -#[derive(Default)] -pub struct App { +#[derive(Default)] pub struct App { pub jack: Arc>, pub edn: String, pub clock: Clock, @@ -33,7 +32,7 @@ impl App { midi_tos: &[PortConnection], ) -> Self { Self { - edn: include_str!("sequencer.edn").to_string(), + edn: include_str!("../edn/sequencer.edn").to_string(), jack: jack.clone(), pool: Some(pool), editor: Some(editor), @@ -56,7 +55,7 @@ impl App { audio_tos: &[&[PortConnection]], ) -> Self { Self { - edn: include_str!("groovebox.edn").to_string(), + edn: include_str!("../edn/groovebox.edn").to_string(), sampler: Some(sampler), ..Self::sequencer( jack, pool, editor, @@ -78,7 +77,7 @@ impl App { track_width: usize, ) -> Self { Self { - edn: include_str!("arranger.edn").to_string(), + edn: include_str!("../edn/arranger.edn").to_string(), ..Self::groovebox( jack, pool, editor, None, midi_froms, midi_tos, @@ -87,7 +86,7 @@ impl App { } } } -pub struct Sequencer { +#[derive(Default)] pub struct Sequencer { pub jack: Arc>, pub compact: bool, pub editor: MidiEditor, @@ -101,7 +100,12 @@ pub struct Sequencer { pub status: bool, pub transport: bool, } -pub struct Groovebox { +has_size!(|self:Sequencer|&self.size); +has_clock!(|self:Sequencer|&self.player.clock); +has_clips!(|self:Sequencer|self.pool.clips); +has_editor!(|self:Sequencer|self.editor); + +#[derive(Default)] pub struct Groovebox { pub jack: Arc>, pub compact: bool, pub editor: MidiEditor, @@ -114,7 +118,9 @@ pub struct Groovebox { pub size: Measure, pub status: bool, } -pub struct Arranger { +has_clock!(|self: Groovebox|self.player.clock()); + +#[derive(Default)] pub struct Arranger { pub clock: Clock, pub color: ItemPalette, pub compact: bool, @@ -133,3 +139,81 @@ pub struct Arranger { pub splits: [u16;2], pub tracks: Vec, } +has_clock!(|self: Arranger|&self.clock); +has_clips!(|self: Arranger|self.pool.clips); +has_editor!(|self: Arranger|self.editor); +#[derive(Debug)] pub struct ArrangerTrack { + /// 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, +} +has_clock!(|self:ArrangerTrack|self.player.clock()); +has_player!(|self:ArrangerTrack|self.player); +#[derive(Default)] pub struct ArrangerScene { + /// Name of scene + pub(crate) name: Arc, + /// Clips in scene, one per track + pub(crate) clips: Vec>>>, + /// Identifying color of scene + pub(crate) color: ItemPalette, +} +#[derive(PartialEq, Clone, Copy, Debug, Default)] +/// Represents the current user selection in the arranger +pub enum ArrangerSelection { + /// 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 ArrangerSelection { + pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) } + pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) } + pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) } + pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) } + pub fn description ( + &self, + tracks: &[ArrangerTrack], + scenes: &[ArrangerScene], + ) -> 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() + } + pub fn track (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(t, _) => Some(*t), + Track(t) => Some(*t), + _ => None + } + } + pub fn scene (&self) -> Option { + use ArrangerSelection::*; + match self { + Clip(_, s) => Some(*s), + Scene(s) => Some(*s), + _ => None + } + } +} diff --git a/tek/src/pool.rs b/tek/src/pool.rs deleted file mode 100644 index 926a8122..00000000 --- a/tek/src/pool.rs +++ /dev/null @@ -1,495 +0,0 @@ -use crate::*; -use super::*; -use ClipLengthFocus::*; -use ClipLengthCommand::*; -use KeyCode::{Up, Down, Left, Right, Enter, Esc}; - -use super::*; - -#[derive(Debug)] -pub struct PoolModel { - pub(crate) visible: bool, - /// Collection of clips - pub(crate) clips: Arc>>>>, - /// Selected clip - pub(crate) clip: AtomicUsize, - /// Mode switch - pub(crate) mode: Option, - /// Rendered size - size: Measure, - /// Scroll offset - scroll: usize, -} -impl Default for PoolModel { - fn default () -> Self { - Self { - visible: true, - clips: Arc::from(RwLock::from(vec![])), - clip: 0.into(), - scroll: 0, - mode: None, - size: Measure::new(), - } - } -} -from!(|clip:&Arc>|PoolModel = { - let mut model = Self::default(); - model.clips.write().unwrap().push(clip.clone()); - model.clip.store(1, Relaxed); - model -}); - -pub struct PoolView<'a>(pub bool, pub &'a PoolModel); -render!(TuiOut: (self: PoolView<'a>) => { - let Self(compact, model) = self; - let PoolModel { clips, mode, .. } = 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(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "▶"))))), - Fill::x(Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "◀"))))), - )))) - }))))) -}); - -/// 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), -} - -#[derive(Clone, PartialEq, Debug)] -pub enum PoolCommand { - Show(bool), - /// Update the contents of the clip pool - Clip(MidiPoolCommand), - /// 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), -} - -command!(|self:PoolCommand, state: PoolModel|{ - use PoolCommand::*; - match self { - Show(visible) => { - state.visible = visible; - Some(Self::Show(!visible)) - } - Rename(command) => match command { - ClipRenameCommand::Begin => { - let length = state.clips()[state.clip_index()].read().unwrap().length; - *state.clips_mode_mut() = Some( - PoolMode::Length(state.clip_index(), length, ClipLengthFocus::Bar) - ); - None - }, - _ => command.execute(state)?.map(Rename) - }, - Length(command) => match command { - ClipLengthCommand::Begin => { - let name = state.clips()[state.clip_index()].read().unwrap().name.clone(); - *state.clips_mode_mut() = Some( - PoolMode::Rename(state.clip_index(), name) - ); - None - }, - _ => command.execute(state)?.map(Length) - }, - Import(command) => match command { - FileBrowserCommand::Begin => { - *state.clips_mode_mut() = Some( - PoolMode::Import(state.clip_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Import) - }, - Export(command) => match command { - FileBrowserCommand::Begin => { - *state.clips_mode_mut() = Some( - PoolMode::Export(state.clip_index(), FileBrowser::new(None)?) - ); - None - }, - _ => command.execute(state)?.map(Export) - }, - Select(clip) => { - state.set_clip_index(clip); - None - }, - Clip(command) => command.execute(state)?.map(Clip), - } -}); - -input_to_command!(PoolCommand: |state: PoolModel, input: Event|match state.clips_mode() { - Some(PoolMode::Rename(..)) => Self::Rename(ClipRenameCommand::input_to_command(state, input)?), - Some(PoolMode::Length(..)) => Self::Length(ClipLengthCommand::input_to_command(state, input)?), - Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?), - Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?), - _ => to_clips_command(state, input)? -}); - -fn to_clips_command (state: &PoolModel, 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(MidiPoolCommand::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(MidiPoolCommand::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(MidiPoolCommand::Swap(index + 1, index)) - } else { - return None - }, - kpat!(Delete) => if index > 0 { - state.set_clip_index(index.min(count.saturating_sub(1))); - Cmd::Clip(MidiPoolCommand::Delete(index)) - } else { - return None - }, - kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Clip(MidiPoolCommand::Add(count, MidiClip::new( - "Clip", true, 4 * PPQ, None, Some(ItemPalette::random()) - ))), - kpat!(Char('i')) => Cmd::Clip(MidiPoolCommand::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(MidiPoolCommand::Add(index + 1, clip)) - }, - _ => return None - }) -} -has_clips!(|self: PoolModel|self.clips); -has_clip!(|self: PoolModel|self.clips().get(self.clip_index()).map(|c|c.clone())); -impl PoolModel { - pub(crate) fn clip_index (&self) -> usize { - self.clip.load(Relaxed) - } - pub(crate) fn set_clip_index (&self, value: usize) { - self.clip.store(value, Relaxed); - } - pub(crate) fn clips_mode (&self) -> &Option { - &self.mode - } - pub(crate) fn clips_mode_mut (&mut self) -> &mut Option { - &mut self.mode - } - pub fn file_picker (&self) -> Option<&FileBrowser> { - match self.mode { - Some(PoolMode::Import(_, ref file_picker)) => Some(file_picker), - Some(PoolMode::Export(_, ref file_picker)) => Some(file_picker), - _ => None - } - } -} -command!(|self: FileBrowserCommand, state: PoolModel|{ - 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; - MidiPoolCommand::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 -}); -input_to_command!(FileBrowserCommand: |state: PoolModel, input: Event|{ - use FileBrowserCommand::*; - use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char}; - if let Some(PoolMode::Import(_index, browser)) = &state.mode { - match input { - kpat!(Up) => Select(browser.index.overflowing_sub(1).0 - .min(browser.len().saturating_sub(1))), - kpat!(Down) => Select(browser.index.saturating_add(1) - % browser.len()), - kpat!(Right) => Chdir(browser.cwd.clone()), - kpat!(Left) => Chdir(browser.cwd.clone()), - kpat!(Enter) => Confirm, - kpat!(Char(_)) => { todo!() }, - kpat!(Backspace) => { todo!() }, - kpat!(Esc) => Cancel, - _ => return None - } - } else if let Some(PoolMode::Export(_index, browser)) = &state.mode { - match input { - kpat!(Up) => Select(browser.index.overflowing_sub(1).0 - .min(browser.len())), - kpat!(Down) => Select(browser.index.saturating_add(1) - % browser.len()), - kpat!(Right) => Chdir(browser.cwd.clone()), - kpat!(Left) => Chdir(browser.cwd.clone()), - kpat!(Enter) => Confirm, - kpat!(Char(_)) => { todo!() }, - kpat!(Backspace) => { todo!() }, - kpat!(Esc) => Cancel, - _ => return None - } - } else { - unreachable!() - } -}); - -/// Displays and edits clip length. -#[derive(Clone)] -pub struct ClipLength { - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of clip in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} - -impl ClipLength { - pub fn new (pulses: usize, focus: Option) -> Self { - Self { ppq: PPQ, bpb: 4, pulses, focus } - } - pub fn bars (&self) -> usize { - self.pulses / (self.bpb * self.ppq) - } - pub fn beats (&self) -> usize { - (self.pulses % (self.bpb * self.ppq)) / self.ppq - } - pub fn ticks (&self) -> usize { - self.pulses % self.ppq - } - pub fn bars_string (&self) -> Arc { - format!("{}", self.bars()).into() - } - pub fn beats_string (&self) -> Arc { - format!("{}", self.beats()).into() - } - pub 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 { - pub fn next (&mut self) { - *self = match self { - Self::Bar => Self::Beat, - Self::Beat => Self::Tick, - Self::Tick => Self::Bar, - } - } - pub fn prev (&mut self) { - *self = match self { - Self::Bar => Self::Tick, - Self::Beat => Self::Bar, - Self::Tick => Self::Beat, - } - } -} - -render!(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()), - } -}); - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum ClipLengthCommand { - Begin, - Cancel, - Set(usize), - Next, - Prev, - Inc, - Dec, -} - -command!(|self:ClipLengthCommand,state:PoolModel|{ - match state.clips_mode_mut().clone() { - Some(PoolMode::Length(clip, ref mut length, ref mut focus)) => match self { - Cancel => { *state.clips_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 mut clip = state.clips()[clip].clone();//.write().unwrap(); - old_length = Some(clip.read().unwrap().length); - clip.write().unwrap().length = length; - } - *state.clips_mode_mut() = None; - return Ok(old_length.map(Self::Set)) - }, - _ => unreachable!() - }, - _ => unreachable!() - }; - None -}); - -input_to_command!(ClipLengthCommand: |state: PoolModel, input: Event|{ - if let Some(PoolMode::Length(_, length, _)) = state.clips_mode() { - match input { - kpat!(Up) => Self::Inc, - kpat!(Down) => Self::Dec, - kpat!(Right) => Self::Next, - kpat!(Left) => Self::Prev, - kpat!(Enter) => Self::Set(*length), - kpat!(Esc) => Self::Cancel, - _ => return None - } - } else { - unreachable!() - } -}); -use crate::*; -use super::*; - -#[derive(Clone, Debug, PartialEq)] -pub enum ClipRenameCommand { - Begin, - Cancel, - Confirm, - Set(Arc), -} - -impl Command for ClipRenameCommand { - fn execute (self, state: &mut PoolModel) -> Perhaps { - use ClipRenameCommand::*; - match state.clips_mode_mut().clone() { - Some(PoolMode::Rename(clip, ref mut old_name)) => 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.clips_mode_mut() = None; - return Ok(Some(Self::Set(old_name))) - }, - Cancel => { - state.clips()[clip].write().unwrap().name = old_name.clone().into(); - }, - _ => unreachable!() - }, - _ => unreachable!() - }; - Ok(None) - } -} - -impl InputToCommand for ClipRenameCommand { - fn input_to_command (state: &PoolModel, input: &Event) -> Option { - use KeyCode::{Char, Backspace, Enter, Esc}; - if let Some(PoolMode::Rename(_, ref old_name)) = state.clips_mode() { - Some(match input { - kpat!(Char(c)) => { - let mut new_name = old_name.clone().to_string(); - new_name.push(*c); - Self::Set(new_name.into()) - }, - kpat!(Backspace) => { - let mut new_name = old_name.clone().to_string(); - new_name.pop(); - Self::Set(new_name.into()) - }, - kpat!(Enter) => Self::Confirm, - kpat!(Esc) => Self::Cancel, - _ => return None - }) - } else { - unreachable!() - } - } -} diff --git a/tek/src/sequencer.rs b/tek/src/sequencer.rs deleted file mode 100644 index 825de2d8..00000000 --- a/tek/src/sequencer.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::*; -use ClockCommand::{Play, Pause}; -use KeyCode::{Tab, Char}; -use SequencerCommand as Cmd; -use MidiEditCommand::*; -use MidiPoolCommand::*; -has_size!(|self:Sequencer|&self.size); -has_clock!(|self:Sequencer|&self.player.clock); -has_clips!(|self:Sequencer|self.pool.clips); -has_editor!(|self:Sequencer|self.editor); diff --git a/tek/src/view.rs b/tek/src/view.rs index f8dba3e6..0d8a4767 100644 --- a/tek/src/view.rs +++ b/tek/src/view.rs @@ -354,7 +354,7 @@ impl EdnViewData for &Sequencer { } } impl Sequencer { - const EDN: &'static str = include_str!("sequencer.edn"); + const EDN: &'static str = include_str!("../edn/sequencer.edn"); fn toolbar_view (&self) -> impl Content + use<'_> { Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock)))) } @@ -429,7 +429,7 @@ impl EdnViewData for &Groovebox { } } impl Groovebox { - const EDN: &'static str = include_str!("groovebox.edn"); + const EDN: &'static str = include_str!("../edn/groovebox.edn"); fn toolbar (&self) -> impl Content + use<'_> { Fill::x(Fixed::y(2, lay!( Fill::x(Align::w(Meter("L/", self.sampler.input_meter[0]))), @@ -466,7 +466,6 @@ impl Groovebox { Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(self.sampler.list(self.compact, &self.editor)))) } } -has_clock!(|self: Groovebox|self.player.clock()); ///// Status bar for sequencer app //#[derive(Clone)] @@ -600,7 +599,7 @@ impl EdnViewData for &Arranger { } } impl Arranger { - const EDN: &'static str = include_str!("arranger.edn"); + const EDN: &'static str = include_str!("../edn/arranger.edn"); pub const LEFT_SEP: char = '▎'; pub const TRACK_MIN_WIDTH: usize = 9;