diff --git a/src/control.rs b/src/control.rs index 59f17867..0472b866 100644 --- a/src/control.rs +++ b/src/control.rs @@ -2,7 +2,7 @@ use crate::{core::*, handle, App, AppFocus}; -submod!{ chain focus mixer plugin sampler transport } +submod!{ chain focus mixer plugin } handle!{ App |self, e| { @@ -29,7 +29,7 @@ handle!{ fn handle_focused (state: &mut App, e: &AppEvent) -> Usually { match state.section { AppFocus::Transport => - handle_keymap(state, e, crate::control::transport::KEYMAP_TRANSPORT), + handle_keymap(state, e, crate::devices::transport::KEYMAP_TRANSPORT), AppFocus::Arranger => handle_keymap(state, e, crate::devices::arranger::KEYMAP_ARRANGER), AppFocus::Sequencer => diff --git a/src/control/sampler.rs b/src/control/sampler.rs deleted file mode 100644 index 97c8b492..00000000 --- a/src/control/sampler.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::{core::*, model::*}; - -pub fn handle_sampler (state: &mut Sampler, event: &AppEvent) -> Usually { - handle_keymap(state, event, KEYMAP_SAMPLER) -} - -/// Key bindings for sampler device. -pub const KEYMAP_SAMPLER: &'static [KeyBinding] = keymap!(Sampler { - [Up, NONE, "cursor_up", "move cursor up", cursor_up], - [Down, NONE, "cursor_down", "move cursor down", cursor_down], - [Char('t'), NONE, "sample_play", "play current sample", trigger], - [Char('a'), NONE, "sample_add", "add a new sample", add_sample], - [Enter, NONE, "sample_edit", "edit selected sample", edit_sample], -}); - -fn cursor_up (state: &mut Sampler) -> Usually { - state.cursor.0 = if state.cursor.0 == 0 { - state.mapped.len() - 1 - } else { - state.cursor.0 - 1 - }; - Ok(true) -} - -fn cursor_down (state: &mut Sampler) -> Usually { - state.cursor.0 = (state.cursor.0 + 1) % state.mapped.len(); - Ok(true) -} - -fn trigger (state: &mut Sampler) -> Usually { - if let Some(sample) = state.sample() { - state.voices.push(sample.play(0, &100.into())) - } - Ok(true) -} - -fn add_sample (state: &mut Sampler) -> Usually { - state.unmapped.push(Sample::new("", 0, 0, vec![])); - Ok(true) -} - -fn edit_sample (state: &mut Sampler) -> Usually { - if let Some(sample) = state.sample() { - state.editing = Some(sample.clone()); - } - Ok(true) -} diff --git a/src/control/transport.rs b/src/control/transport.rs deleted file mode 100644 index e279410f..00000000 --- a/src/control/transport.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::{core::*, model::{App, TransportFocus}}; - -/// Key bindings for transport toolbar. -pub const KEYMAP_TRANSPORT: &'static [KeyBinding] = keymap!(App { - [Left, NONE, "transport_prev", "select previous control", |app: &mut App| Ok({ - app.transport.selected.prev(); - true - })], - [Right, NONE, "transport_next", "select next control", |app: &mut App| Ok({ - app.transport.selected.next(); - true - })], - [Char('.'), NONE, "transport_increment", "increment value at cursor", |app: &mut App| { - match app.transport.selected { - TransportFocus::BPM => { - app.transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed); - }, - TransportFocus::Quant => { - app.transport.quant = next_note_length(app.transport.quant) - }, - TransportFocus::Sync => { - app.transport.sync = next_note_length(app.transport.sync) - }, - }; - Ok(true) - }], - [Char(','), NONE, "transport_decrement", "decrement value at cursor", |app: &mut App| { - match app.transport.selected { - TransportFocus::BPM => { - app.transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed); - }, - TransportFocus::Quant => { - app.transport.quant = prev_note_length(app.transport.quant); - }, - TransportFocus::Sync => { - app.transport.sync = prev_note_length(app.transport.sync); - }, - }; - Ok(true) - }], -}); diff --git a/src/devices.rs b/src/devices.rs index 3b09d5b7..787601e8 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -1 +1 @@ -crate::core::pubmod! { sequencer arranger } +crate::core::pubmod! { arranger sampler sequencer transport } diff --git a/src/model/sampler.rs b/src/devices/sampler.rs similarity index 83% rename from src/model/sampler.rs rename to src/devices/sampler.rs index c3c2ebc6..f77cd32e 100644 --- a/src/model/sampler.rs +++ b/src/devices/sampler.rs @@ -1,4 +1,47 @@ -use crate::core::*; +use crate::{core::*, model::*}; + + +/// Key bindings for sampler device. +pub const KEYMAP_SAMPLER: &'static [KeyBinding] = keymap!(Sampler { + [Up, NONE, "cursor_up", "move cursor up", cursor_up], + [Down, NONE, "cursor_down", "move cursor down", cursor_down], + [Char('t'), NONE, "sample_play", "play current sample", trigger], + [Char('a'), NONE, "sample_add", "add a new sample", add_sample], + [Enter, NONE, "sample_edit", "edit selected sample", edit_sample], +}); + +fn cursor_up (state: &mut Sampler) -> Usually { + state.cursor.0 = if state.cursor.0 == 0 { + state.mapped.len() - 1 + } else { + state.cursor.0 - 1 + }; + Ok(true) +} + +fn cursor_down (state: &mut Sampler) -> Usually { + state.cursor.0 = (state.cursor.0 + 1) % state.mapped.len(); + Ok(true) +} + +fn trigger (state: &mut Sampler) -> Usually { + if let Some(sample) = state.sample() { + state.voices.push(sample.play(0, &100.into())) + } + Ok(true) +} + +fn add_sample (state: &mut Sampler) -> Usually { + state.unmapped.push(Sample::new("", 0, 0, vec![])); + Ok(true) +} + +fn edit_sample (state: &mut Sampler) -> Usually { + if let Some(sample) = state.sample() { + state.editing = Some(sample.clone()); + } + Ok(true) +} /// The sampler plugin plays sounds. pub struct Sampler { @@ -12,6 +55,10 @@ pub struct Sampler { pub buffer: Vec>, pub output_gain: f32 } +process!(Sampler = Sampler::process); +handle!(Sampler |self, event| { + handle_keymap(self, event, KEYMAP_SAMPLER) +}); render!(Sampler |self, buf, area| { let Rect { x, y, height, .. } = area; let style = Style::default().gray(); @@ -42,9 +89,6 @@ render!(Sampler |self, buf, area| { Ok(Rect { x, y, width: (width as u16).min(area.width), height }) }); -handle!(Sampler = crate::control::handle_sampler); -process!(Sampler = Sampler::process); - impl Sampler { pub fn new (name: &str, mapped: Option>>) -> Usually { Jack::new(name)? diff --git a/src/devices/transport.rs b/src/devices/transport.rs new file mode 100644 index 00000000..9e564c44 --- /dev/null +++ b/src/devices/transport.rs @@ -0,0 +1,246 @@ +use crate::{core::*, view::*, model::App}; + +/// Key bindings for transport toolbar. +pub const KEYMAP_TRANSPORT: &'static [KeyBinding] = keymap!(App { + [Left, NONE, "transport_prev", "select previous control", |app: &mut App| Ok({ + app.transport.selected.prev(); + true + })], + [Right, NONE, "transport_next", "select next control", |app: &mut App| Ok({ + app.transport.selected.next(); + true + })], + [Char('.'), NONE, "transport_increment", "increment value at cursor", |app: &mut App| { + match app.transport.selected { + TransportFocus::BPM => { + app.transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed); + }, + TransportFocus::Quant => { + app.transport.quant = next_note_length(app.transport.quant) + }, + TransportFocus::Sync => { + app.transport.sync = next_note_length(app.transport.sync) + }, + }; + Ok(true) + }], + [Char(','), NONE, "transport_decrement", "decrement value at cursor", |app: &mut App| { + match app.transport.selected { + TransportFocus::BPM => { + app.transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed); + }, + TransportFocus::Quant => { + app.transport.quant = prev_note_length(app.transport.quant); + }, + TransportFocus::Sync => { + app.transport.sync = prev_note_length(app.transport.sync); + }, + }; + Ok(true) + }], +}); + +#[derive(PartialEq)] +/// Which section of the transport is focused +pub enum TransportFocus { BPM, Quant, Sync } + +impl TransportFocus { + pub fn prev (&mut self) { + *self = match self { + Self::BPM => Self::Sync, + Self::Quant => Self::BPM, + Self::Sync => Self::Quant, + } + } + pub fn next (&mut self) { + *self = match self { + Self::BPM => Self::Quant, + Self::Quant => Self::Sync, + Self::Sync => Self::BPM, + } + } +} + +/// Stores and displays time-related state. +pub struct TransportToolbar { + /// Enable metronome? + pub metronome: bool, + pub focused: bool, + pub entered: bool, + pub selected: TransportFocus, + /// Current sample rate, tempo, and PPQ. + pub timebase: Arc, + /// JACK transport handle. + transport: Option, + /// Quantization factor + pub quant: u16, + /// Global sync quant + pub sync: u16, + /// 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 { + let timebase = Arc::new(Timebase::default()); + Self { + selected: TransportFocus::BPM, + metronome: false, + focused: false, + entered: false, + playhead: 0, + playing: Some(TransportState::Stopped), + started: None, + quant: 24, + sync: timebase.ppq() as u16 * 4, + transport, + timebase, + } + } + 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 + } +} + +render!(TransportToolbar |self, buf, area| { + let mut area = area; + area.height = 2; + let gray = Style::default().gray(); + let not_dim = Style::default().not_dim(); + let not_dim_bold = not_dim.bold(); + let corners = Corners(Style::default().green().not_dim()); + let ppq = self.ppq(); + let bpm = self.bpm(); + let pulse = self.pulse(); + let usecs = self.usecs(); + let Self { quant, sync, focused, entered, .. } = self; + fill_bg(buf, area, Nord::bg_lo(*focused, *entered)); + Split::right([ + + // Play/Pause button + &|buf: &mut Buffer, Rect { x, y, .. }: Rect|{ + 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 { + 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; + Ok(result) + }, + + // Beats per minute + &|buf: &mut Buffer, Rect { x, y, .. }: Rect|{ + "BPM".blit(buf, x, y, Some(not_dim))?; + let width = format!("{}.{:03}", bpm, bpm % 1).blit(buf, x, y + 1, Some(not_dim_bold))?.width; + let area = Rect { x, y, width: (width + 2).max(10), height: 2 }; + if self.focused && self.entered && self.selected == TransportFocus::BPM { + corners.draw(buf, Rect { x: area.x - 1, ..area })?; + } + Ok(area) + }, + + // Quantization + &|buf: &mut Buffer, Rect { x, y, .. }: Rect|{ + "QUANT".blit(buf, x, y, Some(not_dim))?; + let width = ppq_to_name(*quant as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width; + let area = Rect { x, y, width: (width + 2).max(10), height: 2 }; + if self.focused && self.entered && self.selected == TransportFocus::Quant { + corners.draw(buf, Rect { x: area.x - 1, ..area })?; + } + Ok(area) + }, + + // Clip launch sync + &|buf: &mut Buffer, Rect { x, y, .. }: Rect|{ + "SYNC".blit(buf, x, y, Some(not_dim))?; + let width = ppq_to_name(*sync as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width; + let area = Rect { x, y, width: (width + 2).max(10), height: 2 }; + if self.focused && self.entered && self.selected == TransportFocus::Sync { + corners.draw(buf, Rect { x: area.x - 1, ..area })?; + } + Ok(area) + }, + + // Clock + &|buf: &mut Buffer, Rect { x, y, width, .. }: Rect|{ + let (beats, pulses) = (pulse / ppq, pulse % ppq); + let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1); + let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); + let (minutes, seconds) = (seconds / 60, seconds % 60); + let timer = format!("{minutes}:{seconds:02}:{msecs:03} {bars}.{beats}.{pulses:02}"); + timer.blit(buf, x + width - timer.len() as u16 - 1, y, Some(not_dim)) + } + + ]).render(buf, area) +}); diff --git a/src/edn.rs b/src/edn.rs index 976cd5d9..89022b2d 100644 --- a/src/edn.rs +++ b/src/edn.rs @@ -13,8 +13,7 @@ //! * [Sample::load_edn] //! * [LV2Plugin::load_edn] -use crate::{core::*, model::*, App}; - +use crate::{core::*, model::*, App, devices::sampler::{Sampler, Sample, read_sample_data}}; use clojure_reader::{edn::{read, Edn}, error::Error as EdnError}; /// EDN parsing helper. diff --git a/src/model.rs b/src/model.rs index a52c7168..30961dca 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,8 +1,8 @@ //! Application state. -use crate::{core::*, devices::{arranger::*, sequencer::*}}; +use crate::{core::*, devices::{arranger::*, sequencer::*, transport::*}}; -submod! { axis looper mixer phrase plugin sampler scene track transport } +submod! { axis looper mixer phrase plugin scene track } /// Root of application state. pub struct App { diff --git a/src/model/transport.rs b/src/model/transport.rs deleted file mode 100644 index 3973dc98..00000000 --- a/src/model/transport.rs +++ /dev/null @@ -1,126 +0,0 @@ -use crate::core::*; - -#[derive(PartialEq)] -/// Which section of the transport is focused -pub enum TransportFocus { BPM, Quant, Sync } - -impl TransportFocus { - pub fn prev (&mut self) { - *self = match self { - Self::BPM => Self::Sync, - Self::Quant => Self::BPM, - Self::Sync => Self::Quant, - } - } - pub fn next (&mut self) { - *self = match self { - Self::BPM => Self::Quant, - Self::Quant => Self::Sync, - Self::Sync => Self::BPM, - } - } -} - -/// Stores and displays time-related state. -pub struct TransportToolbar { - /// Enable metronome? - pub metronome: bool, - pub focused: bool, - pub entered: bool, - pub selected: TransportFocus, - /// Current sample rate, tempo, and PPQ. - pub timebase: Arc, - /// JACK transport handle. - transport: Option, - /// Quantization factor - pub quant: u16, - /// Global sync quant - pub sync: u16, - /// 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 { - let timebase = Arc::new(Timebase::default()); - Self { - selected: TransportFocus::BPM, - metronome: false, - focused: false, - entered: false, - playhead: 0, - playing: Some(TransportState::Stopped), - started: None, - quant: 24, - sync: timebase.ppq() as u16 * 4, - transport, - timebase, - } - } - 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 95f1d09f..3efd6fb5 100644 --- a/src/view.rs +++ b/src/view.rs @@ -2,7 +2,7 @@ use crate::{render, App, core::*}; -submod! { border chain help plugin split theme transport } +submod! { border chain help plugin split theme } render!(App |self, buf, area| { Split::down([ diff --git a/src/view/transport.rs b/src/view/transport.rs deleted file mode 100644 index 8744855f..00000000 --- a/src/view/transport.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::{core::*,view::*,model::*}; - -render!(TransportToolbar |self, buf, area| { - let mut area = area; - area.height = 2; - let gray = Style::default().gray(); - let not_dim = Style::default().not_dim(); - let not_dim_bold = not_dim.bold(); - let corners = Corners(Style::default().green().not_dim()); - let ppq = self.ppq(); - let bpm = self.bpm(); - let pulse = self.pulse(); - let usecs = self.usecs(); - let Self { quant, sync, focused, entered, .. } = self; - fill_bg(buf, area, Nord::bg_lo(*focused, *entered)); - Split::right([ - - // Play/Pause button - &|buf: &mut Buffer, Rect { x, y, .. }: Rect|{ - 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 { - 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; - Ok(result) - }, - - // Beats per minute - &|buf: &mut Buffer, Rect { x, y, .. }: Rect|{ - "BPM".blit(buf, x, y, Some(not_dim))?; - let width = format!("{}.{:03}", bpm, bpm % 1).blit(buf, x, y + 1, Some(not_dim_bold))?.width; - let area = Rect { x, y, width: (width + 2).max(10), height: 2 }; - if self.focused && self.entered && self.selected == TransportFocus::BPM { - corners.draw(buf, Rect { x: area.x - 1, ..area })?; - } - Ok(area) - }, - - // Quantization - &|buf: &mut Buffer, Rect { x, y, .. }: Rect|{ - "QUANT".blit(buf, x, y, Some(not_dim))?; - let width = ppq_to_name(*quant as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width; - let area = Rect { x, y, width: (width + 2).max(10), height: 2 }; - if self.focused && self.entered && self.selected == TransportFocus::Quant { - corners.draw(buf, Rect { x: area.x - 1, ..area })?; - } - Ok(area) - }, - - // Clip launch sync - &|buf: &mut Buffer, Rect { x, y, .. }: Rect|{ - "SYNC".blit(buf, x, y, Some(not_dim))?; - let width = ppq_to_name(*sync as u16).blit(buf, x, y + 1, Some(not_dim_bold))?.width; - let area = Rect { x, y, width: (width + 2).max(10), height: 2 }; - if self.focused && self.entered && self.selected == TransportFocus::Sync { - corners.draw(buf, Rect { x: area.x - 1, ..area })?; - } - Ok(area) - }, - - // Clock - &|buf: &mut Buffer, Rect { x, y, width, .. }: Rect|{ - let (beats, pulses) = (pulse / ppq, pulse % ppq); - let (bars, beats) = ((beats / 4) + 1, (beats % 4) + 1); - let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); - let (minutes, seconds) = (seconds / 60, seconds % 60); - let timer = format!("{minutes}:{seconds:02}:{msecs:03} {bars}.{beats}.{pulses:02}"); - timer.blit(buf, x + width - timer.len() as u16 - 1, y, Some(not_dim)) - } - - ]).render(buf, area) -});