mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
328 lines
11 KiB
Rust
328 lines
11 KiB
Rust
use crate::*;
|
|
|
|
/// Create app state from JACK handle.
|
|
impl TryFrom<&Arc<RwLock<JackClient>>> for TransportApp<Tui> {
|
|
type Error = Box<dyn std::error::Error>;
|
|
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
|
Ok(Self::new(TransportView {
|
|
metronome: false,
|
|
transport: jack.read().unwrap().transport(),
|
|
jack: jack.clone(),
|
|
clock: Arc::new(Clock::from(Instant::default())),
|
|
focused: false,
|
|
focus: TransportViewFocus::PlayPause,
|
|
size: Measure::new(),
|
|
}.into(), None, None))
|
|
}
|
|
}
|
|
|
|
/// Root type of application.
|
|
pub type TransportApp<E: Engine> = AppView<
|
|
E,
|
|
TransportView<E>,
|
|
TransportAppCommand,
|
|
TransportStatusBar
|
|
>;
|
|
|
|
/// Handle input.
|
|
impl Handle<Tui> for TransportApp<Tui> {
|
|
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
|
TransportAppCommand::execute_with_state(self, from)
|
|
}
|
|
}
|
|
|
|
pub type TransportAppCommand = AppViewCommand<TransportCommand>;
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum TransportCommand {
|
|
Clock(ClockCommand),
|
|
Playhead(PlayheadCommand),
|
|
}
|
|
|
|
impl InputToCommand<Tui, TransportApp<Tui>> for TransportAppCommand {
|
|
fn input_to_command (app: &TransportApp<Tui>, input: &TuiInput) -> Option<Self> {
|
|
use KeyCode::{Left, Right};
|
|
use FocusCommand::{Prev, Next};
|
|
use AppViewCommand::{Focus, App};
|
|
Some(match input.event() {
|
|
key!(Left) => Focus(Prev),
|
|
key!(Right) => Focus(Next),
|
|
_ => TransportCommand::input_to_command(app.app, input).map(App)
|
|
})
|
|
}
|
|
}
|
|
|
|
impl InputToCommand<Tui, TransportApp<Tui>> for TransportCommand {
|
|
fn input_to_command (app: &TransportApp<Tui>, input: &TuiInput) -> Option<Self> {
|
|
use KeyCode::Char;
|
|
use AppViewFocus::Content;
|
|
use ClockCommand::{SetBpm, SetQuant, SetSync};
|
|
use TransportViewFocus::{Bpm, Quant, Sync, PlayPause, Clock};
|
|
let clock = app.app.model.clock();
|
|
Some(match input.event() {
|
|
key!(Char('.')) => match app.focused() {
|
|
Content(Bpm) => SetBpm(clock.timebase().bpm.get() + 1.0),
|
|
Content(Quant) => SetQuant(next_note_length(clock.quant.get()as usize)as f64),
|
|
Content(Sync) => SetSync(next_note_length(clock.sync.get()as usize)as f64+1.),
|
|
Content(PlayPause) => {todo!()},
|
|
Content(Clock) => {todo!()},
|
|
_ => {todo!()}
|
|
},
|
|
key!(KeyCode::Char(',')) => match app.focused() {
|
|
Content(Bpm) => SetBpm(clock.timebase().bpm.get() - 1.0),
|
|
Content(Quant) => SetQuant(prev_note_length(clock.quant.get()as usize)as f64),
|
|
Content(Sync) => SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.),
|
|
Content(PlayPause) => {todo!()},
|
|
Content(Clock) => {todo!()}
|
|
_ => {todo!()}
|
|
},
|
|
key!(KeyCode::Char('>')) => match app.focused() {
|
|
Content(Bpm) => SetBpm(clock.timebase().bpm.get() + 0.001),
|
|
Content(Quant) => SetQuant(next_note_length(clock.quant.get()as usize)as f64),
|
|
Content(Sync) => SetSync(next_note_length(clock.sync.get()as usize)as f64+1.),
|
|
Content(PlayPause) => {todo!()},
|
|
Content(Clock) => {todo!()}
|
|
_ => {todo!()}
|
|
},
|
|
key!(KeyCode::Char('<')) => match app.focused() {
|
|
Content(Bpm) => SetBpm(clock.timebase().bpm.get() - 0.001),
|
|
Content(Quant) => SetQuant(prev_note_length(clock.quant.get()as usize)as f64),
|
|
Content(Sync) => SetSync(prev_note_length(clock.sync.get()as usize)as f64+1.),
|
|
Content(PlayPause) => {todo!()},
|
|
Content(Clock) => {todo!()}
|
|
_ => {todo!()}
|
|
},
|
|
_ => return None
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Command<TransportApp<Tui>> for TransportAppCommand {
|
|
fn execute (self, state: &mut TransportApp<Tui>) -> Perhaps<Self> {
|
|
use AppViewCommand::{Focus, App};
|
|
use FocusCommand::{Next, Prev};
|
|
Ok(Some(match self {
|
|
App(command) => if let Some(undo) = TransportCommand::execute(command, state)? {
|
|
App(undo)
|
|
} else {
|
|
return Ok(None)
|
|
},
|
|
Focus(command) => Focus(match command {
|
|
Next => { todo!() },
|
|
Prev => { todo!() },
|
|
_ => { todo!() }
|
|
}),
|
|
_ => todo!()
|
|
}))
|
|
}
|
|
}
|
|
|
|
impl Command<TransportApp<Tui>> for TransportCommand {
|
|
fn execute (self, state: &mut TransportApp<Tui>) -> Perhaps<Self> {
|
|
use ClockCommand::{SetBpm, SetQuant, SetSync};
|
|
let clock = state.app.model.clock();
|
|
Ok(Some(match self {
|
|
SetBpm(bpm) => SetBpm(clock.timebase().bpm.set(bpm)),
|
|
SetQuant(quant) => SetQuant(clock.quant.set(quant)),
|
|
SetSync(sync) => SetSync(clock.sync.set(sync)),
|
|
_ => return Ok(None)
|
|
}))
|
|
}
|
|
}
|
|
|
|
/// Stores and displays time-related info.
|
|
#[derive(Debug)]
|
|
pub struct TransportView<E: Engine> {
|
|
_engine: PhantomData<E>,
|
|
jack: Arc<RwLock<JackClient>>,
|
|
/// Playback state
|
|
playing: RwLock<Option<TransportState>>,
|
|
/// Global sample and usec at which playback started
|
|
started: RwLock<Option<(usize, usize)>>,
|
|
/// Current moment in time
|
|
current: Instant,
|
|
/// Note quantization factor
|
|
quant: Quantize,
|
|
/// Launch quantization factor
|
|
sync: LaunchSync,
|
|
/// JACK transport handle.
|
|
transport: jack::Transport,
|
|
/// Enable metronome?
|
|
metronome: bool,
|
|
focus: TransportViewFocus,
|
|
focused: bool,
|
|
size: Measure<E>,
|
|
}
|
|
|
|
/// Which item of the transport toolbar is focused
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum TransportViewFocus {
|
|
Bpm,
|
|
Sync,
|
|
PlayPause,
|
|
Clock,
|
|
Quant,
|
|
}
|
|
|
|
impl TransportViewFocus {
|
|
pub fn next (&mut self) {
|
|
*self = match self {
|
|
Self::PlayPause => Self::Bpm,
|
|
Self::Bpm => Self::Quant,
|
|
Self::Quant => Self::Sync,
|
|
Self::Sync => Self::Clock,
|
|
Self::Clock => Self::PlayPause,
|
|
}
|
|
}
|
|
pub fn prev (&mut self) {
|
|
*self = match self {
|
|
Self::PlayPause => Self::Clock,
|
|
Self::Bpm => Self::PlayPause,
|
|
Self::Quant => Self::Bpm,
|
|
Self::Sync => Self::Quant,
|
|
Self::Clock => Self::Sync,
|
|
}
|
|
}
|
|
pub fn wrap <'a, W: Widget<Engine = Tui>> (
|
|
self, parent_focus: bool, focus: Self, widget: &'a W
|
|
) -> impl Widget<Engine = Tui> + 'a {
|
|
let focused = parent_focus && focus == self;
|
|
let corners = focused.then_some(CORNERS);
|
|
let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
|
|
lay!(corners, highlight, *widget)
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub struct TransportStatusBar;
|
|
|
|
impl Content for TransportView<Tui> {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
lay!(
|
|
self.focus.wrap(self.focused, TransportViewFocus::PlayPause, &Styled(
|
|
None,
|
|
match *self.model.clock().playing.read().unwrap() {
|
|
Some(TransportState::Rolling) => "▶ PLAYING",
|
|
Some(TransportState::Starting) => "READY ...",
|
|
Some(TransportState::Stopped) => "⏹ STOPPED",
|
|
_ => unreachable!(),
|
|
}
|
|
).min_xy(11, 2).push_x(1)).align_x().fill_x(),
|
|
|
|
row!(
|
|
self.focus.wrap(self.focused, TransportViewFocus::Bpm, &Outset::X(1u16, {
|
|
let bpm = self.model.clock().timebase().bpm.get();
|
|
row! { "BPM ", format!("{}.{:03}", bpm as usize, (bpm * 1000.0) % 1000.0) }
|
|
})),
|
|
//let quant = self.focus.wrap(self.focused, TransportViewFocus::Quant, &Outset::X(1u16, row! {
|
|
//"QUANT ", ppq_to_name(self.quant as usize)
|
|
//})),
|
|
self.focus.wrap(self.focused, TransportViewFocus::Sync, &Outset::X(1u16, row! {
|
|
"SYNC ", pulses_to_name(self.model.clock().sync.get() as usize)
|
|
}))
|
|
).align_w().fill_x(),
|
|
|
|
self.focus.wrap(self.focused, TransportViewFocus::Clock, &{
|
|
let time1 = self.model.clock().current.format_beat();
|
|
let time2 = self.model.clock().current.usec.format_msu();
|
|
row!("B" ,time1.as_str(), " T", time2.as_str()).outset_x(1)
|
|
}).align_e().fill_x(),
|
|
|
|
).fill_x().bg(Color::Rgb(40, 50, 30))
|
|
}
|
|
}
|
|
|
|
impl Audio for TransportView<Tui> {
|
|
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
|
self.model.process(client, scope)
|
|
}
|
|
}
|
|
|
|
impl StatusBar for TransportStatusBar {
|
|
type State = ();
|
|
fn hotkey_fg () -> Color {
|
|
TuiTheme::hotkey_fg()
|
|
}
|
|
fn update (&mut self, state: &()) {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
impl Content for TransportStatusBar {
|
|
type Engine = Tui;
|
|
fn content (&self) -> impl Widget<Engine = Tui> {
|
|
todo!();
|
|
""
|
|
}
|
|
}
|
|
|
|
impl FocusGrid for TransportApp<Tui> {
|
|
type Item = AppViewFocus<TransportViewFocus>;
|
|
fn cursor (&self) -> (usize, usize) {
|
|
self.cursor
|
|
}
|
|
fn cursor_mut (&mut self) -> &mut (usize, usize) {
|
|
&mut self.cursor
|
|
}
|
|
fn focus_enter (&mut self) {
|
|
let focused = self.focused();
|
|
if !self.entered {
|
|
self.entered = true;
|
|
// TODO
|
|
}
|
|
}
|
|
fn focus_exit (&mut self) {
|
|
if self.entered {
|
|
self.entered = false;
|
|
// TODO
|
|
}
|
|
}
|
|
fn entered (&self) -> Option<Self::Item> {
|
|
if self.entered {
|
|
Some(self.focused())
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
fn layout (&self) -> &[&[Self::Item]] {
|
|
use AppViewFocus::*;
|
|
use TransportViewFocus::*;
|
|
&[
|
|
&[Menu],
|
|
&[
|
|
Content(Bpm),
|
|
Content(Sync),
|
|
Content(PlayPause),
|
|
Content(Clock),
|
|
Content(Quant),
|
|
],
|
|
]
|
|
}
|
|
fn update_focus (&mut self) {
|
|
// TODO
|
|
}
|
|
}
|
|
|
|
impl HasJack for TransportView {
|
|
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
|
&self.jack
|
|
}
|
|
}
|
|
|
|
impl HasClock for TransportView {
|
|
fn clock (&self) -> &Arc<Clock> {
|
|
&self.clock
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for TransportView {
|
|
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), Error> {
|
|
f.debug_struct("transport")
|
|
.field("jack", &self.jack)
|
|
.field("transport", &"(JACK transport)")
|
|
.field("clock", &self.clock)
|
|
.field("metronome", &self.metronome)
|
|
.finish()
|
|
}
|
|
}
|