diff --git a/crates/tek_api/src/api_jack.rs b/crates/tek_api/src/api_jack.rs index ed8cd72b..82b859b5 100644 --- a/crates/tek_api/src/api_jack.rs +++ b/crates/tek_api/src/api_jack.rs @@ -1,5 +1,5 @@ use crate::*; -use tek_core::jack::*; +use tek_core::jack::{*, Transport as JackTransport}; /// Trait for things that have a JACK process callback. pub trait Audio: Send + Sync { @@ -38,7 +38,7 @@ pub trait AudioEngine { process: impl FnMut(&Arc>, &Client, &ProcessScope) -> Control + Send + 'static ) -> Usually>> where Self: Send + Sync + 'static; fn client (&self) -> &Client; - fn transport (&self) -> Transport { + fn transport (&self) -> JackTransport { self.client().transport() } fn port_by_name (&self, name: &str) -> Option> { diff --git a/crates/tek_api/src/arrange.rs b/crates/tek_api/src/arrange.rs index d9782351..50ac4684 100644 --- a/crates/tek_api/src/arrange.rs +++ b/crates/tek_api/src/arrange.rs @@ -10,11 +10,22 @@ pub struct Arrangement { /// Collection of phrases. pub phrases: Arc>>, /// Collection of tracks. - pub tracks: Vec, + pub tracks: Vec, /// Collection of scenes. pub scenes: Vec, } +pub struct ArrangementTrack { + /// Name of track + pub name: Arc>, + /// Preferred width of track column + pub width: usize, + /// Identifying color of track + pub color: ItemColor, + /// The MIDI player for the track + pub player: MIDIPlayer +} + impl Audio for Arrangement { fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { for track in self.tracks.iter_mut() { @@ -24,6 +35,12 @@ impl Audio for Arrangement { } } +impl Arrangement { + fn is_stopped (&self) -> bool { + *self.clock.playing.read().unwrap() == Some(TransportState::Stopped) + } +} + #[derive(Clone)] pub enum ArrangementCommand { New, @@ -58,8 +75,8 @@ pub enum ArrangementTrackCommand { #[derive(Clone)] pub enum ArrangementClipCommand { - SetLoop(bool), Get(usize, usize), Put(usize, usize, Option>>), Edit(Option>>), + SetLoop(bool), } diff --git a/crates/tek_api/src/lib.rs b/crates/tek_api/src/lib.rs index 6ab881cd..9a635c52 100644 --- a/crates/tek_api/src/lib.rs +++ b/crates/tek_api/src/lib.rs @@ -18,7 +18,7 @@ submod! { pool sampler sample - scene + scene scene_cmd sequencer track transport transport_cmd diff --git a/crates/tek_api/src/plugin.rs b/crates/tek_api/src/plugin.rs index 327758e6..01846d91 100644 --- a/crates/tek_api/src/plugin.rs +++ b/crates/tek_api/src/plugin.rs @@ -4,12 +4,16 @@ use crate::*; #[derive(Debug)] pub struct Plugin { /// JACK client handle (needs to not be dropped for standalone mode to work). - pub jack: Arc>, - pub name: String, - pub path: Option, - pub plugin: Option, - pub selected: usize, - pub mapping: bool, + pub jack: Arc>, + pub name: String, + pub path: Option, + pub plugin: Option, + pub selected: usize, + pub mapping: bool, + pub midi_ins: Vec>, + pub midi_outs: Vec>, + pub audio_ins: Vec>, + pub audio_outs: Vec>, } impl Plugin { pub fn new_lv2 ( @@ -24,6 +28,10 @@ impl Plugin { plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)), selected: 0, mapping: false, + midi_ins: vec![], + midi_outs: vec![], + audio_ins: vec![], + audio_outs: vec![], }) } @@ -57,7 +65,7 @@ impl Audio for Plugin { })) => { let urid = features.midi_urid(); input_buffer.clear(); - for port in self.ports.midi_ins.values() { + for port in self.midi_ins.iter() { let mut atom = ::livi::event::LV2AtomSequence::new( &features, scope.n_frames() as usize @@ -75,24 +83,20 @@ impl Audio for Plugin { input_buffer.push(atom); } let mut outputs = vec![]; - for _ in self.ports.midi_outs.iter() { + for _ in self.midi_outs.iter() { outputs.push(::livi::event::LV2AtomSequence::new( &features, scope.n_frames() as usize )); } let ports = ::livi::EmptyPortConnections::new() - .with_atom_sequence_inputs( - input_buffer.iter() - ) - .with_atom_sequence_outputs( - outputs.iter_mut() - ) + .with_atom_sequence_inputs(input_buffer.iter()) + .with_atom_sequence_outputs(outputs.iter_mut()) .with_audio_inputs( - self.ports.audio_ins.values().map(|o|o.as_slice(scope)) + self.audio_ins.iter().map(|o|o.as_slice(scope)) ) .with_audio_outputs( - self.ports.audio_outs.values_mut().map(|o|o.as_mut_slice(scope)) + self.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)) ); unsafe { instance.run(scope.n_frames() as usize, ports).unwrap() diff --git a/crates/tek_api/src/plugin_lv2.rs b/crates/tek_api/src/plugin_lv2.rs index 404f0fc4..a8b79776 100644 --- a/crates/tek_api/src/plugin_lv2.rs +++ b/crates/tek_api/src/plugin_lv2.rs @@ -42,7 +42,7 @@ impl LV2Plugin { } impl LV2Plugin { - pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { + pub fn from_edn <'e> (jack: &Arc>, args: &[Edn<'e>]) -> Usually { let mut name = String::new(); let mut path = String::new(); edn!(edn in args { diff --git a/crates/tek_api/src/sampler.rs b/crates/tek_api/src/sampler.rs index c9cf029c..d44470a0 100644 --- a/crates/tek_api/src/sampler.rs +++ b/crates/tek_api/src/sampler.rs @@ -9,7 +9,7 @@ pub struct Sampler { pub unmapped: Vec>>, pub voices: Arc>>, pub midi_in: Port, - pub audio_outs: Vec>, + pub audio_outs: Vec>, pub buffer: Vec>, pub output_gain: f32 } @@ -52,33 +52,20 @@ impl Sampler { _ => panic!("unexpected in sampler {name}: {edn:?}") }); Ok(Sampler { - jack: jack.clone(), - name: name.into(), - mapped: samples, - unmapped: Default::default(), - voices: Default::default(), - ports: Default::default(), - buffer: Default::default(), + jack: jack.clone(), + name: name.into(), + mapped: samples, + unmapped: Default::default(), + voices: Default::default(), + buffer: Default::default(), + midi_in: jack.read().unwrap().register_port("in", MidiIn::default())?, + audio_outs: vec![], output_gain: 0. }) } - /// Immutable reference to sample at cursor. - pub fn sample (&self) -> Option<&Arc>> { - for (i, sample) in self.mapped.values().enumerate() { - if i == self.cursor.0 { - return Some(sample) - } - } - for (i, sample) in self.unmapped.iter().enumerate() { - if i + self.mapped.len() == self.cursor.0 { - return Some(sample) - } - } - None - } /// Create [Voice]s from [Sample]s in response to MIDI input. pub fn process_midi_in (&mut self, scope: &ProcessScope) { - for RawMidi { time, bytes } in self.ports.midi_ins.get("midi").unwrap().iter(scope) { + for RawMidi { time, bytes } in self.midi_in.iter(scope) { if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() { if let MidiMessage::NoteOn { ref key, ref vel } = message { if let Some(sample) = self.mapped.get(key) { @@ -117,7 +104,7 @@ impl Sampler { } /// Write output buffer to output ports. pub fn write_output_buffer (&mut self, scope: &ProcessScope) { - for (i, port) in self.ports.audio_outs.values_mut().enumerate() { + for (i, port) in self.audio_outs.iter_mut().enumerate() { let buffer = &self.buffer[i]; for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() { *value = *buffer.get(i).unwrap_or(&0.0); diff --git a/crates/tek_api/src/scene.rs b/crates/tek_api/src/scene.rs index bed8289c..dd330507 100644 --- a/crates/tek_api/src/scene.rs +++ b/crates/tek_api/src/scene.rs @@ -11,43 +11,60 @@ pub struct Scene { } impl Scene { - pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { - let mut name = None; - let mut clips = vec![]; - edn!(edn in args { - Edn::Map(map) => { - let key = map.get(&Edn::Key(":name")); - if let Some(Edn::Str(n)) = key { - name = Some(*n); - } else { - panic!("unexpected key in scene '{name:?}': {key:?}") - } - }, - Edn::Symbol("_") => { - clips.push(None); - }, - Edn::Int(i) => { - clips.push(Some(*i as usize)); - }, - _ => panic!("unexpected in scene '{name:?}': {edn:?}") - }); - Ok(Scene { - name: Arc::new(name.unwrap_or("").to_string().into()), - color: ItemColor::random(), - clips, + //TODO + //pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually { + //let mut name = None; + //let mut clips = vec![]; + //edn!(edn in args { + //Edn::Map(map) => { + //let key = map.get(&Edn::Key(":name")); + //if let Some(Edn::Str(n)) = key { + //name = Some(*n); + //} else { + //panic!("unexpected key in scene '{name:?}': {key:?}") + //} + //}, + //Edn::Symbol("_") => { + //clips.push(None); + //}, + //Edn::Int(i) => { + //clips.push(Some(*i as usize)); + //}, + //_ => panic!("unexpected in scene '{name:?}': {edn:?}") + //}); + //Ok(Scene { + //name: Arc::new(name.unwrap_or("").to_string().into()), + //color: ItemColor::random(), + //clips, + //}) + //} + + /// Returns the pulse length of the longest phrase 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)) }) } -} -#[derive(Clone)] -pub enum SceneCommand { - Next, - Prev, - Add, - Delete, - MoveForward, - MoveBack, - RandomColor, - SetSize(usize), - SetZoom(usize), + /// Returns true if all phrases in the scene are + /// currently playing on the given collection of tracks. + pub fn is_playing (&self, tracks: &[MIDIPlayer]) -> bool { + self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() + .all(|(track_index, clip)|match clip { + Some(clip) => tracks + .get(track_index) + .map(|track|if let Some((_, Some(phrase))) = &track.phrase { + *phrase.read().unwrap() == *clip.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/crates/tek_api/src/scene_cmd.rs b/crates/tek_api/src/scene_cmd.rs new file mode 100644 index 00000000..622864c4 --- /dev/null +++ b/crates/tek_api/src/scene_cmd.rs @@ -0,0 +1,14 @@ +use crate::*; + +#[derive(Clone)] +pub enum SceneCommand { + Next, + Prev, + Add, + Delete, + MoveForward, + MoveBack, + RandomColor, + SetSize(usize), + SetZoom(usize), +} diff --git a/crates/tek_api/src/sequencer.rs b/crates/tek_api/src/sequencer.rs index 4cc9473d..e74371e8 100644 --- a/crates/tek_api/src/sequencer.rs +++ b/crates/tek_api/src/sequencer.rs @@ -1,38 +1,10 @@ use crate::*; -#[derive(Clone, PartialEq)] -pub enum SequencerCommand { - Focus(FocusCommand), - Transport(TransportCommand), - Phrases(PhrasePoolCommand), - Editor(PhraseEditorCommand), -} - -#[derive(Clone, PartialEq)] -pub enum PhraseEditorCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - ToggleDirection, - EnterEditMode, - ExitEditMode, - NoteAppend, - NoteSet, - NoteCursorSet(usize), - NoteLengthSet(usize), - NoteScrollSet(usize), - TimeCursorSet(usize), - TimeScrollSet(usize), - TimeZoomSet(usize), - Go(Direction), -} +/// MIDI message structural +pub type PhraseData = Vec>; #[derive(Debug)] -pub struct SequencerTrack { - /// Name of track - pub name: Arc>, - /// Preferred width of track column - pub width: usize, - /// Identifying color of track - pub color: ItemColor, +pub struct MIDIPlayer { /// Global timebase pub clock: Arc, /// Start time and phrase being played @@ -62,7 +34,7 @@ pub struct SequencerTrack { } /// JACK process callback for a sequencer's phrase player/recorder. -impl Audio for SequencerTrack { +impl Audio for MIDIPlayer { fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control { let has_midi_outputs = self.has_midi_outputs(); let has_midi_inputs = self.has_midi_inputs(); @@ -88,7 +60,33 @@ impl Audio for SequencerTrack { } /// Methods used primarily by the process callback -impl SequencerTrack { +impl MIDIPlayer { + pub fn new ( + jack: &Arc>, + clock: &Arc, + name: &str + ) -> Usually { + let jack = jack.read().unwrap(); + Ok(Self { + clock: clock.clone(), + phrase: None, + next_phrase: None, + notes_in: Arc::new(RwLock::new([false;128])), + notes_out: Arc::new(RwLock::new([false;128])), + monitoring: false, + recording: false, + overdub: true, + reset: true, + midi_note: Vec::with_capacity(8), + midi_chunk: vec![Vec::with_capacity(16);16384], + midi_outputs: vec![ + jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? + ], + midi_inputs: vec![ + jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? + ], + }) + } fn is_rolling (&self) -> bool { *self.clock.playing.read().unwrap() == Some(TransportState::Rolling) } @@ -251,10 +249,28 @@ impl SequencerTrack { } } } + pub fn toggle_monitor (&mut self) { self.monitoring = !self.monitoring; } + pub fn toggle_record (&mut self) { self.recording = !self.recording; } + pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; } + pub fn enqueue_next (&mut self, phrase: Option<&Arc>>) { + let start = self.clock.next_launch_pulse(); + self.next_phrase = Some(( + Instant::from_pulse(&self.clock.timebase(), start as f64), + phrase.map(|p|p.clone()) + )); + self.reset = true; + } + pub fn pulses_since_start (&self) -> Option { + if let Some((started, Some(_))) = self.phrase.as_ref() { + Some(self.clock.current.pulse.get() - started.pulse.get()) + } else { + None + } + } } /// Add "all notes off" to the start of a buffer. -pub fn all_notes_off (output: &mut PhraseChunk) { +pub fn all_notes_off (output: &mut [Vec>]) { let mut buf = vec![]; let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() }; let evt = LiveEvent::Midi { channel: 0.into(), message: msg }; diff --git a/crates/tek_api/src/track.rs b/crates/tek_api/src/track.rs index 6b7f6b37..6c527ca8 100644 --- a/crates/tek_api/src/track.rs +++ b/crates/tek_api/src/track.rs @@ -22,7 +22,9 @@ pub trait MixerTrackDevice: Audio + Debug { impl MixerTrackDevice for Sampler {} -impl MixerTrackDevice for LV2Plugin {} +impl MixerTrackDevice for Plugin {} + +//impl MixerTrackDevice for LV2Plugin {} impl MixerTrack { const SYM_NAME: &'static str = ":name"; @@ -37,8 +39,6 @@ impl MixerTrack { audio_outs: vec![], devices: vec![], }; - #[allow(unused_mut)] - let mut devices: Vec = vec![]; edn!(edn in args { Edn::Map(map) => { if let Some(Edn::Str(n)) = map.get(&Edn::Key(Self::SYM_NAME)) { @@ -51,8 +51,8 @@ impl MixerTrack { Edn::List(args) => match args.get(0) { // Add a sampler device to the track Some(Edn::Symbol(Self::SYM_SAMPLER)) => { - track.add_device( - Sampler::from_edn(jack, &args[1..])? + track.devices.push( + Box::new(Sampler::from_edn(jack, &args[1..])?) as Box ); panic!( "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"", @@ -62,8 +62,8 @@ impl MixerTrack { }, // Add a LV2 plugin to the track. Some(Edn::Symbol(Self::SYM_LV2)) => { - track.add_device( - LV2Plugin::from_edn(jack, &args[1..])? + track.devices.push( + Box::new(LV2Plugin::from_edn(jack, &args[1..])?) as Box ); panic!( "unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"", @@ -80,7 +80,4 @@ impl MixerTrack { }); Ok(track) } - pub fn add_device (&mut self, device: impl MixerTrackDevice) { - self.devices.push(device.boxed()) - } } diff --git a/crates/tek_api/src/voice.rs b/crates/tek_api/src/voice.rs index 7375f5ea..1dd3ba4a 100644 --- a/crates/tek_api/src/voice.rs +++ b/crates/tek_api/src/voice.rs @@ -8,3 +8,23 @@ pub struct Voice { pub position: usize, pub velocity: f32, } + +impl Iterator for Voice { + type Item = [f32;2]; + fn next (&mut self) -> Option { + if self.after > 0 { + self.after = self.after - 1; + return Some([0.0, 0.0]) + } + let sample = self.sample.read().unwrap(); + if self.position < sample.end { + let position = self.position; + self.position = self.position + 1; + return sample.channels[0].get(position).map(|_amplitude|[ + sample.channels[0][position] * self.velocity, + sample.channels[0][position] * self.velocity, + ]) + } + None + } +} diff --git a/crates/tek_cli/src/cli_arranger.rs b/crates/tek_cli/src/cli_arranger.rs index a50ab739..340741ca 100644 --- a/crates/tek_cli/src/cli_arranger.rs +++ b/crates/tek_cli/src/cli_arranger.rs @@ -43,7 +43,7 @@ impl ArrangerCli { Some(scene_color_1.mix(scene_color_2, i as f32 / self.scenes as f32)) )?; } - Ok(ArrangerView::new( + Ok(ArrangerApp::new( jack, self.transport.then_some(transport), arrangement, @@ -53,33 +53,3 @@ impl ArrangerCli { Ok(()) } } - -impl Audio for ArrangerView { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - if let Some(ref transport) = self.transport { - transport.write().unwrap().process(client, scope); - } - let Arrangement { scenes, ref mut tracks, selected, .. } = &mut self.arrangement; - for track in tracks.iter_mut() { - track.player.process(client, scope); - } - if let ArrangementFocus::Clip(t, s) = selected { - if let Some(Some(Some(phrase))) = scenes.get(*s).map(|scene|scene.clips.get(*t)) { - if let Some(track) = tracks.get(*t) { - if let Some((ref started_at, Some(ref playing))) = track.player.phrase { - let phrase = phrase.read().unwrap(); - if *playing.read().unwrap() == *phrase { - let pulse = self.clock.current.pulse.get(); - let start = started_at.pulse.get(); - let now = (pulse - start) % phrase.length as f64; - self.editor.now.set(now); - return Control::Continue - } - } - } - } - } - self.editor.now.set(0.); - Control::Continue - } -} diff --git a/crates/tek_cli/src/cli_sequencer.rs b/crates/tek_cli/src/cli_sequencer.rs index 36ce59e3..cae72eba 100644 --- a/crates/tek_cli/src/cli_sequencer.rs +++ b/crates/tek_cli/src/cli_sequencer.rs @@ -31,7 +31,7 @@ impl SequencerCli { //phrase.write().unwrap().length = length; //} } - Ok(SequencerView { + Ok(SequencerApp { jack: jack.clone(), focus_cursor: (1, 1), entered: false, @@ -45,14 +45,3 @@ impl SequencerCli { Ok(()) } } - -/// JACK process callback for sequencer app -impl Audio for SequencerView { - fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { - if let Some(ref transport) = self.transport { - transport.write().unwrap().process(client, scope); - } - self.player.process(client, scope); - Control::Continue - } -} diff --git a/crates/tek_tui/src/lib.rs b/crates/tek_tui/src/lib.rs index d8ff9389..ca77c32f 100644 --- a/crates/tek_tui/src/lib.rs +++ b/crates/tek_tui/src/lib.rs @@ -10,10 +10,12 @@ pub(crate) use std::ffi::OsString; pub(crate) use std::fs::read_dir; submod! { + tui_arrangement tui_arranger tui_arranger_bar tui_arranger_cmd tui_arranger_col + tui_arranger_foc tui_arranger_hor tui_arranger_ver tui_sequencer diff --git a/crates/tek_tui/src/tui_arrangement.rs b/crates/tek_tui/src/tui_arrangement.rs new file mode 100644 index 00000000..b80dde10 --- /dev/null +++ b/crates/tek_tui/src/tui_arrangement.rs @@ -0,0 +1,149 @@ +use crate::*; + +pub struct ArrangementEditor { + /// Global JACK client + pub jack: Arc>, + /// Global timebase + pub clock: Arc, + /// Name of arranger + pub name: Arc>, + /// Collection of phrases. + pub phrases: Arc>>, + /// Collection of tracks. + pub tracks: Vec, + /// Collection of scenes. + pub scenes: Vec, + /// Currently selected element. + pub selected: ArrangementEditorFocus, + /// Display mode of arranger + pub mode: ArrangementViewMode, + /// Whether the arranger is currently focused + pub focused: bool, + /// Background color of arrangement + pub color: ItemColor, + /// Width and height of arrangement area at last render + pub size: Measure, + /// Whether this is currently in edit mode + pub entered: bool, +} + +/// Display mode of arranger +#[derive(PartialEq)] +pub enum ArrangementViewMode { + /// Tracks are rows + Horizontal, + /// Tracks are columns + Vertical(usize), +} + +impl Content for ArrangementEditor { + type Engine = Tui; + fn content (&self) -> impl Widget { + Layers::new(move |add|{ + match self.mode { + ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) }, + ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) }, + }?; + add(&self.size) + }) + } +} + +#[derive(PartialEq, Clone, Copy)] +/// Represents the current user selection in the arranger +pub enum ArrangementEditorFocus { + /// The whole mix is selected + 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 ArrangementEditorFocus { + pub fn description ( + &self, + tracks: &Vec, + scenes: &Vec, + ) -> String { + format!("Selected: {}", match self { + Self::Mix => format!("Everything"), + Self::Track(t) => match tracks.get(*t) { + Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), + None => format!("T??"), + }, + Self::Scene(s) => match scenes.get(*s) { + Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), + None => format!("S??"), + }, + 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"), + } + }) + } + pub fn is_mix (&self) -> bool { match self { Self::Mix => true, _ => false } } + pub fn is_track (&self) -> bool { match self { Self::Track(_) => true, _ => false } } + pub fn is_scene (&self) -> bool { match self { Self::Scene(_) => true, _ => false } } + pub fn is_clip (&self) -> bool { match self { Self::Clip(_, _) => true, _ => false } } + pub fn track (&self) -> Option { + match self { Self::Clip(t, _) => Some(*t), Self::Track(t) => Some(*t), _ => None } + } + pub fn track_next (&mut self, last_track: usize) { + *self = match self { + Self::Mix => + Self::Track(0), + Self::Track(t) => + Self::Track(last_track.min(*t + 1)), + Self::Scene(s) => + Self::Clip(0, *s), + Self::Clip(t, s) => + Self::Clip(last_track.min(*t + 1), *s), + } + } + pub fn track_prev (&mut self) { + *self = match self { + Self::Mix => + Self::Mix, + Self::Scene(s) => + Self::Scene(*s), + Self::Track(t) => + if *t == 0 { Self::Mix } else { Self::Track(*t - 1) }, + Self::Clip(t, s) => + if *t == 0 { Self::Scene(*s) } else { Self::Clip(t.saturating_sub(1), *s) } + } + } + pub fn scene (&self) -> Option { + match self { Self::Clip(_, s) => Some(*s), Self::Scene(s) => Some(*s), _ => None } + } + pub fn scene_next (&mut self, last_scene: usize) { + *self = match self { + Self::Mix => + Self::Scene(0), + Self::Track(t) => + Self::Clip(*t, 0), + Self::Scene(s) => + Self::Scene(last_scene.min(*s + 1)), + Self::Clip(t, s) => + Self::Clip(*t, last_scene.min(*s + 1)), + } + } + pub fn scene_prev (&mut self) { + *self = match self { + Self::Mix => + Self::Mix, + Self::Track(t) => + Self::Track(*t), + Self::Scene(s) => + if *s == 0 { Self::Mix } else { Self::Scene(*s - 1) }, + Self::Clip(t, s) => + if *s == 0 { Self::Track(*t) } else { Self::Clip(*t, s.saturating_sub(1)) } + } + } +} diff --git a/crates/tek_tui/src/tui_arranger.rs b/crates/tek_tui/src/tui_arranger.rs index 31715f23..520eaa64 100644 --- a/crates/tek_tui/src/tui_arranger.rs +++ b/crates/tek_tui/src/tui_arranger.rs @@ -1,7 +1,7 @@ use crate::*; /// Root level object for standalone `tek_arranger` -pub struct ArrangerView { +pub struct ArrangerApp { /// JACK client handle (needs to not be dropped for standalone mode to work). pub jack: Arc>, /// Which view is focused @@ -13,7 +13,7 @@ pub struct ArrangerView { /// Global timebase pub clock: Arc, /// Contains all the sequencers. - pub arrangement: Arrangement, + pub arrangement: ArrangementEditor, /// Pool of all phrases in the arrangement pub phrases: Arc>>, /// Phrase editor view @@ -31,102 +31,79 @@ pub struct ArrangerView { /// Command history pub history: Vec, } -/// Sections in the arranger app that may be focused -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum ArrangerFocus { - /// The transport (toolbar) is focused - Transport, - /// The arrangement (grid) is focused - Arrangement, - /// The phrase list (pool) is focused - PhrasePool, - /// The phrase editor (sequencer) is focused - PhraseEditor, + +impl Audio for ArrangerApp { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + if let Some(ref transport) = self.transport { + transport.write().unwrap().process(client, scope); + } + let Arrangement { scenes, ref mut tracks, selected, .. } = &mut self.arrangement; + for track in tracks.iter_mut() { + track.player.process(client, scope); + } + if let ArrangementEditorFocus::Clip(t, s) = selected { + if let Some(Some(Some(phrase))) = scenes.get(*s).map(|scene|scene.clips.get(*t)) { + if let Some(track) = tracks.get(*t) { + if let Some((ref started_at, Some(ref playing))) = track.player.phrase { + let phrase = phrase.read().unwrap(); + if *playing.read().unwrap() == *phrase { + let pulse = self.clock.current.pulse.get(); + let start = started_at.pulse.get(); + let now = (pulse - start) % phrase.length as f64; + self.editor.now.set(now); + return Control::Continue + } + } + } + } + } + self.editor.now.set(0.); + Control::Continue + } } -/// Status bar for arranger ap -pub enum ArrangerStatusBar { - Transport, - ArrangementMix, - ArrangementTrack, - ArrangementScene, - ArrangementClip, - PhrasePool, - PhraseView, - PhraseEdit, + +/// Layout for standalone arranger app. +impl Content for ArrangerApp { + type Engine = Tui; + fn content (&self) -> impl Widget { + let focused = self.arrangement.focused; + let border_bg = Arranger::::border_bg(); + let border_fg = Arranger::::border_fg(focused); + let title_fg = Arranger::::title_fg(focused); + let border = Lozenge(Style::default().bg(border_bg).fg(border_fg)); + let entered = if self.arrangement.entered { "■" } else { " " }; + Split::down( + 1, + row!(menu in self.menu.menus.iter() => { + row!(" ", menu.title.as_str(), " ") + }), + Split::up( + 1, + widget(&self.status), + Split::up( + 1, + widget(&self.transport), + Split::down( + self.arrangement_split, + lay!( + widget(&self.arrangement).grow_y(1).border(border), + widget(&self.arrangement.size), + widget(&format!("[{}] Arrangement", entered)).fg(title_fg).push_x(1), + ), + Split::right( + self.phrases_split, + self.phrases.clone(), + widget(&self.editor), + ) + ) + ) + ) + ) + } } -/// Represents the tracks and scenes of the composition. -pub struct Arrangement { - /// Global JACK client - pub jack: Arc>, - /// Global timebase - pub clock: Arc, - /// Name of arranger - pub name: Arc>, - /// Collection of phrases. - pub phrases: Arc>>, - /// Collection of tracks. - pub tracks: Vec, - /// Collection of scenes. - pub scenes: Vec, - /// Currently selected element. - pub selected: ArrangementFocus, - /// Display mode of arranger - pub mode: ArrangementViewMode, - /// Whether the arranger is currently focused - pub focused: bool, - /// Background color of arrangement - pub color: ItemColor, - /// Width and height of arrangement area at last render - pub size: Measure, - /// Whether this is currently in edit mode - pub entered: bool, -} -/// Represents a track in the arrangement -pub struct ArrangementTrack { - /// Name of track - pub name: Arc>, - /// Preferred width of track column - pub width: usize, - /// Identifying color of track - pub color: ItemColor, - /// MIDI player/recorder - pub player: PhrasePlayer, -} -#[derive(Default, Debug)] -pub struct Scene { - /// Name of scene - pub name: Arc>, - /// Clips in scene, one per track - pub clips: Vec>>>, - /// Identifying color of scene - pub color: ItemColor, -} -#[derive(PartialEq, Clone, Copy)] -/// Represents the current user selection in the arranger -pub enum ArrangementFocus { - /// The whole mix is selected - Mix, - /// A track is selected. - Track(usize), - /// A scene is selected. - Scene(usize), - /// A clip (track × scene) is selected. - Clip(usize, usize), -} -/// Display mode of arranger -#[derive(PartialEq)] -pub enum ArrangementViewMode { - /// Tracks are rows - Horizontal, - /// Tracks are columns - Vertical(usize), -} -/// Arrangement, rendered vertically (session/grid mode). -pub struct VerticalArranger<'a, E: Engine>(pub &'a Arrangement, pub usize); -/// Arrangement, rendered horizontally (arrangement/track mode). -pub struct HorizontalArranger<'a, E: Engine>(pub &'a Arrangement); + /// General methods for arranger -impl Arranger { +impl ArrangerApp { pub fn new ( jack: &Arc>, transport: Option>>>, @@ -208,6 +185,29 @@ impl Arranger { app.update_focus(); app } + + //pub fn new ( + //jack: &Arc>, + //clock: &Arc, + //name: &str, + //phrases: &Arc>> + //) -> Self { + //Self { + //jack: jack.clone(), + //clock: clock.clone(), + //name: Arc::new(RwLock::new(name.into())), + //mode: ArrangementViewMode::Vertical(2), + //selected: ArrangementEditorFocus::Clip(0, 0), + //phrases: phrases.clone(), + //scenes: vec![], + //tracks: vec![], + //focused: false, + //color: Color::Rgb(28, 35, 25).into(), + //size: Measure::new(), + //entered: false, + //} + //} + /// Toggle global play/pause pub fn toggle_play (&mut self) -> Perhaps { match self.transport { @@ -217,7 +217,7 @@ impl Arranger { Ok(Some(true)) } pub fn next_color (&self) -> ItemColor { - if let ArrangementFocus::Clip(track, scene) = self.arrangement.selected { + if let ArrangementEditorFocus::Clip(track, scene) = self.arrangement.selected { let track_color = self.arrangement.tracks[track].color; let scene_color = self.arrangement.scenes[scene].color; track_color.mix(scene_color, 0.5).mix(ItemColor::random(), 0.25) @@ -234,20 +234,20 @@ impl Arranger { self.arrangement.phrase_put(); } self.show_phrase(); - self.focus(ArrangerFocus::PhraseEditor); + self.focus(ArrangerAppFocus::PhraseEditor); self.editor.entered = true; } /// Rename the selected track, scene, or clip pub fn rename_selected (&mut self) { let Arrangement { selected, ref scenes, .. } = self.arrangement; match selected { - ArrangementFocus::Mix => {}, - ArrangementFocus::Track(_) => { todo!("rename track"); }, - ArrangementFocus::Scene(_) => { todo!("rename scene"); }, - ArrangementFocus::Clip(t, s) => if let Some(ref phrase) = scenes[s].clips[t] { + ArrangementEditorFocus::Mix => {}, + ArrangementEditorFocus::Track(_) => { todo!("rename track"); }, + ArrangementEditorFocus::Scene(_) => { todo!("rename scene"); }, + ArrangementEditorFocus::Clip(t, s) => if let Some(ref phrase) = scenes[s].clips[t] { let index = self.phrases.read().unwrap().index_of(&*phrase.read().unwrap()); if let Some(index) = index { - self.focus(ArrangerFocus::PhrasePool); + self.focus(ArrangerAppFocus::PhrasePool); self.phrases.write().unwrap().phrase = index; self.phrases.write().unwrap().begin_rename(); } @@ -257,103 +257,23 @@ impl Arranger { /// Update status bar pub fn update_status (&mut self) { self.status = match self.focused() { - ArrangerFocus::Transport => ArrangerStatusBar::Transport, - ArrangerFocus::Arrangement => match self.arrangement.selected { - ArrangementFocus::Mix => ArrangerStatusBar::ArrangementMix, - ArrangementFocus::Track(_) => ArrangerStatusBar::ArrangementTrack, - ArrangementFocus::Scene(_) => ArrangerStatusBar::ArrangementScene, - ArrangementFocus::Clip(_, _) => ArrangerStatusBar::ArrangementClip, + ArrangerAppFocus::Transport => ArrangerStatusBar::Transport, + ArrangerAppFocus::Arrangement => match self.arrangement.selected { + ArrangementEditorFocus::Mix => ArrangerStatusBar::ArrangementMix, + ArrangementEditorFocus::Track(_) => ArrangerStatusBar::ArrangementTrack, + ArrangementEditorFocus::Scene(_) => ArrangerStatusBar::ArrangementScene, + ArrangementEditorFocus::Clip(_, _) => ArrangerStatusBar::ArrangementClip, }, - ArrangerFocus::PhrasePool => ArrangerStatusBar::PhrasePool, - ArrangerFocus::PhraseEditor => match self.editor.entered { + ArrangerAppFocus::PhrasePool => ArrangerStatusBar::PhrasePool, + ArrangerAppFocus::PhraseEditor => match self.editor.entered { true => ArrangerStatusBar::PhraseEdit, false => ArrangerStatusBar::PhraseView, }, } } -} -/// Focus layout of arranger app -impl FocusGrid for Arranger { - type Item = ArrangerFocus; - fn cursor (&self) -> (usize, usize) { self.focus_cursor } - fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor } - fn focus_enter (&mut self) { - let focused = self.focused(); - if !self.entered { - self.entered = true; - use ArrangerFocus::*; - if let Some(transport) = self.transport.as_ref() { - //transport.write().unwrap().entered = focused == Transport - } - self.arrangement.entered = focused == Arrangement; - self.phrases.write().unwrap().entered = focused == PhrasePool; - self.editor.entered = focused == PhraseEditor; - } - } - fn focus_exit (&mut self) { - if self.entered { - self.entered = false; - self.arrangement.entered = false; - self.editor.entered = false; - self.phrases.write().unwrap().entered = false; - } - } - fn entered (&self) -> Option { - if self.entered { - Some(self.focused()) - } else { - None - } - } - fn layout (&self) -> &[&[ArrangerFocus]] { - use ArrangerFocus::*; - &[ - &[Transport, Transport], - &[Arrangement, Arrangement], - &[PhrasePool, PhraseEditor], - ] - } - fn update_focus (&mut self) { - use ArrangerFocus::*; - let focused = self.focused(); - if let Some(transport) = self.transport.as_ref() { - transport.write().unwrap().focused = focused == Transport - } - self.arrangement.focused = focused == Arrangement; - self.phrases.write().unwrap().focused = focused == PhrasePool; - self.editor.focused = focused == PhraseEditor; - self.update_status(); - } -} -/// General methods for arrangement -impl Arrangement { - pub fn new ( - jack: &Arc>, - clock: &Arc, - name: &str, - phrases: &Arc>> - ) -> Self { - Self { - jack: jack.clone(), - clock: clock.clone(), - name: Arc::new(RwLock::new(name.into())), - mode: ArrangementViewMode::Vertical(2), - selected: ArrangementFocus::Clip(0, 0), - phrases: phrases.clone(), - scenes: vec![], - tracks: vec![], - focused: false, - color: Color::Rgb(28, 35, 25).into(), - size: Measure::new(), - entered: false, - } - } - fn is_stopped (&self) -> bool { - *self.clock.playing.read().unwrap() == Some(TransportState::Stopped) - } pub fn activate (&mut self) { match self.selected { - ArrangementFocus::Scene(s) => { + ArrangementEditorFocus::Scene(s) => { for (t, track) in self.tracks.iter_mut().enumerate() { let player = &mut track.player; let clip = self.scenes[s].clips[t].as_ref(); @@ -367,7 +287,7 @@ impl Arrangement { //self.transport.toggle_play() //} }, - ArrangementFocus::Clip(t, s) => { + ArrangementEditorFocus::Clip(t, s) => { self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref()); }, _ => {} @@ -375,26 +295,26 @@ impl Arrangement { } pub fn delete (&mut self) { match self.selected { - ArrangementFocus::Track(_) => self.track_del(), - ArrangementFocus::Scene(_) => self.scene_del(), - ArrangementFocus::Clip(_, _) => self.phrase_del(), + ArrangementEditorFocus::Track(_) => self.track_del(), + ArrangementEditorFocus::Scene(_) => self.scene_del(), + ArrangementEditorFocus::Clip(_, _) => self.phrase_del(), _ => {} } } pub fn increment (&mut self) { match self.selected { - ArrangementFocus::Track(_) => self.track_width_inc(), - ArrangementFocus::Scene(_) => self.scene_next(), - ArrangementFocus::Clip(_, _) => self.phrase_next(), - ArrangementFocus::Mix => self.zoom_in(), + ArrangementEditorFocus::Track(_) => self.track_width_inc(), + ArrangementEditorFocus::Scene(_) => self.scene_next(), + ArrangementEditorFocus::Clip(_, _) => self.phrase_next(), + ArrangementEditorFocus::Mix => self.zoom_in(), } } pub fn decrement (&mut self) { match self.selected { - ArrangementFocus::Track(_) => self.track_width_dec(), - ArrangementFocus::Scene(_) => self.scene_prev(), - ArrangementFocus::Clip(_, _) => self.phrase_prev(), - ArrangementFocus::Mix => self.zoom_out(), + ArrangementEditorFocus::Track(_) => self.track_width_dec(), + ArrangementEditorFocus::Scene(_) => self.scene_prev(), + ArrangementEditorFocus::Clip(_, _) => self.phrase_prev(), + ArrangementEditorFocus::Mix => self.zoom_out(), } } pub fn zoom_in (&mut self) { @@ -414,8 +334,8 @@ impl Arrangement { pub fn is_last_row (&self) -> bool { let selected = self.selected; (self.scenes.len() == 0 && (selected.is_mix() || selected.is_track())) || match selected { - ArrangementFocus::Scene(s) => s == self.scenes.len() - 1, - ArrangementFocus::Clip(_, s) => s == self.scenes.len() - 1, + ArrangementEditorFocus::Scene(s) => s == self.scenes.len() - 1, + ArrangementEditorFocus::Clip(_, s) => s == self.scenes.len() - 1, _ => false } } @@ -450,16 +370,16 @@ impl Arrangement { } pub fn move_back (&mut self) { match self.selected { - ArrangementFocus::Scene(s) => { + ArrangementEditorFocus::Scene(s) => { if s > 0 { self.scenes.swap(s, s - 1); - self.selected = ArrangementFocus::Scene(s - 1); + self.selected = ArrangementEditorFocus::Scene(s - 1); } }, - ArrangementFocus::Track(t) => { + ArrangementEditorFocus::Track(t) => { if t > 0 { self.tracks.swap(t, t - 1); - self.selected = ArrangementFocus::Track(t - 1); + self.selected = ArrangementEditorFocus::Track(t - 1); // FIXME: also swap clip order in scenes } }, @@ -468,16 +388,16 @@ impl Arrangement { } pub fn move_forward (&mut self) { match self.selected { - ArrangementFocus::Scene(s) => { + ArrangementEditorFocus::Scene(s) => { if s < self.scenes.len().saturating_sub(1) { self.scenes.swap(s, s + 1); - self.selected = ArrangementFocus::Scene(s + 1); + self.selected = ArrangementEditorFocus::Scene(s + 1); } }, - ArrangementFocus::Track(t) => { + ArrangementEditorFocus::Track(t) => { if t < self.tracks.len().saturating_sub(1) { self.tracks.swap(t, t + 1); - self.selected = ArrangementFocus::Track(t + 1); + self.selected = ArrangementEditorFocus::Track(t + 1); // FIXME: also swap clip order in scenes } }, @@ -486,15 +406,16 @@ impl Arrangement { } pub fn randomize_color (&mut self) { match self.selected { - ArrangementFocus::Mix => { self.color = ItemColor::random_dark() }, - ArrangementFocus::Track(t) => { self.tracks[t].color = ItemColor::random() }, - ArrangementFocus::Scene(s) => { self.scenes[s].color = ItemColor::random() }, - ArrangementFocus::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] { + ArrangementEditorFocus::Mix => { self.color = ItemColor::random_dark() }, + ArrangementEditorFocus::Track(t) => { self.tracks[t].color = ItemColor::random() }, + ArrangementEditorFocus::Scene(s) => { self.scenes[s].color = ItemColor::random() }, + ArrangementEditorFocus::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] { phrase.write().unwrap().color = ItemColorTriplet::random(); } } } } + /// Methods for tracks in arrangement impl Arrangement { pub fn track (&self) -> Option<&ArrangementTrack> { @@ -597,12 +518,12 @@ impl Arrangement { .map(|scene|scene.clips[track_index] = None)); } pub fn phrase_put (&mut self) { - if let ArrangementFocus::Clip(track, scene) = self.selected { + if let ArrangementEditorFocus::Clip(track, scene) = self.selected { self.scenes[scene].clips[track] = Some(self.phrases.read().unwrap().phrase().clone()); } } pub fn phrase_get (&mut self) { - if let ArrangementFocus::Clip(track, scene) = self.selected { + if let ArrangementEditorFocus::Clip(track, scene) = self.selected { if let Some(phrase) = &self.scenes[scene].clips[track] { let mut phrases = self.phrases.write().unwrap(); if let Some(index) = phrases.index_of(&*phrase.read().unwrap()) { @@ -612,7 +533,7 @@ impl Arrangement { } } pub fn phrase_next (&mut self) { - if let ArrangementFocus::Clip(track, scene) = self.selected { + if let ArrangementEditorFocus::Clip(track, scene) = self.selected { if let Some(ref mut phrase) = self.scenes[scene].clips[track] { let phrases = self.phrases.read().unwrap(); let index = phrases.index_of(&*phrase.read().unwrap()); @@ -625,7 +546,7 @@ impl Arrangement { } } pub fn phrase_prev (&mut self) { - if let ArrangementFocus::Clip(track, scene) = self.selected { + if let ArrangementEditorFocus::Clip(track, scene) = self.selected { if let Some(ref mut phrase) = self.scenes[scene].clips[track] { let phrases = self.phrases.read().unwrap(); let index = phrases.index_of(&*phrase.read().unwrap()); @@ -638,6 +559,7 @@ impl Arrangement { } } } + impl ArrangementTrack { pub fn new ( jack: &Arc>, @@ -659,91 +581,7 @@ impl ArrangementTrack { pub fn width_inc (&mut self) { self.width += 1; } pub fn width_dec (&mut self) { if self.width > Self::MIN_WIDTH { self.width -= 1; } } } -/// Focus identification methods -impl ArrangementFocus { - pub fn description ( - &self, - tracks: &Vec, - scenes: &Vec, - ) -> String { - format!("Selected: {}", match self { - Self::Mix => format!("Everything"), - Self::Track(t) => match tracks.get(*t) { - Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), - None => format!("T??"), - }, - Self::Scene(s) => match scenes.get(*s) { - Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()), - None => format!("S??"), - }, - 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"), - } - }) - } - pub fn is_mix (&self) -> bool { match self { Self::Mix => true, _ => false } } - pub fn is_track (&self) -> bool { match self { Self::Track(_) => true, _ => false } } - pub fn is_scene (&self) -> bool { match self { Self::Scene(_) => true, _ => false } } - pub fn is_clip (&self) -> bool { match self { Self::Clip(_, _) => true, _ => false } } - pub fn track (&self) -> Option { - match self { Self::Clip(t, _) => Some(*t), Self::Track(t) => Some(*t), _ => None } - } - pub fn track_next (&mut self, last_track: usize) { - *self = match self { - Self::Mix => - Self::Track(0), - Self::Track(t) => - Self::Track(last_track.min(*t + 1)), - Self::Scene(s) => - Self::Clip(0, *s), - Self::Clip(t, s) => - Self::Clip(last_track.min(*t + 1), *s), - } - } - pub fn track_prev (&mut self) { - *self = match self { - Self::Mix => - Self::Mix, - Self::Scene(s) => - Self::Scene(*s), - Self::Track(t) => - if *t == 0 { Self::Mix } else { Self::Track(*t - 1) }, - Self::Clip(t, s) => - if *t == 0 { Self::Scene(*s) } else { Self::Clip(t.saturating_sub(1), *s) } - } - } - pub fn scene (&self) -> Option { - match self { Self::Clip(_, s) => Some(*s), Self::Scene(s) => Some(*s), _ => None } - } - pub fn scene_next (&mut self, last_scene: usize) { - *self = match self { - Self::Mix => - Self::Scene(0), - Self::Track(t) => - Self::Clip(*t, 0), - Self::Scene(s) => - Self::Scene(last_scene.min(*s + 1)), - Self::Clip(t, s) => - Self::Clip(*t, last_scene.min(*s + 1)), - } - } - pub fn scene_prev (&mut self) { - *self = match self { - Self::Mix => - Self::Mix, - Self::Track(t) => - Self::Track(*t), - Self::Scene(s) => - if *s == 0 { Self::Mix } else { Self::Scene(*s - 1) }, - Self::Clip(t, s) => - if *s == 0 { Self::Track(*t) } else { Self::Clip(*t, s.saturating_sub(1)) } - } - } -} + /// Arranger display mode can be cycled impl ArrangementViewMode { /// Cycle arranger display mode @@ -769,27 +607,6 @@ impl Scene { color: color.unwrap_or_else(ItemColor::random), } } - /// Returns the pulse length of the longest phrase 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 phrases in the scene are currently playing - pub fn is_playing (&self, tracks: &[ArrangementTrack]) -> bool { - self.clips.iter().any(|clip|clip.is_some()) && self.clips.iter().enumerate() - .all(|(track_index, clip)|match clip { - Some(clip) => tracks - .get(track_index) - .map(|track|if let Some((_, Some(phrase))) = &track.player.phrase { - *phrase.read().unwrap() == *clip.read().unwrap() - } else { - false - }) - .unwrap_or(false), - None => true - }) - } pub fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> { let mut total = 0; if factor == 0 { @@ -807,60 +624,4 @@ impl Scene { pub fn longest_name (scenes: &[Self]) -> usize { scenes.iter().map(|s|s.name.read().unwrap().len()).fold(0, usize::max) } - pub fn clip (&self, index: usize) -> Option<&Arc>> { - match self.clips.get(index) { Some(Some(clip)) => Some(clip), _ => None } - } -} - -/// Layout for standalone arranger app. -impl Content for Arranger { - type Engine = Tui; - fn content (&self) -> impl Widget { - let focused = self.arrangement.focused; - let border_bg = Arranger::::border_bg(); - let border_fg = Arranger::::border_fg(focused); - let title_fg = Arranger::::title_fg(focused); - let border = Lozenge(Style::default().bg(border_bg).fg(border_fg)); - let entered = if self.arrangement.entered { "■" } else { " " }; - Split::down( - 1, - row!(menu in self.menu.menus.iter() => { - row!(" ", menu.title.as_str(), " ") - }), - Split::up( - 1, - widget(&self.status), - Split::up( - 1, - widget(&self.transport), - Split::down( - self.arrangement_split, - lay!( - widget(&self.arrangement).grow_y(1).border(border), - widget(&self.arrangement.size), - widget(&format!("[{}] Arrangement", entered)).fg(title_fg).push_x(1), - ), - Split::right( - self.phrases_split, - self.phrases.clone(), - widget(&self.editor), - ) - ) - ) - ) - ) - } -} - -impl Content for Arrangement { - type Engine = Tui; - fn content (&self) -> impl Widget { - Layers::new(move |add|{ - match self.mode { - ArrangementViewMode::Horizontal => { add(&HorizontalArranger(&self)) }, - ArrangementViewMode::Vertical(factor) => { add(&VerticalArranger(&self, factor)) }, - }?; - add(&self.size) - }) - } } diff --git a/crates/tek_tui/src/tui_arranger_bar.rs b/crates/tek_tui/src/tui_arranger_bar.rs index 722154df..233cdca8 100644 --- a/crates/tek_tui/src/tui_arranger_bar.rs +++ b/crates/tek_tui/src/tui_arranger_bar.rs @@ -1,5 +1,16 @@ use crate::*; +/// Status bar for arranger ap +pub enum ArrangerStatusBar { + Transport, + ArrangementMix, + ArrangementTrack, + ArrangementScene, + ArrangementClip, + PhrasePool, + PhraseView, + PhraseEdit, +} impl Content for ArrangerStatusBar { type Engine = Tui; fn content (&self) -> impl Widget { diff --git a/crates/tek_tui/src/tui_arranger_foc.rs b/crates/tek_tui/src/tui_arranger_foc.rs new file mode 100644 index 00000000..f8d61baf --- /dev/null +++ b/crates/tek_tui/src/tui_arranger_foc.rs @@ -0,0 +1,68 @@ +use crate::*; + +/// Sections in the arranger app that may be focused +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum ArrangerAppFocus { + /// The transport (toolbar) is focused + Transport, + /// The arrangement (grid) is focused + Arrangement, + /// The phrase list (pool) is focused + PhrasePool, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} + +/// Focus layout of arranger app +impl FocusGrid for ArrangerApp { + type Item = ArrangerAppFocus; + fn cursor (&self) -> (usize, usize) { self.focus_cursor } + fn cursor_mut (&mut self) -> &mut (usize, usize) { &mut self.focus_cursor } + fn focus_enter (&mut self) { + let focused = self.focused(); + if !self.entered { + self.entered = true; + use ArrangerAppFocus::*; + if let Some(transport) = self.transport.as_ref() { + //transport.write().unwrap().entered = focused == Transport + } + self.arrangement.entered = focused == Arrangement; + self.phrases.write().unwrap().entered = focused == PhrasePool; + self.editor.entered = focused == PhraseEditor; + } + } + fn focus_exit (&mut self) { + if self.entered { + self.entered = false; + self.arrangement.entered = false; + self.editor.entered = false; + self.phrases.write().unwrap().entered = false; + } + } + fn entered (&self) -> Option { + if self.entered { + Some(self.focused()) + } else { + None + } + } + fn layout (&self) -> &[&[ArrangerAppFocus]] { + use ArrangerAppFocus::*; + &[ + &[Transport, Transport], + &[Arrangement, Arrangement], + &[PhrasePool, PhraseEditor], + ] + } + fn update_focus (&mut self) { + use ArrangerAppFocus::*; + let focused = self.focused(); + if let Some(transport) = self.transport.as_ref() { + transport.write().unwrap().focused = focused == Transport + } + self.arrangement.focused = focused == Arrangement; + self.phrases.write().unwrap().focused = focused == PhrasePool; + self.editor.focused = focused == PhraseEditor; + self.update_status(); + } +} diff --git a/crates/tek_tui/src/tui_arranger_hor.rs b/crates/tek_tui/src/tui_arranger_hor.rs index 2f54a0df..c528073f 100644 --- a/crates/tek_tui/src/tui_arranger_hor.rs +++ b/crates/tek_tui/src/tui_arranger_hor.rs @@ -1,5 +1,8 @@ use crate::*; +/// Arrangement, rendered horizontally (arrangement/track mode). +pub struct HorizontalArranger<'a, E: Engine>(pub &'a Arrangement); + impl<'a> Content for HorizontalArranger<'a, Tui> { type Engine = Tui; fn content (&self) -> impl Widget { diff --git a/crates/tek_tui/src/tui_arranger_ver.rs b/crates/tek_tui/src/tui_arranger_ver.rs index 5fa68ad3..d7ecfc7f 100644 --- a/crates/tek_tui/src/tui_arranger_ver.rs +++ b/crates/tek_tui/src/tui_arranger_ver.rs @@ -1,5 +1,8 @@ use crate::*; +/// Arrangement, rendered vertically (session/grid mode). +pub struct VerticalArranger<'a, E: Engine>(pub &'a Arrangement, pub usize); + impl<'a> Content for VerticalArranger<'a, Tui> { type Engine = Tui; fn content (&self) -> impl Widget { diff --git a/crates/tek_tui/src/tui_phrase.rs b/crates/tek_tui/src/tui_phrase.rs new file mode 100644 index 00000000..679cc742 --- /dev/null +++ b/crates/tek_tui/src/tui_phrase.rs @@ -0,0 +1,418 @@ +use crate::*; + +/// Contains state for viewing and editing a phrase +pub struct PhraseEditor { + _engine: PhantomData, + /// Phrase being played + pub phrase: Option>>, + /// Length of note that will be inserted, in pulses + pub note_len: usize, + /// The full piano keys are rendered to this buffer + pub keys: Buffer, + /// The full piano roll is rendered to this buffer + pub buffer: BigBuffer, + /// Cursor/scroll/zoom in pitch axis + pub note_axis: RwLock>, + /// Cursor/scroll/zoom in time axis + pub time_axis: RwLock>, + /// Whether this widget is focused + pub focused: bool, + /// Whether note enter mode is enabled + pub entered: bool, + /// Display mode + pub mode: bool, + /// Notes currently held at input + pub notes_in: Arc>, + /// Notes currently held at output + pub notes_out: Arc>, + /// Current position of global playhead + pub now: Arc, + /// Width of notes area at last render + pub width: AtomicUsize, + /// Height of notes area at last render + pub height: AtomicUsize, +} + +impl Content for PhraseEditor { + type Engine = Tui; + fn content (&self) -> impl Widget { + let Self { focused, entered, keys, phrase, buffer, note_len, .. } = self; + let FixedAxis { + start: note_start, point: note_point, clamp: note_clamp + } = *self.note_axis.read().unwrap(); + let ScaledAxis { + start: time_start, point: time_point, clamp: time_clamp, scale: time_scale + } = *self.time_axis.read().unwrap(); + //let color = Color::Rgb(0,255,0); + //let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color); + let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ + Ok(if to.area().h() >= 2 { + to.buffer_update(to.area().set_w(5), &|cell, x, y|{ + let y = y + (note_start / 2) as u16; + if x < keys.area.width && y < keys.area.height { + *cell = keys.get(x, y).clone() + } + }); + }) + }).fill_y(); + let notes_bg_null = Color::Rgb(28, 35, 25); + let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ + let area = to.area(); + let h = area.h() as usize; + self.height.store(h, Ordering::Relaxed); + self.width.store(area.w() as usize, Ordering::Relaxed); + let mut axis = self.note_axis.write().unwrap(); + if let Some(point) = axis.point { + if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) { + axis.start += 2; + } + } + Ok(if to.area().h() >= 2 { + let area = to.area(); + to.buffer_update(area, &move |cell, x, y|{ + cell.set_bg(notes_bg_null); + let src_x = (x as usize + time_start) * time_scale; + let src_y = y as usize + note_start / 2; + if src_x < buffer.width && src_y < buffer.height - 1 { + buffer.get(src_x, buffer.height - src_y - 2).map(|src|{ + cell.set_symbol(src.symbol()); + cell.set_fg(src.fg); + cell.set_bg(src.bg); + }); + } + }); + }) + }).fill_x(); + let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ + Ok(if *focused && *entered { + let area = to.area(); + if let (Some(time), Some(note)) = (time_point, note_point) { + let x1 = area.x() + (time / time_scale) as u16; + let x2 = x1 + (self.note_len / time_scale) as u16; + let y = area.y() + note.saturating_sub(note_start) as u16 / 2; + let c = if note % 2 == 0 { "▀" } else { "▄" }; + for x in x1..x2 { + to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0)))); + } + } + }) + }); + let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30)); + let playhead_active = playhead_inactive.clone().yellow().bold().not_dim(); + let playhead = CustomWidget::new( + |to:[u16;2]|Ok(Some(to.clip_h(1))), + move|to: &mut TuiOutput|{ + if let Some(_) = phrase { + let now = self.now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; + let time_clamp = time_clamp + .expect("time_axis of sequencer expected to be clamped"); + for x in 0..(time_clamp/time_scale).saturating_sub(time_start) { + let this_step = time_start + (x + 0) * time_scale; + let next_step = time_start + (x + 1) * time_scale; + let x = to.area().x() + x as u16; + let active = this_step <= now && now < next_step; + let character = if active { "|" } else { "·" }; + let style = if active { playhead_active } else { playhead_inactive }; + to.blit(&character, x, to.area.y(), Some(style)); + } + } + Ok(()) + } + ).push_x(6).align_sw(); + let border_color = if *focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)}; + let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)}; + let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); + let note_area = lay!(notes, cursor).fill_x(); + let piano_roll = row!(keys, note_area).fill_x(); + let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border); + let content = lay!(content, playhead); + let mut upper_left = format!("[{}] Sequencer", if *entered {"■"} else {" "}); + if let Some(phrase) = phrase { + upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name); + } + let mut lower_right = format!( + "┤{}x{}├", + self.width.load(Ordering::Relaxed), + self.height.load(Ordering::Relaxed), + ); + lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale)); + //lower_right = format!("Zoom: {} (+{}:{}*{}|{})", + //pulses_to_name(time_scale), + //time_start, time_point.unwrap_or(0), + //time_scale, time_clamp.unwrap_or(0), + //); + if *focused && *entered { + lower_right = format!("┤Note: {} {}├─{lower_right}", + self.note_axis.read().unwrap().point.unwrap(), + pulses_to_name(*note_len)); + //lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}", + //pulses_to_name(*note_len), + //note_start, + //note_point.unwrap_or(0), + //note_clamp.unwrap_or(0), + //); + } + let upper_right = if let Some(phrase) = phrase { + format!("┤Length: {}├", phrase.read().unwrap().length) + } else { + String::new() + }; + lay!( + content, + TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), + TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), + TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(), + ) + } +} + +impl PhraseEditor { + pub fn new () -> Self { + Self { + _engine: Default::default(), + phrase: None, + note_len: 24, + notes_in: Arc::new(RwLock::new([false;128])), + notes_out: Arc::new(RwLock::new([false;128])), + keys: keys_vert(), + buffer: Default::default(), + focused: false, + entered: false, + mode: false, + now: Arc::new(0.into()), + width: 0.into(), + height: 0.into(), + note_axis: RwLock::new(FixedAxis { + start: 12, + point: Some(36), + clamp: Some(127) + }), + time_axis: RwLock::new(ScaledAxis { + start: 00, + point: Some(00), + clamp: Some(000), + scale: 24 + }), + } + } + + pub fn note_cursor_inc (&self) { + let mut axis = self.note_axis.write().unwrap(); + axis.point_dec(1); + if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } + } + + pub fn note_cursor_dec (&self) { + let mut axis = self.note_axis.write().unwrap(); + axis.point_inc(1); + if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } + } + + pub fn note_page_up (&self) { + let mut axis = self.note_axis.write().unwrap(); + axis.start_dec(3); + axis.point_dec(3); + } + + pub fn note_page_down (&self) { + let mut axis = self.note_axis.write().unwrap(); + axis.start_inc(3); + axis.point_inc(3); + } + + pub fn note_scroll_inc (&self) { + self.note_axis.write().unwrap().start_dec(1); + } + + pub fn note_scroll_dec (&self) { + self.note_axis.write().unwrap().start_inc(1); + } + + pub fn note_length_inc (&mut self) { + self.note_len = next_note_length(self.note_len) + } + + pub fn note_length_dec (&mut self) { + self.note_len = prev_note_length(self.note_len) + } + + pub fn time_cursor_advance (&self) { + let point = self.time_axis.read().unwrap().point; + let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); + let forward = |time|(time + self.note_len) % length; + self.time_axis.write().unwrap().point = point.map(forward); + } + + pub fn time_cursor_inc (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().point_inc(scale); + } + + pub fn time_cursor_dec (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().point_dec(scale); + } + + pub fn time_scroll_inc (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().start_inc(scale); + } + + pub fn time_scroll_dec (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().start_dec(scale); + } + + pub fn time_zoom_in (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().scale = prev_note_length(scale) + } + + pub fn time_zoom_out (&self) { + let scale = self.time_axis.read().unwrap().scale; + self.time_axis.write().unwrap().scale = next_note_length(scale) + } + +} + +impl PhraseEditor { + pub fn put (&mut self) { + if let (Some(phrase), Some(time), Some(note)) = ( + &self.phrase, + self.time_axis.read().unwrap().point, + self.note_axis.read().unwrap().point, + ) { + let mut phrase = phrase.write().unwrap(); + let key: u7 = u7::from((127 - note) as u8); + let vel: u7 = 100.into(); + let start = time; + let end = (start + self.note_len) % phrase.length; + phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); + phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); + self.buffer = Self::redraw(&phrase); + } + } + /// Select which pattern to display. This pre-renders it to the buffer at full resolution. + pub fn show (&mut self, phrase: Option<&Arc>>) { + if let Some(phrase) = phrase { + self.phrase = Some(phrase.clone()); + self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length); + self.buffer = Self::redraw(&*phrase.read().unwrap()); + } else { + self.phrase = None; + self.time_axis.write().unwrap().clamp = Some(0); + self.buffer = Default::default(); + } + } + fn redraw (phrase: &Phrase) -> BigBuffer { + let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); + Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq); + Self::fill_seq_fg(&mut buf, &phrase); + buf + } + fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) { + for x in 0..buf.width { + // Only fill as far as phrase length + if x as usize >= length { break } + // Fill each row with background characters + for y in 0 .. buf.height { + buf.get_mut(x, y).map(|cell|{ + cell.set_char(if ppq == 0 { + '·' + } else if x % (4 * ppq) == 0 { + '│' + } else if x % ppq == 0 { + '╎' + } else { + '·' + }); + cell.set_fg(Color::Rgb(48, 64, 56)); + cell.modifier = Modifier::DIM; + }); + } + } + } + fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) { + let mut notes_on = [false;128]; + for x in 0..buf.width { + if x as usize >= phrase.length { + break + } + if let Some(notes) = phrase.notes.get(x as usize) { + if phrase.percussive { + for note in notes { + match note { + MidiMessage::NoteOn { key, .. } => + notes_on[key.as_int() as usize] = true, + _ => {} + } + } + } else { + for note in notes { + match note { + MidiMessage::NoteOn { key, .. } => + notes_on[key.as_int() as usize] = true, + MidiMessage::NoteOff { key, .. } => + notes_on[key.as_int() as usize] = false, + _ => {} + } + } + } + for y in 0..buf.height { + if y >= 64 { + break + } + if let Some(block) = half_block( + notes_on[y as usize * 2], + notes_on[y as usize * 2 + 1], + ) { + buf.get_mut(x, y).map(|cell|{ + cell.set_char(block); + cell.set_fg(Color::White); + }); + } + } + if phrase.percussive { + notes_on.fill(false); + } + } + } + } +} + +/// Colors of piano keys +const KEY_COLORS: [(Color, Color);6] = [ + (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)), + (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), + (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), + (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), + (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), + (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), +]; + +pub(crate) fn keys_vert () -> Buffer { + let area = [0, 0, 5, 64]; + let mut buffer = Buffer::empty(Rect { + x: area.x(), y: area.y(), width: area.w(), height: area.h() + }); + buffer_update(&mut buffer, area, &|cell, x, y| { + let y = 63 - y; + match x { + 0 => { + cell.set_char('▀'); + let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; + cell.set_fg(fg); + cell.set_bg(bg); + }, + 1 => { + cell.set_char('▀'); + cell.set_fg(Color::White); + cell.set_bg(Color::White); + }, + 2 => if y % 6 == 0 { cell.set_char('C'); }, + 3 => if y % 6 == 0 { cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); }, + _ => {} + } + }); + buffer +} diff --git a/crates/tek_tui/src/tui_phrase_cmd.rs b/crates/tek_tui/src/tui_phrase_cmd.rs new file mode 100644 index 00000000..5d73bfa6 --- /dev/null +++ b/crates/tek_tui/src/tui_phrase_cmd.rs @@ -0,0 +1,95 @@ +use crate::*; + +#[derive(Clone, PartialEq)] +pub enum PhraseEditorCommand { + // TODO: 1-9 seek markers that by default start every 8th of the phrase + ToggleDirection, + EnterEditMode, + ExitEditMode, + NoteAppend, + NoteSet, + NoteCursorSet(usize), + NoteLengthSet(usize), + NoteScrollSet(usize), + TimeCursorSet(usize), + TimeScrollSet(usize), + TimeZoomSet(usize), + Go(Direction), +} + +impl Handle for PhraseEditor { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + PhraseEditorCommand::execute_with_state(self, from) + } +} + +impl InputToCommand> for PhraseEditorCommand { + fn input_to_command (_: &PhraseEditor, from: &TuiInput) -> Option { + match from.event() { + key!(KeyCode::Char('`')) => Some(Self::ToggleDirection), + key!(KeyCode::Enter) => Some(Self::EnterEditMode), + key!(KeyCode::Esc) => Some(Self::ExitEditMode), + key!(KeyCode::Char('[')) => Some(Self::NoteLengthDec), + key!(KeyCode::Char(']')) => Some(Self::NoteLengthInc), + key!(KeyCode::Char('a')) => Some(Self::NoteAppend), + key!(KeyCode::Char('s')) => Some(Self::NoteSet), + key!(KeyCode::Char('-')) => Some(Self::TimeZoomOut), + key!(KeyCode::Char('_')) => Some(Self::TimeZoomOut), + key!(KeyCode::Char('=')) => Some(Self::TimeZoomIn), + key!(KeyCode::Char('+')) => Some(Self::TimeZoomIn), + key!(KeyCode::PageUp) => Some(Self::NotePageUp), + key!(KeyCode::PageDown) => Some(Self::NotePageDown), + key!(KeyCode::Up) => Some(Self::GoUp), + key!(KeyCode::Down) => Some(Self::GoDown), + key!(KeyCode::Left) => Some(Self::GoLeft), + key!(KeyCode::Right) => Some(Self::GoRight), + _ => None + } + } +} + +impl Command> for PhraseEditorCommand { + fn translate (self, state: &PhraseEditor) -> Self { + use PhraseEditorCommand::*; + match self { + GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, }, + GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, }, + GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, }, + GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, }, + _ => self + } + } + fn execute (self, state: &mut PhraseEditor) -> Perhaps { + use PhraseEditorCommand::*; + match self.translate(state) { + ToggleDirection => { state.mode = !state.mode; }, + EnterEditMode => { state.entered = true; }, + ExitEditMode => { state.entered = false; }, + TimeZoomOut => { state.time_zoom_out() }, + TimeZoomIn => { state.time_zoom_in() }, + TimeCursorDec => { state.time_cursor_dec() }, + TimeCursorInc => { state.time_cursor_inc() }, + TimeScrollDec => { state.time_scroll_dec() }, + TimeScrollInc => { state.time_scroll_inc() }, + NoteCursorDec => { state.note_cursor_dec() }, + NoteCursorInc => { state.note_cursor_inc() }, + NoteScrollDec => { state.note_scroll_dec() }, + NoteScrollInc => { state.note_scroll_inc() }, + NoteLengthDec => { state.note_length_dec() }, + NoteLengthInc => { state.note_length_inc() }, + NotePageUp => { state.note_page_up() }, + NotePageDown => { state.note_page_down() }, + NoteAppend => { + if state.entered { + state.put(); + state.time_cursor_advance(); + } + }, + NoteSet => { + if state.entered { state.put(); } + }, + _ => unreachable!() + } + Ok(None) + } +} diff --git a/crates/tek_tui/src/tui_pool.rs b/crates/tek_tui/src/tui_pool.rs new file mode 100644 index 00000000..9a59b0d2 --- /dev/null +++ b/crates/tek_tui/src/tui_pool.rs @@ -0,0 +1,232 @@ +use crate::*; + +pub struct PhrasePoolView { + _engine: PhantomData, + state: PhrasePool, + /// Scroll offset + pub scroll: usize, + /// Mode switch + pub mode: Option, + /// Whether this widget is focused + pub focused: bool, + /// Whether this widget is entered + pub entered: bool, +} + +/// Modes for phrase pool +pub enum PhrasePoolMode { + /// Renaming a pattern + Rename(usize, String), + /// Editing the length of a pattern + Length(usize, usize, PhraseLengthFocus), +} + +/// Displays and edits phrase length. +pub struct PhraseLength { + _engine: PhantomData, + /// Pulses per beat (quaver) + pub ppq: usize, + /// Beats per bar + pub bpb: usize, + /// Length of phrase in pulses + pub pulses: usize, + /// Selected subdivision + pub focus: Option, +} + +impl PhraseLength { + pub fn new (pulses: usize, focus: Option) -> Self { + Self { _engine: Default::default(), 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) -> String { format!("{}", self.bars()) } + pub fn beats_string (&self) -> String { format!("{}", self.beats()) } + pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) } +} + +/// Focused field of `PhraseLength` +#[derive(Copy, Clone)] pub enum PhraseLengthFocus { + /// Editing the number of bars + Bar, + /// Editing the number of beats + Beat, + /// Editing the number of ticks + Tick, +} + +impl PhraseLengthFocus { + 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, + } + } +} + +impl PhrasePool { + pub fn new () -> Self { + Self { + _engine: Default::default(), + scroll: 0, + phrase: 0, + phrases: vec![Arc::new(RwLock::new(Phrase::default()))], + mode: None, + focused: false, + entered: false, + } + } + pub fn len (&self) -> usize { self.phrases.len() } + pub fn phrase (&self) -> &Arc> { &self.phrases[self.phrase] } + pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) } + pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) } + pub fn index_before (&self, index: usize) -> usize { + index.overflowing_sub(1).0.min(self.len() - 1) + } + pub fn index_after (&self, index: usize) -> usize { + (index + 1) % self.len() + } + pub fn index_of (&self, phrase: &Phrase) -> Option { + for i in 0..self.phrases.len() { + if *self.phrases[i].read().unwrap() == *phrase { return Some(i) } + } + return None + } + fn new_phrase (name: Option<&str>, color: Option) -> Arc> { + Arc::new(RwLock::new(Phrase::new( + String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color + ))) + } + pub fn delete_selected (&mut self) { + if self.phrase > 0 { + self.phrases.remove(self.phrase); + self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1)); + } + } + pub fn append_new (&mut self, name: Option<&str>, color: Option) { + self.phrases.push(Self::new_phrase(name, color)); + self.phrase = self.phrases.len() - 1; + } + pub fn insert_new (&mut self, name: Option<&str>, color: Option) { + self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color)); + self.phrase += 1; + } + pub fn insert_dup (&mut self) { + let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate(); + phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); + self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); + self.phrase += 1; + } + pub fn randomize_color (&mut self) { + let mut phrase = self.phrases[self.phrase].write().unwrap(); + phrase.color = ItemColorTriplet::random(); + } + pub fn begin_rename (&mut self) { + self.mode = Some(PhrasePoolMode::Rename( + self.phrase, + self.phrases[self.phrase].read().unwrap().name.clone() + )); + } + pub fn begin_length (&mut self) { + self.mode = Some(PhrasePoolMode::Length( + self.phrase, + self.phrases[self.phrase].read().unwrap().length, + PhraseLengthFocus::Bar + )); + } + pub fn move_up (&mut self) { + if self.phrase > 1 { + self.phrases.swap(self.phrase - 1, self.phrase); + self.phrase -= 1; + } + } + pub fn move_down (&mut self) { + if self.phrase < self.phrases.len().saturating_sub(1) { + self.phrases.swap(self.phrase + 1, self.phrase); + self.phrase += 1; + } + } +} + +// TODO: Display phrases always in order of appearance +impl Content for PhrasePool { + type Engine = Tui; + fn content (&self) -> impl Widget { + let Self { focused, phrases, mode, .. } = self; + let content = col!( + (i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{ + let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); + let mut length = PhraseLength::new(length, None); + if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode { + if *focused && i == *phrase { + length.pulses = *new_length; + length.focus = Some(*focus); + } + } + let length = length.align_e().fill_x(); + let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x(); + let mut row2 = format!(" {name}"); + if let Some(PhrasePoolMode::Rename(phrase, _)) = mode { + if *focused && i == *phrase { row2 = format!("{row2}▄"); } + }; + let row2 = TuiStyle::bold(row2, true); + add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; + Ok(if *focused && i == self.phrase { add(&CORNERS)?; }) + }) + ); + let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; + let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); + let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border); + let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; + let upper_left = format!("[{}] Phrases", if self.entered {"■"} else {" "}); + let upper_right = format!("({})", phrases.len()); + lay!( + content, + TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), + TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), + ) + } +} + +impl Content for PhraseLength { + type Engine = Tui; + fn content (&self) -> impl Widget { + Layers::new(move|add|{ + match self.focus { + None => add(&row!( + " ", self.bars_string(), + ".", self.beats_string(), + ".", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Bar) => add(&row!( + "[", self.bars_string(), + "]", self.beats_string(), + ".", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Beat) => add(&row!( + " ", self.bars_string(), + "[", self.beats_string(), + "]", self.ticks_string(), + " " + )), + Some(PhraseLengthFocus::Tick) => add(&row!( + " ", self.bars_string(), + ".", self.beats_string(), + "[", self.ticks_string(), + "]" + )), + } + }) + } +} diff --git a/crates/tek_tui/src/tui_pool_cmd.rs b/crates/tek_tui/src/tui_pool_cmd.rs new file mode 100644 index 00000000..a85ca00f --- /dev/null +++ b/crates/tek_tui/src/tui_pool_cmd.rs @@ -0,0 +1,225 @@ +use crate::*; + +#[derive(Clone, PartialEq)] +pub enum PhrasePoolCommand { + Prev, + Next, + MoveUp, + MoveDown, + Delete, + Append, + Insert, + Duplicate, + RandomColor, + Edit, + Import, + Export, + Rename(PhraseRenameCommand), + Length(PhraseLengthCommand), +} + +#[derive(Clone, PartialEq)] +pub enum PhraseRenameCommand { + Begin, + Backspace, + Append(char), + Set(String), + Confirm, + Cancel, +} + +#[derive(Clone, PartialEq)] +pub enum PhraseLengthCommand { + Begin, + Next, + Prev, + Inc, + Dec, + Set(usize), + Confirm, + Cancel, +} + +impl Handle for PhrasePool { + fn handle (&mut self, from: &TuiInput) -> Perhaps { + if let Some(command) = PhrasePoolCommand::input_to_command(self, from) { + let _undo = command.execute(self)?; + return Ok(Some(true)) + } + Ok(None) + } +} + +impl InputToCommand> for PhrasePoolCommand { + fn input_to_command (state: &PhrasePool, input: &TuiInput) -> Option { + match input.event() { + key!(KeyCode::Up) => Some(Self::Prev), + key!(KeyCode::Down) => Some(Self::Next), + key!(KeyCode::Char(',')) => Some(Self::MoveUp), + key!(KeyCode::Char('.')) => Some(Self::MoveDown), + key!(KeyCode::Delete) => Some(Self::Delete), + key!(KeyCode::Char('a')) => Some(Self::Append), + key!(KeyCode::Char('i')) => Some(Self::Insert), + key!(KeyCode::Char('d')) => Some(Self::Duplicate), + key!(KeyCode::Char('c')) => Some(Self::RandomColor), + key!(KeyCode::Char('n')) => Some(Self::Rename(PhraseRenameCommand::Begin)), + key!(KeyCode::Char('t')) => Some(Self::Length(PhraseLengthCommand::Begin)), + _ => match state.mode { + Some(PhrasePoolMode::Rename(..)) => PhraseRenameCommand::input_to_command(state, input) + .map(Self::Rename), + Some(PhrasePoolMode::Length(..)) => PhraseLengthCommand::input_to_command(state, input) + .map(Self::Length), + _ => None + } + } + } +} + +impl InputToCommand> for PhraseRenameCommand { + fn input_to_command (_: &PhrasePool, from: &TuiInput) -> Option { + match from.event() { + key!(KeyCode::Backspace) => Some(Self::Backspace), + key!(KeyCode::Enter) => Some(Self::Confirm), + key!(KeyCode::Esc) => Some(Self::Cancel), + key!(KeyCode::Char(c)) => Some(Self::Append(*c)), + _ => None + } + } +} + +impl InputToCommand> for PhraseLengthCommand { + fn input_to_command (_: &PhrasePool, from: &TuiInput) -> Option { + match from.event() { + key!(KeyCode::Up) => Some(Self::Inc), + key!(KeyCode::Down) => Some(Self::Dec), + key!(KeyCode::Right) => Some(Self::Next), + key!(KeyCode::Left) => Some(Self::Prev), + key!(KeyCode::Enter) => Some(Self::Confirm), + key!(KeyCode::Esc) => Some(Self::Cancel), + _ => None + } + } +} + +impl Command> for PhrasePoolCommand { + fn execute (self, state: &mut PhrasePool) -> Perhaps { + use PhrasePoolCommand::*; + use PhraseRenameCommand as Rename; + use PhraseLengthCommand as Length; + match self { + Rename(Rename::Begin) => { state.begin_rename() }, + Length(Length::Begin) => { state.begin_length() }, + Prev => { state.select_prev() }, + Next => { state.select_next() }, + Delete => { state.delete_selected() }, + Append => { state.append_new(None, None) }, + Insert => { state.insert_new(None, None) }, + Duplicate => { state.insert_dup() }, + RandomColor => { state.randomize_color() }, + MoveUp => { state.move_up() }, + MoveDown => { state.move_down() }, + _ => unreachable!(), + } + Ok(None) + } +} + +impl Command> for PhraseRenameCommand { + fn translate (self, state: &PhrasePool) -> Self { + use PhraseRenameCommand::*; + if let Some(PhrasePoolMode::Rename(_, ref old_name)) = state.mode { + match self { + Backspace => { + let mut new_name = old_name.clone(); + new_name.pop(); + return Self::Set(new_name) + }, + Append(c) => { + let mut new_name = old_name.clone(); + new_name.push(c); + return Self::Set(new_name) + }, + _ => {} + } + } else if self != Begin { + unreachable!() + } + self + } + fn execute (self, state: &mut PhrasePool) -> Perhaps { + use PhraseRenameCommand::*; + if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode { + match self { + Set(s) => { + state.phrases[phrase].write().unwrap().name = s.into(); + return Ok(Some(Self::Set(old_name.clone()))) + }, + Confirm => { + let old_name = old_name.clone(); + state.mode = None; + return Ok(Some(Self::Set(old_name))) + }, + Cancel => { + let mut phrase = state.phrases[phrase].write().unwrap(); + phrase.name = old_name.clone(); + }, + _ => unreachable!() + }; + Ok(None) + } else if self == Begin { + todo!() + } else { + unreachable!() + } + } +} + +impl Command> for PhraseLengthCommand { + fn translate (self, state: &PhrasePool) -> Self { + use PhraseLengthCommand::*; + if let Some(PhrasePoolMode::Length(_, length, _)) = state.mode { + match self { + Confirm => { return Self::Set(length) }, + _ => self + } + } else if self == Begin { + todo!() + } else { + unreachable!() + } + } + fn execute (self, state: &mut PhrasePool) -> Perhaps { + use PhraseLengthFocus::*; + use PhraseLengthCommand::*; + if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode { + match self { + Cancel => { state.mode = 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 phrase = state.phrases[phrase].write().unwrap(); + let old_length = phrase.length; + phrase.length = length; + state.mode = None; + return Ok(Some(Self::Set(old_length))) + }, + _ => unreachable!() + } + Ok(None) + } else if self == Begin { + todo!() + } else { + unreachable!() + } + } +} diff --git a/crates/tek_tui/src/tui_sampler.rs b/crates/tek_tui/src/tui_sampler.rs index fd565166..6a2e379a 100644 --- a/crates/tek_tui/src/tui_sampler.rs +++ b/crates/tek_tui/src/tui_sampler.rs @@ -37,6 +37,20 @@ impl SamplerView { modal: Default::default() })) } + /// Immutable reference to sample at cursor. + pub fn sample (&self) -> Option<&Arc>> { + for (i, sample) in self.mapped.values().enumerate() { + if i == self.cursor.0 { + return Some(sample) + } + } + for (i, sample) in self.unmapped.iter().enumerate() { + if i + self.mapped.len() == self.cursor.0 { + return Some(sample) + } + } + None + } } /// A sound sample. @@ -49,7 +63,6 @@ pub struct Sample { pub rate: Option, } - /// Load sample from WAV and assign to MIDI note. #[macro_export] macro_rules! sample { ($note:expr, $name:expr, $src:expr) => {{ @@ -303,35 +316,7 @@ impl Sample { } } -/// A currently playing instance of a sample. -pub struct Voice { - pub sample: Arc>, - pub after: usize, - pub position: usize, - pub velocity: f32, -} - -impl Iterator for Voice { - type Item = [f32;2]; - fn next (&mut self) -> Option { - if self.after > 0 { - self.after = self.after - 1; - return Some([0.0, 0.0]) - } - let sample = self.sample.read().unwrap(); - if self.position < sample.end { - let position = self.position; - self.position = self.position + 1; - return sample.channels[0].get(position).map(|_amplitude|[ - sample.channels[0][position] * self.velocity, - sample.channels[0][position] * self.velocity, - ]) - } - None - } -} - -impl Widget for Sampler { +impl Widget for SamplerView { type Engine = Tui; fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> { todo!() @@ -341,7 +326,7 @@ impl Widget for Sampler { } } -pub fn tui_render_sampler (sampler: &Sampler, to: &mut TuiOutput) -> Usually<()> { +pub fn tui_render_sampler (sampler: &SamplerView, to: &mut TuiOutput) -> Usually<()> { let [x, y, _, height] = to.area(); let style = Style::default().gray(); let title = format!(" {} ({})", sampler.name, sampler.voices.read().unwrap().len()); diff --git a/crates/tek_tui/src/tui_sequencer.rs b/crates/tek_tui/src/tui_sequencer.rs index fe0a6b04..f61dd966 100644 --- a/crates/tek_tui/src/tui_sequencer.rs +++ b/crates/tek_tui/src/tui_sequencer.rs @@ -1,13 +1,8 @@ use crate::*; use std::cmp::PartialEq; -/// MIDI message structural -pub type PhraseData = Vec>; -/// MIDI message serialized -pub type PhraseMessage = Vec; -/// Collection of serialized MIDI messages -pub type PhraseChunk = [Vec]; + /// Root level object for standalone `tek_sequencer` -pub struct SequencerView { +pub struct SequencerApp { /// JACK client handle (needs to not be dropped for standalone mode to work). pub jack: Arc>, /// Controls the JACK transport. @@ -25,394 +20,8 @@ pub struct SequencerView { /// Whether the currently focused item is entered pub entered: bool, } -pub struct PhrasePoolView { - _engine: PhantomData, - state: PhrasePool, - /// Scroll offset - pub scroll: usize, - /// Mode switch - pub mode: Option, - /// Whether this widget is focused - pub focused: bool, - /// Whether this widget is entered - pub entered: bool, -} -/// Sections in the sequencer app that may be focused -#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus { - /// The transport (toolbar) is focused - Transport, - /// The phrase list (pool) is focused - PhrasePool, - /// The phrase editor (sequencer) is focused - PhraseEditor, -} -/// Status bar for sequencer app -pub enum SequencerStatusBar { - Transport, - PhrasePool, - PhraseEditor, -} -/// Modes for phrase pool -pub enum PhrasePoolMode { - /// Renaming a pattern - Rename(usize, String), - /// Editing the length of a pattern - Length(usize, usize, PhraseLengthFocus), -} -/// Contains state for viewing and editing a phrase -pub struct PhraseEditor { - _engine: PhantomData, - /// Phrase being played - pub phrase: Option>>, - /// Length of note that will be inserted, in pulses - pub note_len: usize, - /// The full piano keys are rendered to this buffer - pub keys: Buffer, - /// The full piano roll is rendered to this buffer - pub buffer: BigBuffer, - /// Cursor/scroll/zoom in pitch axis - pub note_axis: RwLock>, - /// Cursor/scroll/zoom in time axis - pub time_axis: RwLock>, - /// Whether this widget is focused - pub focused: bool, - /// Whether note enter mode is enabled - pub entered: bool, - /// Display mode - pub mode: bool, - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, - /// Current position of global playhead - pub now: Arc, - /// Width of notes area at last render - pub width: AtomicUsize, - /// Height of notes area at last render - pub height: AtomicUsize, -} -/// Phrase player. -pub struct PhrasePlayer { - /// Global timebase - pub clock: Arc, - /// Start time and phrase being played - pub phrase: Option<(Instant, Option>>)>, - /// Start time and next phrase - pub next_phrase: Option<(Instant, Option>>)>, - /// Play input through output. - pub monitoring: bool, - /// Write input to sequence. - pub recording: bool, - /// Overdub input to sequence. - pub overdub: bool, - /// Send all notes off - pub reset: bool, // TODO?: after Some(nframes) - /// Record from MIDI ports to current sequence. - pub midi_inputs: Vec>, - /// Play from current sequence to MIDI ports - pub midi_outputs: Vec>, - /// MIDI output buffer - pub midi_note: Vec, - /// MIDI output buffer - pub midi_chunk: Vec>>, - /// Notes currently held at input - pub notes_in: Arc>, - /// Notes currently held at output - pub notes_out: Arc>, -} -/// Displays and edits phrase length. -pub struct PhraseLength { - _engine: PhantomData, - /// Pulses per beat (quaver) - pub ppq: usize, - /// Beats per bar - pub bpb: usize, - /// Length of phrase in pulses - pub pulses: usize, - /// Selected subdivision - pub focus: Option, -} -/// Focused field of `PhraseLength` -#[derive(Copy, Clone)] pub enum PhraseLengthFocus { - /// Editing the number of bars - Bar, - /// Editing the number of beats - Beat, - /// Editing the number of ticks - Tick, -} -/// Focus layout of sequencer app -impl FocusGrid for Sequencer { - type Item = SequencerFocus; - fn cursor (&self) -> (usize, usize) { - self.focus_cursor - } - fn cursor_mut (&mut self) -> &mut (usize, usize) { - &mut self.focus_cursor - } - fn layout (&self) -> &[&[SequencerFocus]] { &[ - &[SequencerFocus::Transport], - &[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor], - ] } - fn focus_enter (&mut self) { - self.entered = true - } - fn focus_exit (&mut self) { - self.entered = false - } - fn entered (&self) -> Option { - if self.entered { Some(self.focused()) } else { None } - } - fn update_focus (&mut self) { - let focused = self.focused(); - if let Some(transport) = self.transport.as_ref() { - transport.write().unwrap().focused = focused == SequencerFocus::Transport - } - self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool; - self.editor.focused = focused == SequencerFocus::PhraseEditor; - } -} -impl PhrasePool { - pub fn new () -> Self { - Self { - _engine: Default::default(), - scroll: 0, - phrase: 0, - phrases: vec![Arc::new(RwLock::new(Phrase::default()))], - mode: None, - focused: false, - entered: false, - } - } - pub fn len (&self) -> usize { self.phrases.len() } - pub fn phrase (&self) -> &Arc> { &self.phrases[self.phrase] } - pub fn select_prev (&mut self) { self.phrase = self.index_before(self.phrase) } - pub fn select_next (&mut self) { self.phrase = self.index_after(self.phrase) } - pub fn index_before (&self, index: usize) -> usize { - index.overflowing_sub(1).0.min(self.len() - 1) - } - pub fn index_after (&self, index: usize) -> usize { - (index + 1) % self.len() - } - pub fn index_of (&self, phrase: &Phrase) -> Option { - for i in 0..self.phrases.len() { - if *self.phrases[i].read().unwrap() == *phrase { return Some(i) } - } - return None - } - fn new_phrase (name: Option<&str>, color: Option) -> Arc> { - Arc::new(RwLock::new(Phrase::new( - String::from(name.unwrap_or("(new)")), true, 4 * PPQ, None, color - ))) - } - pub fn delete_selected (&mut self) { - if self.phrase > 0 { - self.phrases.remove(self.phrase); - self.phrase = self.phrase.min(self.phrases.len().saturating_sub(1)); - } - } - pub fn append_new (&mut self, name: Option<&str>, color: Option) { - self.phrases.push(Self::new_phrase(name, color)); - self.phrase = self.phrases.len() - 1; - } - pub fn insert_new (&mut self, name: Option<&str>, color: Option) { - self.phrases.insert(self.phrase + 1, Self::new_phrase(name, color)); - self.phrase += 1; - } - pub fn insert_dup (&mut self) { - let mut phrase = self.phrases[self.phrase].read().unwrap().duplicate(); - phrase.color = ItemColorTriplet::random_near(phrase.color, 0.25); - self.phrases.insert(self.phrase + 1, Arc::new(RwLock::new(phrase))); - self.phrase += 1; - } - pub fn randomize_color (&mut self) { - let mut phrase = self.phrases[self.phrase].write().unwrap(); - phrase.color = ItemColorTriplet::random(); - } - pub fn begin_rename (&mut self) { - self.mode = Some(PhrasePoolMode::Rename( - self.phrase, - self.phrases[self.phrase].read().unwrap().name.clone() - )); - } - pub fn begin_length (&mut self) { - self.mode = Some(PhrasePoolMode::Length( - self.phrase, - self.phrases[self.phrase].read().unwrap().length, - PhraseLengthFocus::Bar - )); - } - pub fn move_up (&mut self) { - if self.phrase > 1 { - self.phrases.swap(self.phrase - 1, self.phrase); - self.phrase -= 1; - } - } - pub fn move_down (&mut self) { - if self.phrase < self.phrases.len().saturating_sub(1) { - self.phrases.swap(self.phrase + 1, self.phrase); - self.phrase += 1; - } - } -} -impl PhraseEditor { - pub fn new () -> Self { - Self { - _engine: Default::default(), - phrase: None, - note_len: 24, - notes_in: Arc::new(RwLock::new([false;128])), - notes_out: Arc::new(RwLock::new([false;128])), - keys: keys_vert(), - buffer: Default::default(), - focused: false, - entered: false, - mode: false, - now: Arc::new(0.into()), - width: 0.into(), - height: 0.into(), - note_axis: RwLock::new(FixedAxis { - start: 12, - point: Some(36), - clamp: Some(127) - }), - time_axis: RwLock::new(ScaledAxis { - start: 00, - point: Some(00), - clamp: Some(000), - scale: 24 - }), - } - } - pub fn note_cursor_inc (&self) { - let mut axis = self.note_axis.write().unwrap(); - axis.point_dec(1); - if let Some(point) = axis.point { if point < axis.start { axis.start = (point / 2) * 2; } } - } - pub fn note_cursor_dec (&self) { - let mut axis = self.note_axis.write().unwrap(); - axis.point_inc(1); - if let Some(point) = axis.point { if point > 73 { axis.point = Some(73); } } - } - pub fn note_page_up (&self) { - let mut axis = self.note_axis.write().unwrap(); - axis.start_dec(3); - axis.point_dec(3); - } - pub fn note_page_down (&self) { - let mut axis = self.note_axis.write().unwrap(); - axis.start_inc(3); - axis.point_inc(3); - } - pub fn note_scroll_inc (&self) { self.note_axis.write().unwrap().start_dec(1); } - pub fn note_scroll_dec (&self) { self.note_axis.write().unwrap().start_inc(1); } - pub fn note_length_inc (&mut self) { self.note_len = next_note_length(self.note_len) } - pub fn note_length_dec (&mut self) { self.note_len = prev_note_length(self.note_len) } - pub fn time_cursor_advance (&self) { - let point = self.time_axis.read().unwrap().point; - let length = self.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1); - let forward = |time|(time + self.note_len) % length; - self.time_axis.write().unwrap().point = point.map(forward); - } - pub fn time_cursor_inc (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().point_inc(scale); - } - pub fn time_cursor_dec (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().point_dec(scale); - } - pub fn time_scroll_inc (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().start_inc(scale); - } - pub fn time_scroll_dec (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().start_dec(scale); - } - pub fn time_zoom_in (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().scale = prev_note_length(scale) - } - pub fn time_zoom_out (&self) { - let scale = self.time_axis.read().unwrap().scale; - self.time_axis.write().unwrap().scale = next_note_length(scale) - } -} -impl PhrasePlayer { - pub fn new ( - jack: &Arc>, - clock: &Arc, - name: &str - ) -> Usually { - let jack = jack.read().unwrap(); - Ok(Self { - clock: clock.clone(), - phrase: None, - next_phrase: None, - notes_in: Arc::new(RwLock::new([false;128])), - notes_out: Arc::new(RwLock::new([false;128])), - monitoring: false, - recording: false, - overdub: true, - reset: true, - midi_note: Vec::with_capacity(8), - midi_chunk: vec![Vec::with_capacity(16);16384], - midi_outputs: vec![ - jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())? - ], - midi_inputs: vec![ - jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())? - ], - }) - } - pub fn toggle_monitor (&mut self) { self.monitoring = !self.monitoring; } - pub fn toggle_record (&mut self) { self.recording = !self.recording; } - pub fn toggle_overdub (&mut self) { self.overdub = !self.overdub; } - pub fn enqueue_next (&mut self, phrase: Option<&Arc>>) { - let start = self.clock.next_launch_pulse(); - self.next_phrase = Some(( - Instant::from_pulse(&self.clock.timebase(), start as f64), - phrase.map(|p|p.clone()) - )); - self.reset = true; - } - pub fn pulses_since_start (&self) -> Option { - if let Some((started, Some(_))) = self.phrase.as_ref() { - Some(self.clock.current.pulse.get() - started.pulse.get()) - } else { - None - } - } -} -impl PhraseLength { - pub fn new (pulses: usize, focus: Option) -> Self { - Self { _engine: Default::default(), 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) -> String { format!("{}", self.bars()) } - pub fn beats_string (&self) -> String { format!("{}", self.beats()) } - pub fn ticks_string (&self) -> String { format!("{:>02}", self.ticks()) } -} -impl PhraseLengthFocus { - 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, - } - } -} -impl Content for Sequencer { + +impl Content for SequencerApp { type Engine = Tui; fn content (&self) -> impl Widget { Stack::down(move|add|{ @@ -423,452 +32,14 @@ impl Content for Sequencer { }) } } -impl Handle for Sequencer { - fn handle (&mut self, i: &TuiInput) -> Perhaps { - if let Some(entered) = self.entered() { - use SequencerFocus::*; - if let Some(true) = match entered { - Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(), - PhrasePool => self.phrases.write().unwrap().handle(i)?, - PhraseEditor => self.editor.handle(i)?, - } { - return Ok(Some(true)) - } + +/// JACK process callback for sequencer app +impl Audio for SequencerApp { + fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control { + if let Some(ref transport) = self.transport { + transport.write().unwrap().process(client, scope); } - if let Some(command) = SequencerCommand::input_to_command(self, i) { - let _undo = command.execute(self)?; - return Ok(Some(true)) - } - Ok(None) + self.player.process(client, scope); + Control::Continue } } -impl InputToCommand> for SequencerCommand { - fn input_to_command (state: &Sequencer, input: &TuiInput) -> Option { - use SequencerCommand::*; - use FocusCommand::*; - match input.event() { - key!(KeyCode::Tab) => Some(Focus(Next)), - key!(Shift-KeyCode::Tab) => Some(Focus(Prev)), - key!(KeyCode::BackTab) => Some(Focus(Prev)), - key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)), - key!(KeyCode::Up) => Some(Focus(Up)), - key!(KeyCode::Down) => Some(Focus(Down)), - key!(KeyCode::Left) => Some(Focus(Left)), - key!(KeyCode::Right) => Some(Focus(Right)), - key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)), - _ => match state.focused() { - SequencerFocus::Transport => if let Some(t) = state.transport.as_ref() { - TransportCommand::input_to_command(&*t.read().unwrap(), input).map(Transport) - } else { - None - }, - SequencerFocus::PhrasePool => - PhrasePoolCommand::input_to_command(&*state.phrases.read().unwrap(), input) - .map(Phrases), - SequencerFocus::PhraseEditor => - PhraseEditorCommand::input_to_command(&state.editor, input) - .map(Editor), - } - } - } -} -// TODO: Display phrases always in order of appearance -impl Content for PhrasePool { - type Engine = Tui; - fn content (&self) -> impl Widget { - let Self { focused, phrases, mode, .. } = self; - let content = col!( - (i, phrase) in phrases.iter().enumerate() => Layers::new(|add|{ - let Phrase { ref name, color, length, .. } = *phrase.read().unwrap(); - let mut length = PhraseLength::new(length, None); - if let Some(PhrasePoolMode::Length(phrase, new_length, focus)) = mode { - if *focused && i == *phrase { - length.pulses = *new_length; - length.focus = Some(*focus); - } - } - let length = length.align_e().fill_x(); - let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x(); - let mut row2 = format!(" {name}"); - if let Some(PhrasePoolMode::Rename(phrase, _)) = mode { - if *focused && i == *phrase { row2 = format!("{row2}▄"); } - }; - let row2 = TuiStyle::bold(row2, true); - add(&col!(row1, row2).fill_x().bg(color.base.rgb))?; - Ok(if *focused && i == self.phrase { add(&CORNERS)?; }) - }) - ); - let border_color = if *focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)}; - let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); - let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border); - let title_color = if *focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)}; - let upper_left = format!("[{}] Phrases", if self.entered {"■"} else {" "}); - let upper_right = format!("({})", phrases.len()); - lay!( - content, - TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), - TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), - ) - } -} -impl Handle for PhrasePool { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - if let Some(command) = PhrasePoolCommand::input_to_command(self, from) { - let _undo = command.execute(self)?; - return Ok(Some(true)) - } - Ok(None) - } -} -impl InputToCommand> for PhrasePoolCommand { - fn input_to_command (state: &PhrasePool, input: &TuiInput) -> Option { - match input.event() { - key!(KeyCode::Up) => Some(Self::Prev), - key!(KeyCode::Down) => Some(Self::Next), - key!(KeyCode::Char(',')) => Some(Self::MoveUp), - key!(KeyCode::Char('.')) => Some(Self::MoveDown), - key!(KeyCode::Delete) => Some(Self::Delete), - key!(KeyCode::Char('a')) => Some(Self::Append), - key!(KeyCode::Char('i')) => Some(Self::Insert), - key!(KeyCode::Char('d')) => Some(Self::Duplicate), - key!(KeyCode::Char('c')) => Some(Self::RandomColor), - key!(KeyCode::Char('n')) => Some(Self::Rename(PhraseRenameCommand::Begin)), - key!(KeyCode::Char('t')) => Some(Self::Length(PhraseLengthCommand::Begin)), - _ => match state.mode { - Some(PhrasePoolMode::Rename(..)) => PhraseRenameCommand::input_to_command(state, input) - .map(Self::Rename), - Some(PhrasePoolMode::Length(..)) => PhraseLengthCommand::input_to_command(state, input) - .map(Self::Length), - _ => None - } - } - } -} -impl InputToCommand> for PhraseRenameCommand { - fn input_to_command (_: &PhrasePool, from: &TuiInput) -> Option { - match from.event() { - key!(KeyCode::Backspace) => Some(Self::Backspace), - key!(KeyCode::Enter) => Some(Self::Confirm), - key!(KeyCode::Esc) => Some(Self::Cancel), - key!(KeyCode::Char(c)) => Some(Self::Append(*c)), - _ => None - } - } -} -impl InputToCommand> for PhraseLengthCommand { - fn input_to_command (_: &PhrasePool, from: &TuiInput) -> Option { - match from.event() { - key!(KeyCode::Up) => Some(Self::Inc), - key!(KeyCode::Down) => Some(Self::Dec), - key!(KeyCode::Right) => Some(Self::Next), - key!(KeyCode::Left) => Some(Self::Prev), - key!(KeyCode::Enter) => Some(Self::Confirm), - key!(KeyCode::Esc) => Some(Self::Cancel), - _ => None - } - } -} -impl Content for PhraseLength { - type Engine = Tui; - fn content (&self) -> impl Widget { - Layers::new(move|add|{ - match self.focus { - None => add(&row!( - " ", self.bars_string(), - ".", self.beats_string(), - ".", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Bar) => add(&row!( - "[", self.bars_string(), - "]", self.beats_string(), - ".", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Beat) => add(&row!( - " ", self.bars_string(), - "[", self.beats_string(), - "]", self.ticks_string(), - " " - )), - Some(PhraseLengthFocus::Tick) => add(&row!( - " ", self.bars_string(), - ".", self.beats_string(), - "[", self.ticks_string(), - "]" - )), - } - }) - } -} -impl Content for PhraseEditor { - type Engine = Tui; - fn content (&self) -> impl Widget { - let Self { focused, entered, keys, phrase, buffer, note_len, .. } = self; - let FixedAxis { - start: note_start, point: note_point, clamp: note_clamp - } = *self.note_axis.read().unwrap(); - let ScaledAxis { - start: time_start, point: time_point, clamp: time_clamp, scale: time_scale - } = *self.time_axis.read().unwrap(); - //let color = Color::Rgb(0,255,0); - //let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color); - let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{ - Ok(if to.area().h() >= 2 { - to.buffer_update(to.area().set_w(5), &|cell, x, y|{ - let y = y + (note_start / 2) as u16; - if x < keys.area.width && y < keys.area.height { - *cell = keys.get(x, y).clone() - } - }); - }) - }).fill_y(); - let notes_bg_null = Color::Rgb(28, 35, 25); - let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ - let area = to.area(); - let h = area.h() as usize; - self.height.store(h, Ordering::Relaxed); - self.width.store(area.w() as usize, Ordering::Relaxed); - let mut axis = self.note_axis.write().unwrap(); - if let Some(point) = axis.point { - if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) { - axis.start += 2; - } - } - Ok(if to.area().h() >= 2 { - let area = to.area(); - to.buffer_update(area, &move |cell, x, y|{ - cell.set_bg(notes_bg_null); - let src_x = (x as usize + time_start) * time_scale; - let src_y = y as usize + note_start / 2; - if src_x < buffer.width && src_y < buffer.height - 1 { - buffer.get(src_x, buffer.height - src_y - 2).map(|src|{ - cell.set_symbol(src.symbol()); - cell.set_fg(src.fg); - cell.set_bg(src.bg); - }); - } - }); - }) - }).fill_x(); - let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{ - Ok(if *focused && *entered { - let area = to.area(); - if let (Some(time), Some(note)) = (time_point, note_point) { - let x1 = area.x() + (time / time_scale) as u16; - let x2 = x1 + (self.note_len / time_scale) as u16; - let y = area.y() + note.saturating_sub(note_start) as u16 / 2; - let c = if note % 2 == 0 { "▀" } else { "▄" }; - for x in x1..x2 { - to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0)))); - } - } - }) - }); - let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30)); - let playhead_active = playhead_inactive.clone().yellow().bold().not_dim(); - let playhead = CustomWidget::new( - |to:[u16;2]|Ok(Some(to.clip_h(1))), - move|to: &mut TuiOutput|{ - if let Some(_) = phrase { - let now = self.now.get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length; - let time_clamp = time_clamp - .expect("time_axis of sequencer expected to be clamped"); - for x in 0..(time_clamp/time_scale).saturating_sub(time_start) { - let this_step = time_start + (x + 0) * time_scale; - let next_step = time_start + (x + 1) * time_scale; - let x = to.area().x() + x as u16; - let active = this_step <= now && now < next_step; - let character = if active { "|" } else { "·" }; - let style = if active { playhead_active } else { playhead_inactive }; - to.blit(&character, x, to.area.y(), Some(style)); - } - } - Ok(()) - } - ).push_x(6).align_sw(); - let border_color = if *focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)}; - let title_color = if *focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)}; - let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color)); - let note_area = lay!(notes, cursor).fill_x(); - let piano_roll = row!(keys, note_area).fill_x(); - let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border); - let content = lay!(content, playhead); - let mut upper_left = format!("[{}] Sequencer", if *entered {"■"} else {" "}); - if let Some(phrase) = phrase { - upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name); - } - let mut lower_right = format!( - "┤{}x{}├", - self.width.load(Ordering::Relaxed), - self.height.load(Ordering::Relaxed), - ); - lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale)); - //lower_right = format!("Zoom: {} (+{}:{}*{}|{})", - //pulses_to_name(time_scale), - //time_start, time_point.unwrap_or(0), - //time_scale, time_clamp.unwrap_or(0), - //); - if *focused && *entered { - lower_right = format!("┤Note: {} {}├─{lower_right}", - self.note_axis.read().unwrap().point.unwrap(), - pulses_to_name(*note_len)); - //lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}", - //pulses_to_name(*note_len), - //note_start, - //note_point.unwrap_or(0), - //note_clamp.unwrap_or(0), - //); - } - let upper_right = if let Some(phrase) = phrase { - format!("┤Length: {}├", phrase.read().unwrap().length) - } else { - String::new() - }; - lay!( - content, - TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(), - TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(), - TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(), - ) - } -} -impl PhraseEditor { - pub fn put (&mut self) { - if let (Some(phrase), Some(time), Some(note)) = ( - &self.phrase, - self.time_axis.read().unwrap().point, - self.note_axis.read().unwrap().point, - ) { - let mut phrase = phrase.write().unwrap(); - let key: u7 = u7::from((127 - note) as u8); - let vel: u7 = 100.into(); - let start = time; - let end = (start + self.note_len) % phrase.length; - phrase.notes[time].push(MidiMessage::NoteOn { key, vel }); - phrase.notes[end].push(MidiMessage::NoteOff { key, vel }); - self.buffer = Self::redraw(&phrase); - } - } - /// Select which pattern to display. This pre-renders it to the buffer at full resolution. - pub fn show (&mut self, phrase: Option<&Arc>>) { - if let Some(phrase) = phrase { - self.phrase = Some(phrase.clone()); - self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length); - self.buffer = Self::redraw(&*phrase.read().unwrap()); - } else { - self.phrase = None; - self.time_axis.write().unwrap().clamp = Some(0); - self.buffer = Default::default(); - } - } - fn redraw (phrase: &Phrase) -> BigBuffer { - let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65); - Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq); - Self::fill_seq_fg(&mut buf, &phrase); - buf - } - fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) { - for x in 0..buf.width { - // Only fill as far as phrase length - if x as usize >= length { break } - // Fill each row with background characters - for y in 0 .. buf.height { - buf.get_mut(x, y).map(|cell|{ - cell.set_char(if ppq == 0 { - '·' - } else if x % (4 * ppq) == 0 { - '│' - } else if x % ppq == 0 { - '╎' - } else { - '·' - }); - cell.set_fg(Color::Rgb(48, 64, 56)); - cell.modifier = Modifier::DIM; - }); - } - } - } - fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) { - let mut notes_on = [false;128]; - for x in 0..buf.width { - if x as usize >= phrase.length { - break - } - if let Some(notes) = phrase.notes.get(x as usize) { - if phrase.percussive { - for note in notes { - match note { - MidiMessage::NoteOn { key, .. } => - notes_on[key.as_int() as usize] = true, - _ => {} - } - } - } else { - for note in notes { - match note { - MidiMessage::NoteOn { key, .. } => - notes_on[key.as_int() as usize] = true, - MidiMessage::NoteOff { key, .. } => - notes_on[key.as_int() as usize] = false, - _ => {} - } - } - } - for y in 0..buf.height { - if y >= 64 { - break - } - if let Some(block) = half_block( - notes_on[y as usize * 2], - notes_on[y as usize * 2 + 1], - ) { - buf.get_mut(x, y).map(|cell|{ - cell.set_char(block); - cell.set_fg(Color::White); - }); - } - } - if phrase.percussive { - notes_on.fill(false); - } - } - } - } -} -/// Colors of piano keys -const KEY_COLORS: [(Color, Color);6] = [ - (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)), - (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), - (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)), -]; -pub(crate) fn keys_vert () -> Buffer { - let area = [0, 0, 5, 64]; - let mut buffer = Buffer::empty(Rect { - x: area.x(), y: area.y(), width: area.w(), height: area.h() - }); - buffer_update(&mut buffer, area, &|cell, x, y| { - let y = 63 - y; - match x { - 0 => { - cell.set_char('▀'); - let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize]; - cell.set_fg(fg); - cell.set_bg(bg); - }, - 1 => { - cell.set_char('▀'); - cell.set_fg(Color::White); - cell.set_bg(Color::White); - }, - 2 => if y % 6 == 0 { cell.set_char('C'); }, - 3 => if y % 6 == 0 { cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]); }, - _ => {} - } - }); - buffer -} diff --git a/crates/tek_tui/src/tui_sequencer_bar.rs b/crates/tek_tui/src/tui_sequencer_bar.rs new file mode 100644 index 00000000..9efe09e6 --- /dev/null +++ b/crates/tek_tui/src/tui_sequencer_bar.rs @@ -0,0 +1,8 @@ +use crate::*; + +/// Status bar for sequencer app +pub enum SequencerStatusBar { + Transport, + PhrasePool, + PhraseEditor, +} diff --git a/crates/tek_tui/src/tui_sequencer_cmd.rs b/crates/tek_tui/src/tui_sequencer_cmd.rs index c797cbce..b7718fb3 100644 --- a/crates/tek_tui/src/tui_sequencer_cmd.rs +++ b/crates/tek_tui/src/tui_sequencer_cmd.rs @@ -1,78 +1,35 @@ use crate::*; #[derive(Clone, PartialEq)] -pub enum SequencerCommand { +pub enum SequencerAppCommand { Focus(FocusCommand), Transport(TransportCommand), Phrases(PhrasePoolCommand), Editor(PhraseEditorCommand), } -#[derive(Clone, PartialEq)] -pub enum PhrasePoolCommand { - Prev, - Next, - MoveUp, - MoveDown, - Delete, - Append, - Insert, - Duplicate, - RandomColor, - Edit, - Import, - Export, - Rename(PhraseRenameCommand), - Length(PhraseLengthCommand), + +impl Handle for SequencerApp { + fn handle (&mut self, i: &TuiInput) -> Perhaps { + if let Some(entered) = self.entered() { + use SequencerFocus::*; + if let Some(true) = match entered { + Transport => self.transport.as_mut().map(|t|t.handle(i)).transpose()?.flatten(), + PhrasePool => self.phrases.write().unwrap().handle(i)?, + PhraseEditor => self.editor.handle(i)?, + } { + return Ok(Some(true)) + } + } + if let Some(command) = SequencerCommand::input_to_command(self, i) { + let _undo = command.execute(self)?; + return Ok(Some(true)) + } + Ok(None) + } } -#[derive(Clone, PartialEq)] -pub enum PhraseRenameCommand { - Begin, - Backspace, - Append(char), - Set(String), - Confirm, - Cancel, -} -#[derive(Clone, PartialEq)] -pub enum PhraseLengthCommand { - Begin, - Next, - Prev, - Inc, - Dec, - Set(usize), - Confirm, - Cancel, -} -#[derive(Clone, PartialEq)] -pub enum PhraseEditorCommand { - // TODO: 1-9 seek markers that by default start every 8th of the phrase - ToggleDirection, - EnterEditMode, - ExitEditMode, - NoteAppend, - NoteCursorDec, - NoteCursorInc, - NoteLengthDec, - NoteLengthInc, - NotePageDown, - NotePageUp, - NoteScrollDec, - NoteScrollInc, - NoteSet, - TimeCursorDec, - TimeCursorInc, - TimeScrollDec, - TimeScrollInc, - TimeZoomIn, - TimeZoomOut, - GoUp, - GoDown, - GoLeft, - GoRight, -} -impl Command> for SequencerCommand { - fn execute (self, state: &mut Sequencer) -> Perhaps { + +impl Command> for SequencerAppCommand { + fn execute (self, state: &mut SequencerApp) -> Perhaps { match self { Self::Focus(cmd) => { return delegate(cmd, Self::Focus, state) @@ -90,197 +47,34 @@ impl Command> for SequencerCommand { Ok(None) } } -impl Command> for PhrasePoolCommand { - fn execute (self, state: &mut PhrasePool) -> Perhaps { - use PhrasePoolCommand::*; - use PhraseRenameCommand as Rename; - use PhraseLengthCommand as Length; - match self { - Rename(Rename::Begin) => { state.begin_rename() }, - Length(Length::Begin) => { state.begin_length() }, - Prev => { state.select_prev() }, - Next => { state.select_next() }, - Delete => { state.delete_selected() }, - Append => { state.append_new(None, None) }, - Insert => { state.insert_new(None, None) }, - Duplicate => { state.insert_dup() }, - RandomColor => { state.randomize_color() }, - MoveUp => { state.move_up() }, - MoveDown => { state.move_down() }, - _ => unreachable!(), - } - Ok(None) - } -} -impl Command> for PhraseRenameCommand { - fn translate (self, state: &PhrasePool) -> Self { - use PhraseRenameCommand::*; - if let Some(PhrasePoolMode::Rename(_, ref old_name)) = state.mode { - match self { - Backspace => { - let mut new_name = old_name.clone(); - new_name.pop(); - return Self::Set(new_name) + +impl InputToCommand> for SequencerCommand { + fn input_to_command (state: &SequencerApp, input: &TuiInput) -> Option { + use SequencerCommand::*; + use FocusCommand::*; + match input.event() { + key!(KeyCode::Tab) => Some(Focus(Next)), + key!(Shift-KeyCode::Tab) => Some(Focus(Prev)), + key!(KeyCode::BackTab) => Some(Focus(Prev)), + key!(Shift-KeyCode::BackTab) => Some(Focus(Prev)), + key!(KeyCode::Up) => Some(Focus(Up)), + key!(KeyCode::Down) => Some(Focus(Down)), + key!(KeyCode::Left) => Some(Focus(Left)), + key!(KeyCode::Right) => Some(Focus(Right)), + key!(KeyCode::Char(' ')) => Some(Transport(TransportCommand::PlayToggle)), + _ => match state.focused() { + SequencerFocus::Transport => if let Some(t) = state.transport.as_ref() { + TransportCommand::input_to_command(&*t.read().unwrap(), input).map(Transport) + } else { + None }, - Append(c) => { - let mut new_name = old_name.clone(); - new_name.push(c); - return Self::Set(new_name) - }, - _ => {} + SequencerFocus::PhrasePool => + PhrasePoolCommand::input_to_command(&*state.phrases.read().unwrap(), input) + .map(Phrases), + SequencerFocus::PhraseEditor => + PhraseEditorCommand::input_to_command(&state.editor, input) + .map(Editor), } - } else if self != Begin { - unreachable!() - } - self - } - fn execute (self, state: &mut PhrasePool) -> Perhaps { - use PhraseRenameCommand::*; - if let Some(PhrasePoolMode::Rename(phrase, ref mut old_name)) = state.mode { - match self { - Set(s) => { - state.phrases[phrase].write().unwrap().name = s.into(); - return Ok(Some(Self::Set(old_name.clone()))) - }, - Confirm => { - let old_name = old_name.clone(); - state.mode = None; - return Ok(Some(Self::Set(old_name))) - }, - Cancel => { - let mut phrase = state.phrases[phrase].write().unwrap(); - phrase.name = old_name.clone(); - }, - _ => unreachable!() - }; - Ok(None) - } else if self == Begin { - todo!() - } else { - unreachable!() - } - } -} -impl Command> for PhraseLengthCommand { - fn translate (self, state: &PhrasePool) -> Self { - use PhraseLengthCommand::*; - if let Some(PhrasePoolMode::Length(_, length, _)) = state.mode { - match self { - Confirm => { return Self::Set(length) }, - _ => self - } - } else if self == Begin { - todo!() - } else { - unreachable!() - } - } - fn execute (self, state: &mut PhrasePool) -> Perhaps { - use PhraseLengthFocus::*; - use PhraseLengthCommand::*; - if let Some(PhrasePoolMode::Length(phrase, ref mut length, ref mut focus)) = state.mode { - match self { - Cancel => { state.mode = 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 phrase = state.phrases[phrase].write().unwrap(); - let old_length = phrase.length; - phrase.length = length; - state.mode = None; - return Ok(Some(Self::Set(old_length))) - }, - _ => unreachable!() - } - Ok(None) - } else if self == Begin { - todo!() - } else { - unreachable!() - } - } -} -impl Command> for PhraseEditorCommand { - fn translate (self, state: &PhraseEditor) -> Self { - use PhraseEditorCommand::*; - match self { - GoUp => match state.entered { true => NoteCursorInc, false => NoteScrollInc, }, - GoDown => match state.entered { true => NoteCursorDec, false => NoteScrollDec, }, - GoLeft => match state.entered { true => TimeCursorDec, false => TimeScrollDec, }, - GoRight => match state.entered { true => TimeCursorInc, false => TimeScrollInc, }, - _ => self - } - } - fn execute (self, state: &mut PhraseEditor) -> Perhaps { - use PhraseEditorCommand::*; - match self.translate(state) { - ToggleDirection => { state.mode = !state.mode; }, - EnterEditMode => { state.entered = true; }, - ExitEditMode => { state.entered = false; }, - TimeZoomOut => { state.time_zoom_out() }, - TimeZoomIn => { state.time_zoom_in() }, - TimeCursorDec => { state.time_cursor_dec() }, - TimeCursorInc => { state.time_cursor_inc() }, - TimeScrollDec => { state.time_scroll_dec() }, - TimeScrollInc => { state.time_scroll_inc() }, - NoteCursorDec => { state.note_cursor_dec() }, - NoteCursorInc => { state.note_cursor_inc() }, - NoteScrollDec => { state.note_scroll_dec() }, - NoteScrollInc => { state.note_scroll_inc() }, - NoteLengthDec => { state.note_length_dec() }, - NoteLengthInc => { state.note_length_inc() }, - NotePageUp => { state.note_page_up() }, - NotePageDown => { state.note_page_down() }, - NoteAppend => { - if state.entered { - state.put(); - state.time_cursor_advance(); - } - }, - NoteSet => { - if state.entered { state.put(); } - }, - _ => unreachable!() - } - Ok(None) - } -} -impl Handle for PhraseEditor { - fn handle (&mut self, from: &TuiInput) -> Perhaps { - PhraseEditorCommand::execute_with_state(self, from) - } -} -impl InputToCommand> for PhraseEditorCommand { - fn input_to_command (_: &PhraseEditor, from: &TuiInput) -> Option { - match from.event() { - key!(KeyCode::Char('`')) => Some(Self::ToggleDirection), - key!(KeyCode::Enter) => Some(Self::EnterEditMode), - key!(KeyCode::Esc) => Some(Self::ExitEditMode), - key!(KeyCode::Char('[')) => Some(Self::NoteLengthDec), - key!(KeyCode::Char(']')) => Some(Self::NoteLengthInc), - key!(KeyCode::Char('a')) => Some(Self::NoteAppend), - key!(KeyCode::Char('s')) => Some(Self::NoteSet), - key!(KeyCode::Char('-')) => Some(Self::TimeZoomOut), - key!(KeyCode::Char('_')) => Some(Self::TimeZoomOut), - key!(KeyCode::Char('=')) => Some(Self::TimeZoomIn), - key!(KeyCode::Char('+')) => Some(Self::TimeZoomIn), - key!(KeyCode::PageUp) => Some(Self::NotePageUp), - key!(KeyCode::PageDown) => Some(Self::NotePageDown), - key!(KeyCode::Up) => Some(Self::GoUp), - key!(KeyCode::Down) => Some(Self::GoDown), - key!(KeyCode::Left) => Some(Self::GoLeft), - key!(KeyCode::Right) => Some(Self::GoRight), - _ => None } } } diff --git a/crates/tek_tui/src/tui_sequencer_foc.rs b/crates/tek_tui/src/tui_sequencer_foc.rs new file mode 100644 index 00000000..bf13b381 --- /dev/null +++ b/crates/tek_tui/src/tui_sequencer_foc.rs @@ -0,0 +1,43 @@ +use crate::*; + +/// Sections in the sequencer app that may be focused +#[derive(Copy, Clone, PartialEq, Eq)] pub enum SequencerFocus { + /// The transport (toolbar) is focused + Transport, + /// The phrase list (pool) is focused + PhrasePool, + /// The phrase editor (sequencer) is focused + PhraseEditor, +} + +/// Focus layout of sequencer app +impl FocusGrid for Sequencer { + type Item = SequencerFocus; + fn cursor (&self) -> (usize, usize) { + self.focus_cursor + } + fn cursor_mut (&mut self) -> &mut (usize, usize) { + &mut self.focus_cursor + } + fn layout (&self) -> &[&[SequencerFocus]] { &[ + &[SequencerFocus::Transport], + &[SequencerFocus::PhrasePool, SequencerFocus::PhraseEditor], + ] } + fn focus_enter (&mut self) { + self.entered = true + } + fn focus_exit (&mut self) { + self.entered = false + } + fn entered (&self) -> Option { + if self.entered { Some(self.focused()) } else { None } + } + fn update_focus (&mut self) { + let focused = self.focused(); + if let Some(transport) = self.transport.as_ref() { + transport.write().unwrap().focused = focused == SequencerFocus::Transport + } + self.phrases.write().unwrap().focused = focused == SequencerFocus::PhrasePool; + self.editor.focused = focused == SequencerFocus::PhraseEditor; + } +}