mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 19:56:42 +01:00
374 lines
14 KiB
Rust
374 lines
14 KiB
Rust
use crate::*;
|
|
use ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync};
|
|
use TransportCommand::{Focus, Clock};
|
|
use FocusCommand::{Next, Prev};
|
|
use KeyCode::{Enter, Left, Right, Char};
|
|
/// Transport clock app.
|
|
pub struct TransportTui {
|
|
pub jack: Arc<RwLock<JackConnection>>,
|
|
pub clock: ClockModel,
|
|
pub size: Measure<Tui>,
|
|
pub cursor: (usize, usize),
|
|
pub focus: TransportFocus,
|
|
}
|
|
from_jack!(|jack|TransportTui Self {
|
|
jack: jack.clone(),
|
|
clock: ClockModel::from(jack),
|
|
size: Measure::new(),
|
|
cursor: (0, 0),
|
|
focus: TransportFocus::PlayPause
|
|
});
|
|
has_clock!(|self:TransportTui|&self.clock);
|
|
audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope));
|
|
handle!(<Tui>|self:TransportTui,from|TransportCommand::execute_with_state(self, from));
|
|
render!(<Tui>|self: TransportTui|TransportView::from((self, None, true)));
|
|
impl std::fmt::Debug for TransportTui {
|
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
|
|
f.debug_struct("TransportTui")
|
|
.field("jack", &self.jack)
|
|
.field("size", &self.size)
|
|
.field("cursor", &self.cursor)
|
|
.finish()
|
|
}
|
|
}
|
|
pub struct TransportView {
|
|
color: ItemPalette,
|
|
focused: bool,
|
|
sr: String,
|
|
chunk: String,
|
|
latency: String,
|
|
bpm: String,
|
|
ppq: String,
|
|
beat: String,
|
|
global_sample: String,
|
|
global_second: String,
|
|
started: bool,
|
|
current_sample: f64,
|
|
current_second: f64,
|
|
}
|
|
impl<T: HasClock> From<(&T, Option<ItemPalette>, bool)> for TransportView {
|
|
fn from ((state, color, focused): (&T, Option<ItemPalette>, bool)) -> Self {
|
|
let clock = state.clock();
|
|
let rate = clock.timebase.sr.get();
|
|
let chunk = clock.chunk.load(Relaxed);
|
|
let latency = chunk as f64 / rate * 1000.;
|
|
let sr = format!("{:.1}k", rate / 1000.0);
|
|
let bpm = format!("{:.3}", clock.timebase.bpm.get());
|
|
let ppq = format!("{:.0}", clock.timebase.ppq.get());
|
|
let chunk = format!("{chunk}");
|
|
let latency = format!("{latency}");
|
|
let color = color.unwrap_or(ItemPalette::from(TuiTheme::g(32)));
|
|
if let Some(started) = clock.started.read().unwrap().as_ref() {
|
|
let current_sample = (clock.global.sample.get() - started.sample.get())/1000.;
|
|
let current_usec = clock.global.usec.get() - started.usec.get();
|
|
let current_second = current_usec/1000000.;
|
|
Self {
|
|
color, focused, sr, bpm, ppq, chunk, latency,
|
|
started: true,
|
|
global_sample: format!("{:.0}k", started.sample.get()/1000.),
|
|
global_second: format!("{:.1}s", started.usec.get()/1000.),
|
|
current_sample,
|
|
current_second,
|
|
beat: clock.timebase.format_beats_0(
|
|
clock.timebase.usecs_to_pulse(current_usec)
|
|
),
|
|
}
|
|
} else {
|
|
Self {
|
|
color, focused, sr, bpm, ppq, chunk, latency,
|
|
started: false,
|
|
global_sample: format!("{:.0}k", clock.global.sample.get()/1000.),
|
|
global_second: format!("{:.1}s", clock.global.usec.get()/1000000.),
|
|
current_sample: 0.0,
|
|
current_second: 0.0,
|
|
beat: format!("000.0.00")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
render!(<Tui>|self: TransportView|{
|
|
let color = self.color;
|
|
struct Field<'a>(&'a str, &'a str, &'a ItemPalette);
|
|
render!(<Tui>|self: Field<'a>|row!([
|
|
Tui::fg_bg(self.2.lightest.rgb, self.2.base.rgb, Tui::bold(true, self.0)),
|
|
Tui::fg_bg(self.2.base.rgb, self.2.darkest.rgb, "▌"),
|
|
Tui::fg_bg(self.2.lightest.rgb, self.2.darkest.rgb, format!("{:>10}", self.1)),
|
|
Tui::fg_bg(self.2.darkest.rgb, self.2.base.rgb, "▌"),
|
|
]));
|
|
Tui::bg(color.base.rgb, Fill::w(row!([
|
|
//PlayPause(self.started), " ",
|
|
col!([
|
|
Field(" Beat", self.beat.as_str(), &color),
|
|
Field(" Time", format!("{:.1}s", self.current_second).as_str(), &color),
|
|
]),
|
|
col!([
|
|
Field(" BPM", self.bpm.as_str(), &color),
|
|
//Field(" Smpl", format!("{:.1}k", self.current_sample).as_str(), &color),
|
|
Field(" Rate", format!("{}", self.sr).as_str(), &color),
|
|
//Field(" CPU%", format!("{:.1}ms", self.perf).as_str(), &color),
|
|
]),
|
|
col!([
|
|
Field(" Chunk", format!("{}", self.chunk).as_str(), &color),
|
|
Field(" Lag", format!("{:.3}ms", self.latency).as_str(), &color),
|
|
]),
|
|
])))
|
|
});
|
|
pub struct PlayPause(pub bool);
|
|
render!(<Tui>|self: PlayPause|Tui::bg(
|
|
if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)},
|
|
Fixed::w(5, col!(|add|if self.0 {
|
|
add(&Tui::fg(Color::Rgb(0, 255, 0), col!([
|
|
" 🭍🭑🬽 ",
|
|
" 🭞🭜🭘 ",
|
|
])))
|
|
} else {
|
|
add(&Tui::fg(Color::Rgb(255, 128, 0), col!([
|
|
" ▗▄▖ ",
|
|
" ▝▀▘ ",
|
|
])))
|
|
}))
|
|
));
|
|
impl HasFocus for TransportTui {
|
|
type Item = TransportFocus;
|
|
fn focused (&self) -> Self::Item {
|
|
self.focus
|
|
}
|
|
fn set_focused (&mut self, to: Self::Item) {
|
|
self.focus = to
|
|
}
|
|
}
|
|
/// Which item of the transport toolbar is focused
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum TransportFocus {
|
|
Bpm,
|
|
Sync,
|
|
PlayPause,
|
|
Clock,
|
|
Quant,
|
|
}
|
|
impl FocusWrap<TransportFocus> for TransportFocus {
|
|
fn wrap <'a, W: Render<Tui>> (self, focus: TransportFocus, content: &'a W)
|
|
-> impl Render<Tui> + 'a
|
|
{
|
|
let focused = focus == self;
|
|
let corners = focused.then_some(CORNERS);
|
|
//let highlight = focused.then_some(Tui::bg(Color::Rgb(60, 70, 50)));
|
|
lay!([corners, /*highlight,*/ *content])
|
|
}
|
|
}
|
|
impl FocusWrap<TransportFocus> for Option<TransportFocus> {
|
|
fn wrap <'a, W: Render<Tui>> (self, focus: TransportFocus, content: &'a W)
|
|
-> impl Render<Tui> + 'a
|
|
{
|
|
let focused = Some(focus) == self;
|
|
let corners = focused.then_some(CORNERS);
|
|
//let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
|
|
lay!([corners, /*highlight,*/ *content])
|
|
}
|
|
}
|
|
pub trait TransportControl<T>: HasClock + {
|
|
fn transport_focused (&self) -> Option<TransportFocus>;
|
|
}
|
|
impl TransportControl<TransportFocus> for TransportTui {
|
|
fn transport_focused (&self) -> Option<TransportFocus> {
|
|
Some(self.focus)
|
|
}
|
|
}
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum TransportCommand {
|
|
Focus(FocusCommand<TransportFocus>),
|
|
Clock(ClockCommand),
|
|
}
|
|
command!(|self:TransportCommand,state:TransportTui|match self {
|
|
//Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus),
|
|
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
|
|
_ => unreachable!(),
|
|
});
|
|
//command!(|self:TransportFocus,state:TransportTui|{
|
|
//if let FocusCommand::Set(to) = self { state.set_focused(to); }
|
|
//Ok(None)
|
|
//});
|
|
impl InputToCommand<Tui, TransportTui> for TransportCommand {
|
|
fn input_to_command (state: &TransportTui, input: &TuiInput) -> Option<Self> {
|
|
to_transport_command(state, input)
|
|
.or_else(||to_focus_command(input).map(TransportCommand::Focus))
|
|
}
|
|
}
|
|
pub fn to_transport_command <T, U> (state: &T, input: &TuiInput) -> Option<TransportCommand>
|
|
where
|
|
T: TransportControl<U>,
|
|
U: Into<Option<TransportFocus>>,
|
|
{
|
|
Some(match input.event() {
|
|
key_pat!(Left) => Focus(Prev),
|
|
key_pat!(Right) => Focus(Next),
|
|
key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() {
|
|
Play(None)
|
|
} else {
|
|
Pause(None)
|
|
}),
|
|
key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() {
|
|
Play(Some(0))
|
|
} else {
|
|
Pause(Some(0))
|
|
}),
|
|
_ => match state.transport_focused().unwrap() {
|
|
TransportFocus::Bpm => to_bpm_command(input, state.clock().bpm().get())?,
|
|
TransportFocus::Quant => to_quant_command(input, &state.clock().quant)?,
|
|
TransportFocus::Sync => to_sync_command(input, &state.clock().sync)?,
|
|
TransportFocus::Clock => to_seek_command(input)?,
|
|
TransportFocus::PlayPause => match input.event() {
|
|
key_pat!(Enter) => Clock(
|
|
if state.clock().is_stopped() {
|
|
Play(None)
|
|
} else {
|
|
Pause(None)
|
|
}
|
|
),
|
|
key_pat!(Shift-Enter) => Clock(
|
|
if state.clock().is_stopped() {
|
|
Play(Some(0))
|
|
} else {
|
|
Pause(Some(0))
|
|
}
|
|
),
|
|
_ => return None,
|
|
},
|
|
}
|
|
})
|
|
}
|
|
fn to_bpm_command (input: &TuiInput, bpm: f64) -> Option<TransportCommand> {
|
|
Some(match input.event() {
|
|
key_pat!(Char(',')) => Clock(SetBpm(bpm - 1.0)),
|
|
key_pat!(Char('.')) => Clock(SetBpm(bpm + 1.0)),
|
|
key_pat!(Char('<')) => Clock(SetBpm(bpm - 0.001)),
|
|
key_pat!(Char('>')) => Clock(SetBpm(bpm + 0.001)),
|
|
_ => return None,
|
|
})
|
|
}
|
|
fn to_quant_command (input: &TuiInput, quant: &Quantize) -> Option<TransportCommand> {
|
|
Some(match input.event() {
|
|
key_pat!(Char(',')) => Clock(SetQuant(quant.prev())),
|
|
key_pat!(Char('.')) => Clock(SetQuant(quant.next())),
|
|
key_pat!(Char('<')) => Clock(SetQuant(quant.prev())),
|
|
key_pat!(Char('>')) => Clock(SetQuant(quant.next())),
|
|
_ => return None,
|
|
})
|
|
}
|
|
fn to_sync_command (input: &TuiInput, sync: &LaunchSync) -> Option<TransportCommand> {
|
|
Some(match input.event() {
|
|
key_pat!(Char(',')) => Clock(SetSync(sync.prev())),
|
|
key_pat!(Char('.')) => Clock(SetSync(sync.next())),
|
|
key_pat!(Char('<')) => Clock(SetSync(sync.prev())),
|
|
key_pat!(Char('>')) => Clock(SetSync(sync.next())),
|
|
_ => return None,
|
|
})
|
|
}
|
|
fn to_seek_command (input: &TuiInput) -> Option<TransportCommand> {
|
|
Some(match input.event() {
|
|
key_pat!(Char(',')) => todo!("transport seek bar"),
|
|
key_pat!(Char('.')) => todo!("transport seek bar"),
|
|
key_pat!(Char('<')) => todo!("transport seek beat"),
|
|
key_pat!(Char('>')) => todo!("transport seek beat"),
|
|
_ => return None,
|
|
})
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
//struct Field(&'static str, String);
|
|
|
|
//render!(|self: Field|{
|
|
//Tui::to_east("│", Tui::to_east(
|
|
//Tui::bold(true, self.0),
|
|
//Tui::bg(Color::Rgb(0, 0, 0), self.1.as_str()),
|
|
//))
|
|
//});
|
|
|
|
//pub struct TransportView {
|
|
//pub(crate) state: Option<TransportState>,
|
|
//pub(crate) selected: Option<TransportFocus>,
|
|
//pub(crate) focused: bool,
|
|
//pub(crate) bpm: f64,
|
|
//pub(crate) sync: f64,
|
|
//pub(crate) quant: f64,
|
|
//pub(crate) beat: String,
|
|
//pub(crate) msu: String,
|
|
//}
|
|
////)?;
|
|
////match *state {
|
|
////Some(TransportState::Rolling) => {
|
|
////add(&row!(
|
|
////"│",
|
|
////TuiStyle::fg("▶ PLAYING", Color::Rgb(0, 255, 0)),
|
|
////format!("│0 (0)"),
|
|
////format!("│00m00s000u"),
|
|
////format!("│00B 0b 00/00")
|
|
////))?;
|
|
////add(&row!("│Now ", row!(
|
|
////format!("│0 (0)"), //sample(chunk)
|
|
////format!("│00m00s000u"), //msu
|
|
////format!("│00B 0b 00/00"), //bbt
|
|
////)))?;
|
|
////},
|
|
////_ => {
|
|
////add(&row!("│", TuiStyle::fg("⏹ STOPPED", Color::Rgb(255, 128, 0))))?;
|
|
////add(&"")?;
|
|
////}
|
|
////}
|
|
////Ok(())
|
|
////}).fill_x().bg(Color::Rgb(40, 50, 30))
|
|
////});
|
|
|
|
//impl<'a, T: HasClock> From<&'a T> for TransportView where Option<TransportFocus>: From<&'a T> {
|
|
//fn from (state: &'a T) -> Self {
|
|
//let selected = state.into();
|
|
//Self {
|
|
//selected,
|
|
//focused: selected.is_some(),
|
|
//state: Some(state.clock().transport.query_state().unwrap()),
|
|
//bpm: state.clock().bpm().get(),
|
|
//sync: state.clock().sync.get(),
|
|
//quant: state.clock().quant.get(),
|
|
//beat: state.clock().playhead.format_beat(),
|
|
//msu: state.clock().playhead.usec.format_msu(),
|
|
//}
|
|
//}
|
|
//}
|
|
|
|
//row!(
|
|
////selected.wrap(TransportFocus::PlayPause, &play_pause.fixed_xy(10, 3)),
|
|
//row!(
|
|
//col!(
|
|
//Field("SR ", format!("192000")),
|
|
//Field("BUF ", format!("1024")),
|
|
//Field("LEN ", format!("21300")),
|
|
//Field("CPU ", format!("00.0%"))
|
|
//),
|
|
//col!(
|
|
//Field("PUL ", format!("000000000")),
|
|
//Field("PPQ ", format!("96")),
|
|
//Field("BBT ", format!("00B0b00p"))
|
|
//),
|
|
//col!(
|
|
//Field("SEC ", format!("000000.000")),
|
|
//Field("BPM ", format!("000.000")),
|
|
//Field("MSU ", format!("00m00s00u"))
|
|
//),
|
|
//),
|
|
//selected.wrap(TransportFocus::Bpm, &Margin::X(1u16, {
|
|
//row! {
|
|
//"BPM ",
|
|
//format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
|
|
//}
|
|
//})),
|
|
//selected.wrap(TransportFocus::Sync, &Margin::X(1u16, row! {
|
|
//"SYNC ", pulses_to_name(*sync as usize)
|
|
//})),
|
|
//selected.wrap(TransportFocus::Quant, &Margin::X(1u16, row! {
|
|
//"QUANT ", pulses_to_name(*quant as usize)
|
|
//})),
|
|
//selected.wrap(TransportFocus::Clock, &{
|
|
//row!("B" , beat.as_str(), " T", msu.as_str()).margin_x(1)
|
|
//}).align_e().fill_x(),
|
|
//).fill_x().bg(Color::Rgb(40, 50, 30))
|