diff --git a/src/control.rs b/src/control.rs index ee10b8e9..72b1357e 100644 --- a/src/control.rs +++ b/src/control.rs @@ -72,7 +72,7 @@ const KEYMAP: &'static [KeyBinding] = keymap!(App { }], [Char(' '), NONE, "play_toggle", "play or pause", |app: &mut App| { - app.toggle_play()?; + app.transport.toggle_play()?; Ok(true) }], [Char('r'), NONE, "record_toggle", "toggle recording", |app: &mut App| { @@ -89,11 +89,11 @@ const KEYMAP: &'static [KeyBinding] = keymap!(App { }], [Char('+'), NONE, "quant_inc", "Quantize coarser", |app: &mut App| { - app.quant = next_note_length(app.quant); + app.transport.quant = next_note_length(app.transport.quant); Ok(true) }], [Char('_'), NONE, "quant_dec", "Quantize finer", |app: &mut App| { - app.quant = prev_note_length(app.quant); + app.transport.quant = prev_note_length(app.transport.quant); Ok(true) }], diff --git a/src/control/mixer.rs b/src/control/mixer.rs index 5029e4a6..38909668 100644 --- a/src/control/mixer.rs +++ b/src/control/mixer.rs @@ -1,5 +1,15 @@ use crate::{core::*, model::*}; +// TODO: +// - Meters: propagate clipping: +// - If one stage clips, all stages after it are marked red +// - If one track clips, all tracks that feed from it are marked red? + +//pub const ACTIONS: [(&'static str, &'static str);2] = [ + //("+/-", "Adjust"), + //("Ins/Del", "Add/remove track"), +//]; + pub fn handle (state: &mut Mixer, event: &AppEvent) -> Usually { if let AppEvent::Input(crossterm::event::Event::Key(event)) = event { @@ -47,13 +57,3 @@ pub fn handle (state: &mut Mixer, event: &AppEvent) -> Usually { } Ok(false) } - -// TODO: -// - Meters: propagate clipping: -// - If one stage clips, all stages after it are marked red -// - If one track clips, all tracks that feed from it are marked red? - -pub const ACTIONS: [(&'static str, &'static str);2] = [ - ("+/-", "Adjust"), - ("Ins/Del", "Add/remove track"), -]; diff --git a/src/core.rs b/src/core.rs index c96985b4..00486201 100644 --- a/src/core.rs +++ b/src/core.rs @@ -77,10 +77,6 @@ pub fn run (state: Arc>) -> Usually>> terminal_teardown()?; Ok(state) } -pub trait Run: Render + Handle + Send + Sync + Sized + 'static { - fn run (self) -> Usually>> { run(Arc::new(RwLock::new(self))) } -} -impl Run for T {} /// Set up panic hook pub fn panic_hook_setup () { diff --git a/src/edn.rs b/src/edn.rs index 52c43e6b..6b5265d5 100644 --- a/src/edn.rs +++ b/src/edn.rs @@ -21,7 +21,7 @@ impl App { } pub fn load_edn (&mut self, mut src: &str) -> Usually<&mut Self> { loop { - match clojure_reader::edn::read(src) { + match read(src) { Ok((edn, rest)) => { self.load_edn_one(edn)?; if rest.len() > 0 { @@ -46,8 +46,10 @@ impl App { match items.get(0) { Some(Edn::Symbol("bpm")) => { match items.get(1) { - Some(Edn::Int(b)) => self.timebase.set_bpm(*b as f64), - Some(Edn::Double(b)) => self.timebase.set_bpm(f64::from(*b)), + Some(Edn::Int(b)) => + self.transport.timebase.set_bpm(*b as f64), + Some(Edn::Double(b)) => + self.transport.timebase.set_bpm(f64::from(*b)), _ => panic!("unspecified bpm") } }, @@ -99,7 +101,7 @@ impl Scene { impl Track { fn load_edn <'a, 'e> (app: &'a mut App, args: &[Edn<'e>]) -> Usually<&'a mut Self> { - let ppq = app.timebase.ppq() as usize; + let ppq = app.transport.ppq(); let mut name = app.new_track_name(); let mut _gain = 0.0f64; let mut devices: Vec = vec![]; diff --git a/src/model.rs b/src/model.rs index b5466570..ead0273a 100644 --- a/src/model.rs +++ b/src/model.rs @@ -5,6 +5,7 @@ pub mod plugin; pub mod sampler; pub mod scene; pub mod track; +pub mod transport; pub use self::phrase::{Phrase, PhraseData}; pub use self::scene::Scene; @@ -12,12 +13,11 @@ pub use self::track::Track; pub use self::sampler::{Sampler, Sample, read_sample_data}; pub use self::mixer::Mixer; pub use self::plugin::{Plugin, PluginKind, lv2::LV2Plugin}; +pub use self::transport::TransportToolbar; use crate::{core::*, view::*}; pub struct App { - /// Paths to user directories - pub xdg: Option>, /// Main JACK client. pub jack: Option, /// Map of external MIDI outs in the jack graph @@ -25,18 +25,6 @@ pub struct App { pub midi_in: Option>>, /// Names of ports to connect to main MIDI IN. pub midi_ins: Vec, - /// Main audio outputs. - pub audio_outs: Vec>>, - /// JACK transport handle. - pub transport: Option, - /// Current transport state - pub playing: Option, - /// Current position according to transport - pub playhead: usize, - /// Position of T0 for this playback within global timeline - pub play_started: Option<(usize, usize)>, - /// Current sample rate and tempo. - pub timebase: Arc, /// Display mode of arranger section pub arranger_mode: bool, /// Display mode of chain section @@ -49,10 +37,8 @@ pub struct App { pub modal: Option>, /// Currently focused section pub section: AppSection, - /// Whether the section is focused + /// Whether the current focus section has input priority pub entered: bool, - /// Current frame - pub metronome: bool, /// Display position of cursor within note range pub note_cursor: usize, /// Range of notes to display @@ -67,12 +53,16 @@ pub struct App { pub track_cursor: usize, /// Collection of tracks pub tracks: Vec, + /// Paths to user directories + xdg: Option>, + /// Main audio outputs. + audio_outs: Vec>>, + /// Tick enable? + metronome: bool, /// Number of frames requested by process callback - pub chunk_size: usize, - /// Quantization factor - pub quant: usize, - /// Init callbacks called once after root JACK client has activated. - pub callbacks: Vec>)->Usually<()>) + Send + Sync>> + chunk_size: usize, + + pub transport: TransportToolbar, } impl App { @@ -80,11 +70,10 @@ impl App { let xdg = Arc::new(microxdg::XdgApp::new("tek")?); let first_run = crate::config::AppPaths::new(&xdg)?.should_create(); let jack = JackClient::Inactive(Client::new("tek", ClientOptions::NO_START_SERVER)?.0); - let transport = jack.transport(); Ok(Self { + transport: TransportToolbar::new(Some(jack.transport())), arranger_mode: false, audio_outs: vec![], - callbacks: vec![], chain_mode: false, chunk_size: 0, entered: true, @@ -95,35 +84,30 @@ impl App { modal: first_run.then(||crate::config::SetupModal(Some(xdg.clone())).boxed()), note_cursor: 0, note_start: 2, - play_started: None, - playhead: 0, - playing: None, - quant: 24, scene_cursor: 1, scenes: vec![], section: AppSection::default(), seq_mode: false, seq_buf: BufferedSequencerView::new(96, 16384), time_cursor: 0, - timebase: Arc::new(Timebase::default()), track_cursor: 1, tracks: vec![], - transport: Some(transport), xdg: Some(xdg), }) } } process!(App |self, _client, scope| { let ( - reset, current_frames, current_usecs, next_usecs, period_usecs - ) = self.update_time(&scope); + reset, current_frames, chunk_size, current_usecs, next_usecs, period_usecs + ) = self.transport.update(&scope); + self.chunk_size = chunk_size; for track in self.tracks.iter_mut() { track.process( self.midi_in.as_ref().map(|p|p.iter(&scope)), - &self.timebase, - self.playing, - self.play_started, - self.quant, + &self.transport.timebase, + self.transport.playing, + self.transport.started, + self.transport.quant, reset, &scope, (current_frames as usize, self.chunk_size), @@ -169,51 +153,6 @@ impl App { } Ok(app) } - pub fn update_time (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, f64) { - let CycleTimes { - current_frames, - current_usecs, - next_usecs, - period_usecs - } = scope.cycle_times().unwrap(); - self.chunk_size = scope.n_frames() as usize; - let transport = self.transport.as_ref().unwrap().query().unwrap(); - self.playhead = transport.pos.frame() as usize; - let mut reset = false; - if self.playing != Some(transport.state) { - match transport.state { - TransportState::Rolling => { - self.play_started = Some(( - current_frames as usize, - current_usecs as usize, - )); - }, - TransportState::Stopped => { - self.play_started = None; - reset = true; - }, - _ => {} - } - } - self.playing = Some(transport.state); - ( - reset, current_frames as usize, current_usecs as usize, next_usecs as usize, period_usecs as f64 - ) - } - pub fn toggle_play (&mut self) -> Usually<()> { - self.playing = match self.playing.expect("1st frame has not been processed yet") { - TransportState::Stopped => { - self.transport.as_ref().unwrap().start()?; - Some(TransportState::Starting) - }, - _ => { - self.transport.as_ref().unwrap().stop()?; - self.transport.as_ref().unwrap().locate(0)?; - Some(TransportState::Stopped) - }, - }; - Ok(()) - } } #[derive(PartialEq, Clone, Copy)] diff --git a/src/model/phrase.rs b/src/model/phrase.rs index a84a3f7b..ce64149d 100644 --- a/src/model/phrase.rs +++ b/src/model/phrase.rs @@ -44,11 +44,6 @@ impl App { let clip = (*scene.clips.get(track_id)?)?; self.track_mut()?.1.phrases.get_mut(clip) } - fn phrase_id (&self) -> Option { - let (track_id, _) = self.track()?; - let (_, scene) = self.scene()?; - *scene.clips.get(track_id)? - } } /// Define a MIDI phrase. diff --git a/src/model/transport.rs b/src/model/transport.rs new file mode 100644 index 00000000..0305cb69 --- /dev/null +++ b/src/model/transport.rs @@ -0,0 +1,97 @@ +use crate::{core::*,view::*,model::{App, AppSection}}; + +pub struct TransportToolbar { + mode: bool, + pub focused: bool, + pub entered: bool, + /// Current sample rate, tempo, and PPQ. + pub timebase: Arc, + /// JACK transport handle. + transport: Option, + /// Quantization factor + pub quant: usize, + /// Current transport state + pub playing: Option, + /// Current position according to transport + playhead: usize, + /// Global frame and usec at which playback started + pub started: Option<(usize, usize)>, +} + +impl TransportToolbar { + pub fn new (transport: Option) -> Self { + Self { + transport, + mode: false, + focused: false, + entered: false, + timebase: Arc::new(Timebase::default()), + playhead: 0, + playing: Some(TransportState::Stopped), + started: None, + quant: 24, + } + } + pub fn toggle_play (&mut self) -> Usually<()> { + self.playing = match self.playing.expect("1st frame has not been processed yet") { + TransportState::Stopped => { + self.transport.as_ref().unwrap().start()?; + Some(TransportState::Starting) + }, + _ => { + self.transport.as_ref().unwrap().stop()?; + self.transport.as_ref().unwrap().locate(0)?; + Some(TransportState::Stopped) + }, + }; + Ok(()) + } + pub fn update (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, usize, f64) { + let CycleTimes { + current_frames, + current_usecs, + next_usecs, + period_usecs + } = scope.cycle_times().unwrap(); + let chunk_size = scope.n_frames() as usize; + let transport = self.transport.as_ref().unwrap().query().unwrap(); + self.playhead = transport.pos.frame() as usize; + let mut reset = false; + if self.playing != Some(transport.state) { + match transport.state { + TransportState::Rolling => { + self.started = Some(( + current_frames as usize, + current_usecs as usize, + )); + }, + TransportState::Stopped => { + self.started = None; + reset = true; + }, + _ => {} + } + } + self.playing = Some(transport.state); + ( + reset, + current_frames as usize, + chunk_size as usize, + current_usecs as usize, + next_usecs as usize, + period_usecs as f64 + ) + } + pub fn bpm (&self) -> usize { + self.timebase.bpm() as usize + } + pub fn ppq (&self) -> usize { + self.timebase.ppq() as usize + } + pub fn pulse (&self) -> usize { + self.timebase.frame_to_pulse(self.playhead as f64) as usize + } + pub fn usecs (&self) -> usize { + self.timebase.frame_to_usec(self.playhead as f64) as usize + } +} diff --git a/src/view.rs b/src/view.rs index 0c1f4ed7..e276f924 100644 --- a/src/view.rs +++ b/src/view.rs @@ -8,7 +8,6 @@ pub mod theme; pub use self::border::*; pub use self::theme::*; -pub use self::transport::TransportView; pub use self::arranger::*; pub use self::chain::ChainView; pub use self::sequencer::{SequencerView, BufferedSequencerView}; @@ -17,7 +16,7 @@ use crate::{render, App, core::*}; render!(App |self, buf, area| { Split::down([ - &TransportView::new(self), + &self.transport, &ArrangerView::new(&self, !self.arranger_mode), &If(self.track_cursor > 0, &Split::right([ &ChainView::vertical(&self), diff --git a/src/view/arranger.rs b/src/view/arranger.rs index 167cecce..fd2fc64b 100644 --- a/src/view/arranger.rs +++ b/src/view/arranger.rs @@ -11,8 +11,6 @@ pub struct ArrangerView<'a> { vertical: bool, } -const HELP: &'static str = "[C-t] Add track [C-a] Add scene [v] Show/hide track"; - impl<'a> ArrangerView<'a> { pub fn new (app: &'a App, vertical: bool) -> Self { Self { @@ -39,11 +37,8 @@ impl<'a> Render for ArrangerView<'a> { } else { self.draw_horizontal(buf, area) }?; - if self.focused { - //HELP.blit(buf, area.x + 2, area.y + area.height - 1, Some(Style::default().dim()))?; - if self.entered { - Corners(Style::default().green().not_dim()).draw(buf, area)?; - } + if self.focused && self.entered { + Corners(Style::default().green().not_dim()).draw(buf, area)?; } Ok(area) } @@ -62,7 +57,7 @@ impl<'a> ArrangerView<'a> { if y + 2 * scene_index as u16 >= height { break } - let style = Some(highlight( + let style = Some(Nord::style_hi( self.focused, (0 == self.cursor.0) && (scene_index + 1 == self.cursor.1) ).bold()); @@ -82,7 +77,7 @@ impl<'a> ArrangerView<'a> { if x >= width { break } - let mut width = 16u16; + let width = 16u16; track.name.blit(buf, x, y, Some(Style::default().bold()))?; for (scene_index, scene) in self.scenes.iter().enumerate() { if y + 2 * scene_index as u16 >= height { @@ -90,11 +85,8 @@ impl<'a> ArrangerView<'a> { } let label = if let Some(Some(clip)) = scene.clips.get(track_index) { if let Some(phrase) = track.phrases.get(*clip) { - format!("{} {}", if track.sequence == Some(*clip) { - "" - } else { - "┊" - }, phrase.name) + let icon = match track.sequence { Some(_) => "", None => "┊" }; + format!("{icon} {}", phrase.name) } else { format!(" ??? ") } @@ -102,12 +94,11 @@ impl<'a> ArrangerView<'a> { format!("┊ ········") }; let hi = (track_index + 1 == self.cursor.0) && (scene_index + 1 == self.cursor.1); - let style = Some(highlight(self.focused, hi)); + let style = Some(Nord::style_hi(self.focused, hi)); let y = 1 + y + 2 * scene_index as u16; "┊".blit(buf, x, y, Some(Style::default().dim()))?; "┊".blit(buf, x, y + 1, Some(Style::default().dim()))?; label.blit(buf, x, y, style)?; - //width = width.max(2label.len() as u16 + 3); } if track_index + 1 == self.cursor.0 { let bg = Nord::bg_hi(self.focused, self.entered); @@ -305,9 +296,7 @@ impl<'a> ArrangerView<'a> { x3 = x3.max(label.len() as u16) } } - x2 = x2 + x3; - - x2 = x2 + 1; + x2 = x2 + x3 + 1; } Ok(Rect { x, y, height, width: x2 }) }, @@ -315,18 +304,6 @@ impl<'a> ArrangerView<'a> { } } -fn highlight (focused: bool, highlight: bool) -> Style { - if highlight { - if focused { - Style::default().yellow().not_dim() - } else { - Style::default().yellow().dim() - } - } else { - Style::default() - } -} - //use crate::core::*; //use crate::view::*; //use crate::model::*; diff --git a/src/view/sequencer.rs b/src/view/sequencer.rs index 528c5e01..2dbd16eb 100644 --- a/src/view/sequencer.rs +++ b/src/view/sequencer.rs @@ -113,8 +113,8 @@ impl<'a> SequencerView<'a> { phrase: app.phrase(), focused: app.section == AppSection::Sequencer, entered: app.entered, - ppq: app.timebase.ppq() as usize, - now: app.timebase.frame_to_pulse(app.playhead as f64) as usize, + ppq: app.transport.ppq(), + now: app.transport.pulse(), time_cursor: app.time_cursor, time_start: app.seq_buf.time_start, time_zoom: app.seq_buf.time_zoom, diff --git a/src/view/theme.rs b/src/view/theme.rs index 648e7975..182ba167 100644 --- a/src/view/theme.rs +++ b/src/view/theme.rs @@ -29,6 +29,16 @@ pub trait Theme { Color::Reset } } + + fn style_hi (focused: bool, highlight: bool) -> Style { + if highlight && focused { + Style::default().yellow().not_dim() + } else if highlight { + Style::default().yellow().dim() + } else { + Style::default() + } + } } pub struct Nord; diff --git a/src/view/transport.rs b/src/view/transport.rs index b8301a88..14729a4f 100644 --- a/src/view/transport.rs +++ b/src/view/transport.rs @@ -1,52 +1,33 @@ -use crate::{core::*,view::*,model::{App, AppSection}}; -pub struct TransportView { - focused: bool, - entered: bool, - playing: TransportState, - quant: usize, - ppq: usize, - bpm: usize, - pulse: usize, - usecs: usize, -} -impl TransportView { - pub fn new (app: &App) -> Self { - Self { - focused: app.section == AppSection::Transport, - entered: app.entered, - playing: *app.playing.as_ref().unwrap_or(&TransportState::Stopped), - quant: app.quant, - ppq: app.timebase.ppq() as usize, - bpm: app.timebase.bpm() as usize, - pulse: app.timebase.frame_to_pulse(app.playhead as f64) as usize, - usecs: app.timebase.frame_to_usec(app.playhead as f64) as usize, - } - } -} -render!(TransportView |self, buf, area| { - let mut area = area; - area.height = 2; - let Self { ppq, bpm, quant, pulse, usecs, .. } = self; - - fill_bg(buf, area, Nord::bg_lo(self.focused, self.entered)); +use crate::{core::*,view::*,model::*}; +render!(TransportToolbar |self, buf, area| { let gray = Style::default().gray(); let not_dim = Style::default().not_dim(); let not_dim_bold = not_dim.bold(); + let mut area = area; + area.height = 2; + let ppq = self.ppq(); + let bpm = self.bpm(); + let pulse = self.pulse(); + let usecs = self.usecs(); + let Self { quant, focused, entered, .. } = self; + fill_bg(buf, area, Nord::bg_lo(*focused, *entered)); let area = Split::right([ // Play/Pause button &|buf: &mut Buffer, Rect { x, y, .. }: Rect|{ - let style = Some(match &self.playing { - TransportState::Stopped => gray.dim().bold(), - TransportState::Starting => gray.not_dim().bold(), - TransportState::Rolling => gray.not_dim().white().bold() + let style = Some(match self.playing { + Some(TransportState::Stopped) => gray.dim().bold(), + Some(TransportState::Starting) => gray.not_dim().bold(), + Some(TransportState::Rolling) => gray.not_dim().white().bold(), + _ => unreachable!(), }); - let label = match &self.playing { - TransportState::Rolling => "▶ PLAYING", - TransportState::Starting => "READY ...", - TransportState::Stopped => "⏹ STOPPED", + let label = match self.playing { + Some(TransportState::Rolling) => "▶ PLAYING", + Some(TransportState::Starting) => "READY ...", + Some(TransportState::Stopped) => "⏹ STOPPED", + _ => unreachable!(), }; let mut result = label.blit(buf, x + 1, y, style)?; result.width = result.width + 1; @@ -86,7 +67,7 @@ render!(TransportView |self, buf, area| { ]).render(buf, area)?; - Ok(if self.focused && self.entered { + Ok(if *focused && *entered { Corners(Style::default().green().not_dim()).draw(buf, area)? } else { area