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]]
|
||||
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::ratatui::prelude::*;
|
||||
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 atomic_float::AtomicF64;
|
||||
submod! {
|
||||
|
|
@ -9,6 +8,8 @@ submod! {
|
|||
ticks
|
||||
transport
|
||||
transport_focus
|
||||
transport_handle
|
||||
transport_render
|
||||
}
|
||||
|
||||
/// (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,
|
||||
/// Current sample rate, tempo, and PPQ.
|
||||
pub timebase: Arc<Timebase>,
|
||||
/// JACK client handle (needs to not be dropped for standalone mode to work).
|
||||
pub jack: Option<JackClient>,
|
||||
/// JACK transport handle.
|
||||
transport: Option<Transport>,
|
||||
pub transport: Option<Transport>,
|
||||
/// Quantization factor
|
||||
pub quant: usize,
|
||||
/// Global sync quant
|
||||
|
|
@ -18,15 +20,38 @@ pub struct TransportToolbar {
|
|||
/// Current transport state
|
||||
pub playing: Option<TransportState>,
|
||||
/// Current position according to transport
|
||||
playhead: usize,
|
||||
pub playhead: usize,
|
||||
/// Global frame and usec at which playback started
|
||||
pub started: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
process!(TransportToolbar |self, _client, scope| {
|
||||
self.update(&scope);
|
||||
Control::Continue
|
||||
});
|
||||
|
||||
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 {
|
||||
let timebase = Arc::new(Timebase::default());
|
||||
Self {
|
||||
|
|
@ -39,10 +64,12 @@ impl TransportToolbar {
|
|||
started: None,
|
||||
quant: 24,
|
||||
sync: timebase.ppq() as usize * 4,
|
||||
jack: None,
|
||||
transport,
|
||||
timebase,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_play (&mut self) -> Usually<()> {
|
||||
self.playing = match self.playing.expect("1st frame has not been processed yet") {
|
||||
TransportState::Stopped => {
|
||||
|
|
@ -57,6 +84,7 @@ impl TransportToolbar {
|
|||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update (&mut self, scope: &ProcessScope) -> (bool, usize, usize, usize, usize, f64) {
|
||||
let CycleTimes {
|
||||
current_frames,
|
||||
|
|
@ -93,142 +121,21 @@ impl TransportToolbar {
|
|||
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 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