mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
transport: some standalone functioning
This commit is contained in:
parent
430c51e305
commit
b6da43e93e
7 changed files with 179 additions and 150 deletions
|
|
@ -13,4 +13,4 @@ path = "src/lib.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "tek_timer"
|
name = "tek_timer"
|
||||||
path = "src/main.rs"
|
path = "src/transport_main.rs"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
pub(crate) use tek_core::*;
|
pub(crate) use tek_core::*;
|
||||||
pub(crate) use tek_core::ratatui::prelude::*;
|
|
||||||
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
||||||
pub(crate) use tek_jack::jack::*;
|
pub(crate) use tek_jack::{*, jack::*};
|
||||||
pub(crate) use std::sync::{Arc, atomic::Ordering};
|
pub(crate) use std::sync::{Arc, atomic::Ordering};
|
||||||
pub(crate) use atomic_float::AtomicF64;
|
pub(crate) use atomic_float::AtomicF64;
|
||||||
submod! {
|
submod! {
|
||||||
|
|
@ -9,6 +8,8 @@ submod! {
|
||||||
ticks
|
ticks
|
||||||
transport
|
transport
|
||||||
transport_focus
|
transport_focus
|
||||||
|
transport_handle
|
||||||
|
transport_render
|
||||||
}
|
}
|
||||||
|
|
||||||
/// (pulses, name)
|
/// (pulses, name)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
pub(crate) use tek_core::*;
|
|
||||||
pub(crate) use tek_core::ratatui::prelude::*;
|
|
||||||
pub(crate) use tek_core::crossterm::event::{KeyCode, KeyModifiers};
|
|
||||||
pub(crate) use tek_jack::jack::*;
|
|
||||||
pub(crate) use std::sync::{Arc, atomic::Ordering};
|
|
||||||
pub(crate) use atomic_float::AtomicF64;
|
|
||||||
submod! {
|
|
||||||
timebase
|
|
||||||
ticks
|
|
||||||
transport
|
|
||||||
transport_focus
|
|
||||||
}
|
|
||||||
/// Application entrypoint.
|
|
||||||
pub fn main () -> Usually<()> {
|
|
||||||
run(Arc::new(RwLock::new(TransportToolbar::standalone())))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -9,8 +9,10 @@ pub struct TransportToolbar {
|
||||||
pub selected: TransportFocus,
|
pub selected: TransportFocus,
|
||||||
/// Current sample rate, tempo, and PPQ.
|
/// Current sample rate, tempo, and PPQ.
|
||||||
pub timebase: Arc<Timebase>,
|
pub timebase: Arc<Timebase>,
|
||||||
|
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||||
|
pub jack: Option<JackClient>,
|
||||||
/// JACK transport handle.
|
/// JACK transport handle.
|
||||||
transport: Option<Transport>,
|
pub transport: Option<Transport>,
|
||||||
/// Quantization factor
|
/// Quantization factor
|
||||||
pub quant: usize,
|
pub quant: usize,
|
||||||
/// Global sync quant
|
/// Global sync quant
|
||||||
|
|
@ -18,15 +20,38 @@ pub struct TransportToolbar {
|
||||||
/// Current transport state
|
/// Current transport state
|
||||||
pub playing: Option<TransportState>,
|
pub playing: Option<TransportState>,
|
||||||
/// Current position according to transport
|
/// Current position according to transport
|
||||||
playhead: usize,
|
pub playhead: usize,
|
||||||
/// Global frame and usec at which playback started
|
/// Global frame and usec at which playback started
|
||||||
pub started: Option<(usize, usize)>,
|
pub started: Option<(usize, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
process!(TransportToolbar |self, _client, scope| {
|
||||||
|
self.update(&scope);
|
||||||
|
Control::Continue
|
||||||
|
});
|
||||||
|
|
||||||
impl TransportToolbar {
|
impl TransportToolbar {
|
||||||
pub fn standalone () -> Self {
|
|
||||||
Self::new(None)
|
pub fn standalone () -> Usually<Arc<RwLock<Self>>> {
|
||||||
|
let mut transport = Self::new(None);
|
||||||
|
transport.focused = true;
|
||||||
|
transport.entered = true;
|
||||||
|
let jack = JackClient::Inactive(
|
||||||
|
Client::new("tek_transport", ClientOptions::NO_START_SERVER)?.0
|
||||||
|
);
|
||||||
|
transport.transport = Some(jack.transport());
|
||||||
|
let transport = Arc::new(RwLock::new(transport));
|
||||||
|
transport.write().unwrap().jack = Some(
|
||||||
|
jack.activate(
|
||||||
|
&transport.clone(),
|
||||||
|
|state, client, scope| {
|
||||||
|
state.write().unwrap().process(client, scope)
|
||||||
|
}
|
||||||
|
)?
|
||||||
|
);
|
||||||
|
Ok(transport)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new (transport: Option<Transport>) -> Self {
|
pub fn new (transport: Option<Transport>) -> Self {
|
||||||
let timebase = Arc::new(Timebase::default());
|
let timebase = Arc::new(Timebase::default());
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -39,10 +64,12 @@ impl TransportToolbar {
|
||||||
started: None,
|
started: None,
|
||||||
quant: 24,
|
quant: 24,
|
||||||
sync: timebase.ppq() as usize * 4,
|
sync: timebase.ppq() as usize * 4,
|
||||||
|
jack: None,
|
||||||
transport,
|
transport,
|
||||||
timebase,
|
timebase,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_play (&mut self) -> Usually<()> {
|
pub fn toggle_play (&mut self) -> Usually<()> {
|
||||||
self.playing = match self.playing.expect("1st frame has not been processed yet") {
|
self.playing = match self.playing.expect("1st frame has not been processed yet") {
|
||||||
TransportState::Stopped => {
|
TransportState::Stopped => {
|
||||||
|
|
@ -57,6 +84,7 @@ impl TransportToolbar {
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, usize, f64) {
|
pub fn update (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, usize, f64) {
|
||||||
let CycleTimes {
|
let CycleTimes {
|
||||||
current_frames,
|
current_frames,
|
||||||
|
|
@ -93,142 +121,21 @@ impl TransportToolbar {
|
||||||
period_usecs as f64
|
period_usecs as f64
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bpm (&self) -> usize {
|
pub fn bpm (&self) -> usize {
|
||||||
self.timebase.bpm() as usize
|
self.timebase.bpm() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppq (&self) -> usize {
|
pub fn ppq (&self) -> usize {
|
||||||
self.timebase.ppq() as usize
|
self.timebase.ppq() as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pulse (&self) -> usize {
|
pub fn pulse (&self) -> usize {
|
||||||
self.timebase.frame_to_pulse(self.playhead as f64) as usize
|
self.timebase.frame_to_pulse(self.playhead as f64) as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn usecs (&self) -> usize {
|
pub fn usecs (&self) -> usize {
|
||||||
self.timebase.frame_to_usec(self.playhead as f64) as 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 usize).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 usize).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)
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Key bindings for transport toolbar.
|
|
||||||
pub const KEYMAP_TRANSPORT: &'static [KeyBinding<TransportToolbar>] = keymap!(TransportToolbar {
|
|
||||||
[Left, NONE, "transport_prev", "select previous control", |transport: &mut TransportToolbar| Ok({
|
|
||||||
transport.selected.prev();
|
|
||||||
true
|
|
||||||
})],
|
|
||||||
[Right, NONE, "transport_next", "select next control", |transport: &mut TransportToolbar| Ok({
|
|
||||||
transport.selected.next();
|
|
||||||
true
|
|
||||||
})],
|
|
||||||
[Char('.'), NONE, "transport_increment", "increment value at cursor", |transport: &mut TransportToolbar| {
|
|
||||||
match transport.selected {
|
|
||||||
TransportFocus::BPM => {
|
|
||||||
transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
|
|
||||||
},
|
|
||||||
TransportFocus::Quant => {
|
|
||||||
transport.quant = next_note_length(transport.quant)
|
|
||||||
},
|
|
||||||
TransportFocus::Sync => {
|
|
||||||
transport.sync = next_note_length(transport.sync)
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok(true)
|
|
||||||
}],
|
|
||||||
[Char(','), NONE, "transport_decrement", "decrement value at cursor", |transport: &mut TransportToolbar| {
|
|
||||||
match transport.selected {
|
|
||||||
TransportFocus::BPM => {
|
|
||||||
transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
|
|
||||||
},
|
|
||||||
TransportFocus::Quant => {
|
|
||||||
transport.quant = prev_note_length(transport.quant);
|
|
||||||
},
|
|
||||||
TransportFocus::Sync => {
|
|
||||||
transport.sync = prev_note_length(transport.sync);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok(true)
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
|
|
||||||
handle!{
|
|
||||||
TransportToolbar |self, e| {
|
|
||||||
handle_keymap(self, e, KEYMAP_TRANSPORT)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
crates/tek_timer/src/transport_handle.rs
Normal file
51
crates/tek_timer/src/transport_handle.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
handle!{
|
||||||
|
TransportToolbar |self, e| {
|
||||||
|
handle_keymap(self, e, KEYMAP_TRANSPORT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Key bindings for transport toolbar.
|
||||||
|
pub const KEYMAP_TRANSPORT: &'static [KeyBinding<TransportToolbar>] = keymap!(TransportToolbar {
|
||||||
|
[Left, NONE, "transport_prev", "select previous control", |transport: &mut TransportToolbar| Ok({
|
||||||
|
transport.selected.prev();
|
||||||
|
true
|
||||||
|
})],
|
||||||
|
[Right, NONE, "transport_next", "select next control", |transport: &mut TransportToolbar| Ok({
|
||||||
|
transport.selected.next();
|
||||||
|
true
|
||||||
|
})],
|
||||||
|
[Char('.'), NONE, "transport_increment", "increment value at cursor", |transport: &mut TransportToolbar| {
|
||||||
|
match transport.selected {
|
||||||
|
TransportFocus::BPM => {
|
||||||
|
transport.timebase.bpm.fetch_add(1.0, Ordering::Relaxed);
|
||||||
|
},
|
||||||
|
TransportFocus::Quant => {
|
||||||
|
transport.quant = next_note_length(transport.quant)
|
||||||
|
},
|
||||||
|
TransportFocus::Sync => {
|
||||||
|
transport.sync = next_note_length(transport.sync)
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
[Char(','), NONE, "transport_decrement", "decrement value at cursor", |transport: &mut TransportToolbar| {
|
||||||
|
match transport.selected {
|
||||||
|
TransportFocus::BPM => {
|
||||||
|
transport.timebase.bpm.fetch_sub(1.0, Ordering::Relaxed);
|
||||||
|
},
|
||||||
|
TransportFocus::Quant => {
|
||||||
|
transport.quant = prev_note_length(transport.quant);
|
||||||
|
},
|
||||||
|
TransportFocus::Sync => {
|
||||||
|
transport.sync = prev_note_length(transport.sync);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
[Char(' '), NONE, "transport_play_toggle", "play or pause", |transport: &mut TransportToolbar| {
|
||||||
|
transport.toggle_play()?;
|
||||||
|
Ok(true)
|
||||||
|
}],
|
||||||
|
});
|
||||||
6
crates/tek_timer/src/transport_main.rs
Normal file
6
crates/tek_timer/src/transport_main.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
include!("lib.rs");
|
||||||
|
/// Application entrypoint.
|
||||||
|
pub fn main () -> Usually<()> {
|
||||||
|
run(TransportToolbar::standalone()?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
81
crates/tek_timer/src/transport_render.rs
Normal file
81
crates/tek_timer/src/transport_render.rs
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
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 usize).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 usize).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)
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue