mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-06 11:46:41 +01:00
WorldClock/PlayClock
This commit is contained in:
parent
f5128829d6
commit
26a9efaa86
19 changed files with 1216 additions and 3039 deletions
|
|
@ -20,13 +20,13 @@ palette = { version = "0.7.6", features = [ "random" ] }
|
|||
quanta = "0.12.3"
|
||||
rand = "0.8.5"
|
||||
ratatui = { version = "0.26.3", features = [ "unstable-widget-ref", "underline-color" ] }
|
||||
suil-rs = { path = "../suil" }
|
||||
#suil-rs = { path = "../suil" }
|
||||
symphonia = { version = "0.5.4", features = [ "all" ] }
|
||||
toml = "0.8.12"
|
||||
uuid = { version = "1.10.0", features = [ "v4" ] }
|
||||
vst = "0.4.0"
|
||||
#vst = "0.4.0"
|
||||
wavers = "1.4.3"
|
||||
winit = { version = "0.30.4", features = [ "x11" ] }
|
||||
#winit = { version = "0.30.4", features = [ "x11" ] }
|
||||
|
||||
[dev-dependencies]
|
||||
#tek_app = { version = "0.1.0", path = "../tek_app" }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
|
||||
|
||||
# `tek_sequencer`
|
||||
|
||||
This crate implements a MIDI sequencer and arranger with clip launching.
|
||||
|
|
@ -44,3 +45,5 @@ in response to MIDI notes.
|
|||
---
|
||||
|
||||
# `tek_plugin`
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -160,6 +160,8 @@ impl ClockModel {
|
|||
pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> {
|
||||
self.set_chunk(scope.n_frames() as usize);
|
||||
let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
|
||||
self.global.sample.set(current_frames as f64);
|
||||
self.global.usec.set(current_usecs as f64);
|
||||
let mut started = self.started.write().unwrap();
|
||||
match self.transport.query_state()? {
|
||||
TransportState::Rolling => {
|
||||
|
|
|
|||
|
|
@ -1,32 +1,21 @@
|
|||
use crate::*;
|
||||
|
||||
mod jack_arranger; pub(crate) use app_arranger::*;
|
||||
mod jack_sequencer; pub(crate) use app_sequencer::*;
|
||||
mod jack_transport; pub(crate) use app_transport::*;
|
||||
mod engine_focus; pub(crate) use engine_focus::*;
|
||||
mod engine_input; pub(crate) use engine_input::*;
|
||||
mod engine_style; pub(crate) use engine_style::*;
|
||||
mod engine_output; pub(crate) use engine_output::*;
|
||||
|
||||
mod engine_focus; pub(crate) use engine_focus::*;
|
||||
mod engine_input; pub(crate) use engine_input::*;
|
||||
mod engine_style; pub(crate) use engine_style::*;
|
||||
mod engine_output; pub(crate) use engine_output::*;
|
||||
mod app_transport; pub(crate) use app_transport::*;
|
||||
|
||||
mod app_arranger; pub(crate) use app_arranger::*;
|
||||
mod app_sequencer; pub(crate) use app_sequencer::*;
|
||||
mod app_transport; pub(crate) use app_transport::*;
|
||||
mod app_sequencer; pub(crate) use app_sequencer::*;
|
||||
mod ctrl_sequencer; pub(crate) use ctrl_sequencer::*;
|
||||
|
||||
mod view_arranger; pub(crate) use view_arranger::*;
|
||||
mod view_sequencer; pub(crate) use view_sequencer::*;
|
||||
mod view_transport; pub(crate) use view_transport::*;
|
||||
|
||||
mod ctrl_arranger; pub(crate) use ctrl_arranger::*;
|
||||
mod ctrl_sequencer; pub(crate) use ctrl_sequencer::*;
|
||||
mod ctrl_transport; pub(crate) use ctrl_transport::*;
|
||||
|
||||
mod model_arranger; pub(crate) use model_arranger::*;
|
||||
mod app_arranger; pub(crate) use app_arranger::*;
|
||||
mod ctrl_arranger; pub(crate) use ctrl_arranger::*;
|
||||
mod model_arranger; pub(crate) use model_arranger::*;
|
||||
|
||||
////////////////////////////////////////////////////////
|
||||
|
||||
mod model_clock; pub(crate) use model_clock::*;
|
||||
|
||||
mod view_status_bar; pub(crate) use view_status_bar::*;
|
||||
|
||||
mod model_file_browser; pub(crate) use model_file_browser::*;
|
||||
|
|
@ -191,3 +180,100 @@ impl Tui {
|
|||
buffer
|
||||
}
|
||||
}
|
||||
|
||||
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, &Outset::X(1u16, {
|
||||
//row! {
|
||||
//"BPM ",
|
||||
//format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
|
||||
//}
|
||||
//})),
|
||||
//selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! {
|
||||
//"SYNC ", pulses_to_name(*sync as usize)
|
||||
//})),
|
||||
//selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! {
|
||||
//"QUANT ", pulses_to_name(*quant as usize)
|
||||
//})),
|
||||
//selected.wrap(TransportFocus::Clock, &{
|
||||
//row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1)
|
||||
//}).align_e().fill_x(),
|
||||
//).fill_x().bg(Color::Rgb(40, 50, 30))
|
||||
|
|
|
|||
|
|
@ -1,30 +1,5 @@
|
|||
use crate::*;
|
||||
|
||||
/// Root view for standalone `tek_arranger`
|
||||
pub struct ArrangerTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub phrases: PhraseListModel,
|
||||
pub tracks: Vec<ArrangerTrack>,
|
||||
pub scenes: Vec<ArrangerScene>,
|
||||
pub name: Arc<RwLock<String>>,
|
||||
pub splits: [u16;2],
|
||||
pub selected: ArrangerSelection,
|
||||
pub mode: ArrangerMode,
|
||||
pub color: ItemColor,
|
||||
pub entered: bool,
|
||||
pub size: Measure<Tui>,
|
||||
pub cursor: (usize, usize),
|
||||
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
|
||||
pub status_bar: Option<ArrangerStatus>,
|
||||
pub history: Vec<ArrangerCommand>,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub editor: PhraseEditorModel,
|
||||
pub focus: FocusState<ArrangerFocus>,
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
|
|
@ -54,6 +29,120 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
|
|||
}
|
||||
}
|
||||
|
||||
/// Root view for standalone `tek_arranger`
|
||||
pub struct ArrangerTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub phrases: PhraseListModel,
|
||||
pub tracks: Vec<ArrangerTrack>,
|
||||
pub scenes: Vec<ArrangerScene>,
|
||||
pub name: Arc<RwLock<String>>,
|
||||
pub splits: [u16;2],
|
||||
pub selected: ArrangerSelection,
|
||||
pub mode: ArrangerMode,
|
||||
pub color: ItemColor,
|
||||
pub entered: bool,
|
||||
pub size: Measure<Tui>,
|
||||
pub cursor: (usize, usize),
|
||||
pub menu_bar: Option<MenuBar<Tui, Self, ArrangerCommand>>,
|
||||
pub status_bar: Option<ArrangerStatus>,
|
||||
pub history: Vec<ArrangerCommand>,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub editor: PhraseEditorModel,
|
||||
pub focus: FocusState<ArrangerFocus>,
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
|
||||
impl JackApi for ArrangerTui {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for ArrangerTui {
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
// Start profiling cycle
|
||||
let t0 = self.perf.get_t0();
|
||||
|
||||
// Update transport clock
|
||||
if ClockAudio(self).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
|
||||
// Update MIDI sequencers
|
||||
if TracksAudio(
|
||||
&mut self.tracks,
|
||||
&mut self.note_buf,
|
||||
&mut self.midi_buf,
|
||||
Default::default(),
|
||||
).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
|
||||
// FIXME: one of these per playing track
|
||||
//self.now.set(0.);
|
||||
//if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
//let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t));
|
||||
//if let Some(Some(Some(phrase))) = phrase {
|
||||
//if let Some(track) = self.tracks().get(t) {
|
||||
//if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase {
|
||||
//let phrase = phrase.read().unwrap();
|
||||
//if *playing.read().unwrap() == *phrase {
|
||||
//let pulse = self.current().pulse.get();
|
||||
//let start = started_at.pulse.get();
|
||||
//let now = (pulse - start) % phrase.length as f64;
|
||||
//self.now.set(now);
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
// End profiling cycle
|
||||
self.perf.update(t0, scope);
|
||||
|
||||
return Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
// Layout for standalone arranger app.
|
||||
render!(|self: ArrangerTui|{
|
||||
let arranger_focused = self.arranger_focused();
|
||||
let border = Lozenge(Style::default().bg(TuiTheme::border_bg()).fg(TuiTheme::border_fg(arranger_focused)));
|
||||
col_up!([
|
||||
WorldClock::from(self),
|
||||
PlayClock::from(self),
|
||||
col!([
|
||||
Tui::fixed_y(self.splits[0], lay!([
|
||||
border.wrap(Tui::grow_y(1, Layers::new(move |add|{
|
||||
match self.mode {
|
||||
ArrangerMode::Horizontal =>
|
||||
add(&arranger_content_horizontal(self))?,
|
||||
ArrangerMode::Vertical(factor) =>
|
||||
add(&arranger_content_vertical(self, factor))?
|
||||
};
|
||||
add(&self.size)
|
||||
}))),
|
||||
self.size,
|
||||
Tui::push_x(1, Tui::fg(
|
||||
TuiTheme::title_fg(arranger_focused),
|
||||
format!("[{}] Arranger", if self.entered {
|
||||
"■"
|
||||
} else {
|
||||
" "
|
||||
})
|
||||
))
|
||||
])),
|
||||
Split::right(
|
||||
self.splits[1],
|
||||
PhraseListView::from(self),
|
||||
PhraseView::from(self),
|
||||
)
|
||||
])
|
||||
])
|
||||
});
|
||||
|
||||
impl HasClock for ArrangerTui {
|
||||
fn clock (&self) -> &ClockModel {
|
||||
&self.clock
|
||||
|
|
@ -86,6 +175,15 @@ pub enum ArrangerFocus {
|
|||
PhraseEditor,
|
||||
}
|
||||
|
||||
impl From<&ArrangerTui> for Option<TransportFocus> {
|
||||
fn from (state: &ArrangerTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
ArrangerFocus::Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_focus!(ArrangerTui ArrangerFocus [
|
||||
//&[
|
||||
//Menu,
|
||||
|
|
@ -237,3 +335,527 @@ render!(|self: ArrangerStatus|{
|
|||
Tui::bg(status_bar_bg, Tui::fill_x(Tui::to_east(mode, commands)))
|
||||
|
||||
});
|
||||
|
||||
/// Display mode of arranger
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ArrangerMode {
|
||||
/// Tracks are rows
|
||||
Horizontal,
|
||||
/// Tracks are columns
|
||||
Vertical(usize),
|
||||
}
|
||||
|
||||
/// Arranger display mode can be cycled
|
||||
impl ArrangerMode {
|
||||
/// Cycle arranger display mode
|
||||
pub fn to_next (&mut self) {
|
||||
*self = match self {
|
||||
Self::Horizontal => Self::Vertical(1),
|
||||
Self::Vertical(1) => Self::Vertical(2),
|
||||
Self::Vertical(2) => Self::Vertical(2),
|
||||
Self::Vertical(0) => Self::Horizontal,
|
||||
Self::Vertical(_) => Self::Vertical(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArrangerViewState {
|
||||
fn arranger_focused (&self) -> bool;
|
||||
}
|
||||
impl ArrangerViewState for ArrangerTui {
|
||||
fn arranger_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::Arranger
|
||||
}
|
||||
}
|
||||
|
||||
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
|
||||
let mut widths = vec![];
|
||||
let mut total = 0;
|
||||
for track in tracks.iter() {
|
||||
let width = track.width;
|
||||
widths.push((width, total));
|
||||
total += width;
|
||||
}
|
||||
widths.push((0, total));
|
||||
widths
|
||||
}
|
||||
|
||||
fn any_size <E: Engine> (_: E::Size) -> Perhaps<E::Size>{
|
||||
Ok(Some([0.into(),0.into()].into()))
|
||||
}
|
||||
|
||||
fn custom_render <F: Fn(&mut TuiOutput)->Usually<()>+Send+Sync> (render: F) -> impl Render<Tui> {
|
||||
Widget::new(|_|Ok(Some([0u16,0u16].into())), render)
|
||||
}
|
||||
|
||||
pub fn arranger_content_vertical (
|
||||
view: &ArrangerTui,
|
||||
factor: usize
|
||||
) -> impl Render<Tui> + use<'_> {
|
||||
lay!([
|
||||
Tui::at_se(Tui::fill_xy(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()),
|
||||
format!("{}x{}", view.size.w(), view.size.h()))
|
||||
))),
|
||||
Tui::bg(view.color.rgb, lay!(![
|
||||
ArrangerVerticalColumnSeparator::from(view),
|
||||
ArrangerVerticalRowSeparator::from((view, factor)),
|
||||
col!(![
|
||||
ArrangerVerticalHeader::from(view),
|
||||
ArrangerVerticalContent::from((view, factor)),
|
||||
]),
|
||||
ArrangerVerticalCursor::from((view, factor)),
|
||||
])),
|
||||
])
|
||||
}
|
||||
|
||||
struct ArrangerVerticalColumnSeparator {
|
||||
cols: Vec<(usize, usize)>,
|
||||
scenes_w: u16,
|
||||
sep_fg: Color,
|
||||
}
|
||||
impl From<&ArrangerTui> for ArrangerVerticalColumnSeparator {
|
||||
fn from (state: &ArrangerTui) -> Self {
|
||||
Self {
|
||||
cols: track_widths(state.tracks()),
|
||||
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
||||
sep_fg: TuiTheme::separator_fg(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self: ArrangerVerticalColumnSeparator|custom_render(move|to: &mut TuiOutput|{
|
||||
let style = Some(Style::default().fg(self.sep_fg));
|
||||
Ok(for x in self.cols.iter().map(|col|col.1) {
|
||||
let x = self.scenes_w + to.area().x() + x as u16;
|
||||
for y in to.area().y()..to.area().y2() {
|
||||
to.blit(&"▎", x, y, style);
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
struct ArrangerVerticalRowSeparator {
|
||||
rows: Vec<(usize, usize)>,
|
||||
sep_fg: Color,
|
||||
}
|
||||
impl From<(&ArrangerTui, usize)> for ArrangerVerticalRowSeparator {
|
||||
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
|
||||
Self {
|
||||
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
||||
sep_fg: TuiTheme::separator_fg(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render!(|self: ArrangerVerticalRowSeparator|custom_render(move|to: &mut TuiOutput|{
|
||||
Ok(for y in self.rows.iter().map(|row|row.1) {
|
||||
let y = to.area().y() + (y / PPQ) as u16 + 1;
|
||||
if y >= to.buffer.area.height { break }
|
||||
for x in to.area().x()..to.area().x2().saturating_sub(2) {
|
||||
if x < to.buffer.area.x && y < to.buffer.area.y {
|
||||
let cell = to.buffer.get_mut(x, y);
|
||||
cell.modifier = Modifier::UNDERLINED;
|
||||
cell.underline_color = self.sep_fg;
|
||||
}
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
struct ArrangerVerticalCursor {
|
||||
cols: Vec<(usize, usize)>,
|
||||
rows: Vec<(usize, usize)>,
|
||||
focused: bool,
|
||||
selected: ArrangerSelection,
|
||||
scenes_w: u16,
|
||||
header_h: u16,
|
||||
}
|
||||
impl From<(&ArrangerTui, usize)> for ArrangerVerticalCursor {
|
||||
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
|
||||
Self {
|
||||
cols: track_widths(state.tracks()),
|
||||
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
||||
focused: state.arranger_focused(),
|
||||
selected: state.selected,
|
||||
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
||||
header_h: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self: ArrangerVerticalCursor|custom_render(move|to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
let focused = self.focused;
|
||||
let selected = self.selected;
|
||||
let get_track_area = |t: usize| [
|
||||
self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(),
|
||||
self.cols[t].0 as u16, area.h(),
|
||||
];
|
||||
let get_scene_area = |s: usize| [
|
||||
area.x(), self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16,
|
||||
area.w(), (self.rows[s].0 / PPQ) as u16
|
||||
];
|
||||
let get_clip_area = |t: usize, s: usize| [
|
||||
self.scenes_w + area.x() + self.cols[t].1 as u16,
|
||||
self.header_h + area.y() + (self.rows[s].1/PPQ) as u16,
|
||||
self.cols[t].0 as u16,
|
||||
(self.rows[s].0 / PPQ) as u16
|
||||
];
|
||||
let mut track_area: Option<[u16;4]> = None;
|
||||
let mut scene_area: Option<[u16;4]> = None;
|
||||
let mut clip_area: Option<[u16;4]> = None;
|
||||
let area = match selected {
|
||||
ArrangerSelection::Mix => area,
|
||||
ArrangerSelection::Track(t) => {
|
||||
track_area = Some(get_track_area(t));
|
||||
area
|
||||
},
|
||||
ArrangerSelection::Scene(s) => {
|
||||
scene_area = Some(get_scene_area(s));
|
||||
area
|
||||
},
|
||||
ArrangerSelection::Clip(t, s) => {
|
||||
track_area = Some(get_track_area(t));
|
||||
scene_area = Some(get_scene_area(s));
|
||||
clip_area = Some(get_clip_area(t, s));
|
||||
area
|
||||
},
|
||||
};
|
||||
let bg = TuiTheme::border_bg();
|
||||
if let Some([x, y, width, height]) = track_area {
|
||||
to.fill_fg([x, y, 1, height], bg);
|
||||
to.fill_fg([x + width, y, 1, height], bg);
|
||||
}
|
||||
if let Some([_, y, _, height]) = scene_area {
|
||||
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
|
||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
|
||||
}
|
||||
Ok(if focused {
|
||||
to.render_in(if let Some(clip_area) = clip_area { clip_area }
|
||||
else if let Some(track_area) = track_area { track_area.clip_h(self.header_h) }
|
||||
else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) }
|
||||
else { area.clip_w(self.scenes_w).clip_h(self.header_h) }, &CORNERS)?
|
||||
})
|
||||
}));
|
||||
|
||||
struct ArrangerVerticalHeader<'a> {
|
||||
tracks: &'a Vec<ArrangerTrack>,
|
||||
cols: Vec<(usize, usize)>,
|
||||
focused: bool,
|
||||
selected: ArrangerSelection,
|
||||
scenes_w: u16,
|
||||
header_h: u16,
|
||||
timebase: &'a Arc<Timebase>,
|
||||
current: &'a Arc<Moment>,
|
||||
}
|
||||
impl<'a> From<&'a ArrangerTui> for ArrangerVerticalHeader<'a> {
|
||||
fn from (state: &'a ArrangerTui) -> Self {
|
||||
Self {
|
||||
tracks: &state.tracks,
|
||||
cols: track_widths(state.tracks()),
|
||||
focused: state.arranger_focused(),
|
||||
selected: state.selected,
|
||||
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
||||
header_h: 3,
|
||||
timebase: state.clock().timebase(),
|
||||
current: &state.clock().playhead,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self: ArrangerVerticalHeader<'a>|row!(
|
||||
(track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => {
|
||||
// name and width of track
|
||||
let name = track.name().read().unwrap();
|
||||
let max_w = w.saturating_sub(1).min(name.len()).max(2);
|
||||
let name = format!("▎{}", &name[0..max_w]);
|
||||
let name = Tui::bold(true, name);
|
||||
// beats elapsed
|
||||
let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
|
||||
let length = phrase.read().unwrap().length;
|
||||
let elapsed = track.player.pulses_since_start().unwrap();
|
||||
let elapsed = self.timebase.format_beats_1_short(
|
||||
(elapsed as usize % length) as f64
|
||||
);
|
||||
format!("▎+{elapsed:>}")
|
||||
} else {
|
||||
String::from("▎")
|
||||
};
|
||||
// beats until switchover
|
||||
let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{
|
||||
let target = t.pulse.get();
|
||||
let current = self.current.pulse.get();
|
||||
if target > current {
|
||||
let remaining = target - current;
|
||||
format!("▎-{:>}", self.timebase.format_beats_0_short(remaining))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}).unwrap_or(String::from("▎"));
|
||||
let timer = col!([until_next, elapsed]);
|
||||
// name of active MIDI input
|
||||
let _input = format!("▎>{}", track.player.midi_ins().get(0)
|
||||
.map(|port|port.short_name())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into()));
|
||||
// name of active MIDI output
|
||||
let _output = format!("▎<{}", track.player.midi_outs().get(0)
|
||||
.map(|port|port.short_name())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into()));
|
||||
Tui::push_x(self.scenes_w,
|
||||
Tui::bg(track.color().rgb,
|
||||
Tui::min_xy(w as u16, self.header_h,
|
||||
col!([name, timer]))))
|
||||
}
|
||||
));
|
||||
|
||||
struct ArrangerVerticalContent<'a> {
|
||||
size: &'a Measure<Tui>,
|
||||
scenes: &'a Vec<ArrangerScene>,
|
||||
tracks: &'a Vec<ArrangerTrack>,
|
||||
rows: Vec<(usize, usize)>,
|
||||
cols: Vec<(usize, usize)>,
|
||||
header_h: u16,
|
||||
}
|
||||
impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVerticalContent<'a> {
|
||||
fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self {
|
||||
Self {
|
||||
size: &state.size,
|
||||
scenes: &state.scenes,
|
||||
tracks: &state.tracks,
|
||||
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
||||
cols: track_widths(state.tracks()),
|
||||
header_h: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self: ArrangerVerticalContent<'a>|Tui::fixed_y(
|
||||
(self.size.h() as u16).saturating_sub(self.header_h),
|
||||
col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => {
|
||||
let height = 1.max((pulses / PPQ) as u16);
|
||||
let playing = scene.is_playing(self.tracks);
|
||||
Tui::fixed_y(
|
||||
height,
|
||||
Tui::to_east(
|
||||
Tui::to_east(
|
||||
if playing { "▶ " } else { " " },
|
||||
Tui::bold(true, scene.name.read().unwrap().as_str())
|
||||
),
|
||||
row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => {
|
||||
Tui::fixed_xy(w as u16, height, Layers::new(move |add|{
|
||||
let mut bg = TuiTheme::border_bg();
|
||||
match (self.tracks.get(track), scene.clips.get(track)) {
|
||||
(Some(track), Some(Some(phrase))) => {
|
||||
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
|
||||
let name = format!("{}", name);
|
||||
let max_w = name.len().min((w as usize).saturating_sub(2));
|
||||
let color = phrase.read().unwrap().color;
|
||||
bg = color.dark.rgb;
|
||||
if let Some((_, Some(ref playing))) = track.player.play_phrase() {
|
||||
if *playing.read().unwrap() == *phrase.read().unwrap() {
|
||||
bg = color.light.rgb
|
||||
}
|
||||
};
|
||||
add(&Tui::fixed_x(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?;
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
//add(&Background(bg))
|
||||
Ok(())
|
||||
}))
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
));
|
||||
|
||||
pub fn arranger_content_horizontal (
|
||||
view: &ArrangerTui,
|
||||
) -> impl Render<Tui> + use<'_> {
|
||||
todo!()
|
||||
}
|
||||
//let focused = view.arranger_focused();
|
||||
//let _tracks = view.tracks();
|
||||
//lay!(
|
||||
//focused.then_some(Background(TuiTheme::border_bg())),
|
||||
//row!(
|
||||
//// name
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks, selected) = self;
|
||||
////let yellow = Some(Style::default().yellow().bold().not_dim());
|
||||
////let white = Some(Style::default().white().bold().not_dim());
|
||||
////let area = to.area();
|
||||
////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
|
||||
////let offset = 0; // track scroll offset
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2 + offset;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////let selected = selected.track() == Some(index);
|
||||
////let style = if selected { yellow } else { white };
|
||||
////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
|
||||
////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// monitor
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let on = Some(Style::default().not_dim().green().bold());
|
||||
////let off = Some(DIM);
|
||||
////area.x += 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////let style = if track.monitoring { on } else { off };
|
||||
////to.blit(&" MON ", area.x(), area.y() + y, style)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// record
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let on = Some(Style::default().not_dim().red().bold());
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x += 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////let style = if track.recording { on } else { off };
|
||||
////to.blit(&" REC ", area.x(), area.y() + y, style)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// overdub
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let on = Some(Style::default().not_dim().yellow().bold());
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x = area.x + 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
|
||||
////on
|
||||
////} else {
|
||||
////off
|
||||
////})?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// erase
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x = area.x + 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(_) = tracks.get(index) {
|
||||
////to.blit(&" DEL ", area.x(), area.y() + y, off)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// gain
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x = area.x() + 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(_) = tracks.get(index) {
|
||||
////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 7;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// scenes
|
||||
//Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{
|
||||
//let [x, y, _, height] = to.area();
|
||||
//let mut x2 = 0;
|
||||
//Ok(for (scene_index, scene) in view.scenes().iter().enumerate() {
|
||||
//let active_scene = view.selected.scene() == Some(scene_index);
|
||||
//let sep = Some(if active_scene {
|
||||
//Style::default().yellow().not_dim()
|
||||
//} else {
|
||||
//Style::default().dim()
|
||||
//});
|
||||
//for y in y+1..y+height {
|
||||
//to.blit(&"│", x + x2, y, sep);
|
||||
//}
|
||||
//let name = scene.name.read().unwrap();
|
||||
//let mut x3 = name.len() as u16;
|
||||
//to.blit(&*name, x + x2, y, sep);
|
||||
//for (i, clip) in scene.clips.iter().enumerate() {
|
||||
//let active_track = view.selected.track() == Some(i);
|
||||
//if let Some(clip) = clip {
|
||||
//let y2 = y + 2 + i as u16 * 2;
|
||||
//let label = format!("{}", clip.read().unwrap().name);
|
||||
//to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
|
||||
//Style::default().not_dim().yellow().bold()
|
||||
//} else {
|
||||
//Style::default().not_dim()
|
||||
//}));
|
||||
//x3 = x3.max(label.len() as u16)
|
||||
//}
|
||||
//}
|
||||
//x2 = x2 + x3 + 1;
|
||||
//})
|
||||
//}),
|
||||
//)
|
||||
//)
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -2,23 +2,7 @@ use crate::*;
|
|||
use SequencerFocus::*;
|
||||
use super::app_transport::TransportFocus::*;
|
||||
|
||||
/// Root view for standalone `tek_sequencer`.
|
||||
pub struct SequencerTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub phrases: PhraseListModel,
|
||||
pub player: PhrasePlayerModel,
|
||||
pub editor: PhraseEditorModel,
|
||||
pub size: Measure<Tui>,
|
||||
pub cursor: (usize, usize),
|
||||
pub split: u16,
|
||||
pub entered: bool,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub focus: FocusState<SequencerFocus>,
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
|
||||
/// Create app state from JACK handle.
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
|
||||
|
|
@ -41,6 +25,93 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for SequencerTui {
|
|||
}
|
||||
}
|
||||
|
||||
/// Root view for standalone `tek_sequencer`.
|
||||
pub struct SequencerTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub phrases: PhraseListModel,
|
||||
pub player: PhrasePlayerModel,
|
||||
pub editor: PhraseEditorModel,
|
||||
pub size: Measure<Tui>,
|
||||
pub cursor: (usize, usize),
|
||||
pub split: u16,
|
||||
pub entered: bool,
|
||||
pub note_buf: Vec<u8>,
|
||||
pub midi_buf: Vec<Vec<Vec<u8>>>,
|
||||
pub focus: FocusState<SequencerFocus>,
|
||||
pub perf: PerfModel,
|
||||
}
|
||||
|
||||
impl JackApi for SequencerTui {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for SequencerTui {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
// Start profiling cycle
|
||||
let t0 = self.perf.get_t0();
|
||||
|
||||
// Update transport clock
|
||||
if ClockAudio(self).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
|
||||
// Update MIDI sequencer
|
||||
if PlayerAudio(
|
||||
&mut self.player,
|
||||
&mut self.note_buf,
|
||||
&mut self.midi_buf,
|
||||
).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
|
||||
// Update sequencer playhead indicator
|
||||
//self.now().set(0.);
|
||||
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
|
||||
//let phrase = phrase.read().unwrap();
|
||||
//if *playing.read().unwrap() == *phrase {
|
||||
//let pulse = self.current().pulse.get();
|
||||
//let start = started_at.pulse.get();
|
||||
//let now = (pulse - start) % phrase.length as f64;
|
||||
//self.now().set(now);
|
||||
//}
|
||||
//}
|
||||
|
||||
// End profiling cycle
|
||||
self.perf.update(t0, scope);
|
||||
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
||||
render!(|self: SequencerTui|{
|
||||
//col_up!([
|
||||
//Tui::max_y(2, SequencerStatusBar::from(self)),
|
||||
col!([
|
||||
WorldClock::from(self),
|
||||
PlayClock::from(self),
|
||||
Tui::min_y(20, row!([
|
||||
Tui::min_x(20, col!([
|
||||
Tui::fixed_y(4, PhraseSelector::play_phrase(
|
||||
&self.player,
|
||||
self.focused() == SequencerFocus::PhrasePlay,
|
||||
self.entered()
|
||||
)),
|
||||
Tui::fixed_y(4, PhraseSelector::next_phrase(
|
||||
&self.player,
|
||||
self.focused() == SequencerFocus::PhraseNext,
|
||||
self.entered()
|
||||
)),
|
||||
PhraseListView::from(self)
|
||||
])),
|
||||
PhraseView::from(self)
|
||||
]))
|
||||
])
|
||||
//])
|
||||
});
|
||||
|
||||
impl HasClock for SequencerTui {
|
||||
fn clock (&self) -> &ClockModel {
|
||||
&self.clock
|
||||
|
|
@ -70,6 +141,15 @@ pub enum SequencerFocus {
|
|||
PhraseNext,
|
||||
}
|
||||
|
||||
impl From<&SequencerTui> for Option<TransportFocus> {
|
||||
fn from (state: &SequencerTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
SequencerFocus::Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_focus!(SequencerTui SequencerFocus [
|
||||
//&[
|
||||
//Menu,
|
||||
|
|
@ -209,3 +289,73 @@ impl From<&SequencerTui> for SequencerStatusBar {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
render!(|self: SequencerStatusBar|{
|
||||
|
||||
lay!(|add|if self.width > 60 {
|
||||
add(&row!(![
|
||||
SequencerMode::from(self),
|
||||
SequencerStats::from(self),
|
||||
]))
|
||||
} else if self.width > 0 {
|
||||
add(&col!(![
|
||||
SequencerMode::from(self),
|
||||
SequencerStats::from(self),
|
||||
]))
|
||||
} else {
|
||||
Ok(())
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
struct SequencerMode {
|
||||
mode: &'static str,
|
||||
help: &'static [(&'static str, &'static str, &'static str)]
|
||||
}
|
||||
impl From<&SequencerStatusBar> for SequencerMode {
|
||||
fn from (state: &SequencerStatusBar) -> Self {
|
||||
Self {
|
||||
mode: state.mode,
|
||||
help: state.help,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self:SequencerMode|{
|
||||
let orange = Color::Rgb(255,128,0);
|
||||
let light = Color::Rgb(100,100,100);
|
||||
let yellow = Color::Rgb(255,255,0);
|
||||
let black = Color::Rgb(0,0,0);
|
||||
row!([
|
||||
Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))),
|
||||
Tui::bg(light, row!((prefix, hotkey, suffix) in self.help.iter() => {
|
||||
row!([" ", prefix, Tui::fg(yellow, *hotkey), suffix])
|
||||
}))
|
||||
])
|
||||
});
|
||||
|
||||
struct SequencerStats<'a> {
|
||||
cpu: &'a Option<String>,
|
||||
size: &'a String,
|
||||
res: &'a String,
|
||||
}
|
||||
impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> {
|
||||
fn from (state: &'a SequencerStatusBar) -> Self {
|
||||
Self {
|
||||
cpu: &state.cpu,
|
||||
size: &state.size,
|
||||
res: &state.res,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self:SequencerStats<'a>|{
|
||||
let orange = Color::Rgb(255,128,0);
|
||||
let dark = Color::Rgb(100,100,100);
|
||||
let cpu = &self.cpu;
|
||||
let res = &self.res;
|
||||
let size = &self.size;
|
||||
Tui::bg(dark, row!([
|
||||
Tui::fg(orange, cpu),
|
||||
Tui::fg(orange, res),
|
||||
Tui::fg(orange, size),
|
||||
]))
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,7 @@
|
|||
use crate::*;
|
||||
|
||||
/// Stores and displays time-related info.
|
||||
pub struct TransportTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub size: Measure<Tui>,
|
||||
pub cursor: (usize, usize),
|
||||
pub focus: FocusState<TransportFocus>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
use crate::api::ClockCommand::{SetBpm, SetQuant, SetSync};
|
||||
use TransportCommand::{Focus, Clock};
|
||||
use KeyCode::{Enter, Left, Right, Char};
|
||||
|
||||
/// Create app state from JACK handle.
|
||||
impl TryFrom<&Arc<RwLock<JackClient>>> for TransportTui {
|
||||
|
|
@ -33,6 +17,121 @@ impl TryFrom<&Arc<RwLock<JackClient>>> for TransportTui {
|
|||
}
|
||||
}
|
||||
|
||||
/// Stores and displays time-related info.
|
||||
pub struct TransportTui {
|
||||
pub jack: Arc<RwLock<JackClient>>,
|
||||
pub clock: ClockModel,
|
||||
pub size: Measure<Tui>,
|
||||
pub cursor: (usize, usize),
|
||||
pub focus: FocusState<TransportFocus>,
|
||||
}
|
||||
|
||||
impl JackApi for TransportTui {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for TransportTui {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
ClockAudio(self).process(client, scope)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WorldClock {
|
||||
rate: String,
|
||||
sample: String,
|
||||
second: String,
|
||||
}
|
||||
|
||||
impl<T: HasClock> From<&T> for WorldClock {
|
||||
fn from (state: &T) -> Self {
|
||||
let clock = state.clock();
|
||||
let rate = format!("{}", clock.timebase.sr.get());
|
||||
if let Some(started) = clock.started.read().unwrap().as_ref() {
|
||||
Self {
|
||||
rate,
|
||||
sample: format!("{:.0}", started.sample.get()),
|
||||
second: format!("{:.0}", started.usec.get()),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
rate,
|
||||
sample: format!("{:.0}", clock.global.sample.get()),
|
||||
second: format!("{:.0}", clock.global.usec.get()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render!(|self: WorldClock|row!([
|
||||
col!(["Rate", self.rate ]), " ",
|
||||
col!(["Sample", self.sample]), " ",
|
||||
col!(["Second", self.second]), " ",
|
||||
]));
|
||||
|
||||
pub struct PlayClock {
|
||||
started: bool,
|
||||
sample: String,
|
||||
second: String,
|
||||
}
|
||||
impl<T: HasClock> From<&T> for PlayClock {
|
||||
fn from (state: &T) -> Self {
|
||||
let clock = state.clock();
|
||||
if let Some(started) = clock.started.read().unwrap().as_ref() {
|
||||
Self {
|
||||
started: true,
|
||||
sample: format!("{:.0}", clock.global.sample.get() - started.sample.get()),
|
||||
second: format!("{:.0}", clock.global.usec.get() - started.usec.get()),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
started: false,
|
||||
sample: "".to_string(),
|
||||
second: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self: PlayClock|lay!(|add|{
|
||||
if self.started {
|
||||
add(&row!([
|
||||
col!(["", Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING ")]),
|
||||
" ",
|
||||
col!(["Sample", self.sample]),
|
||||
" ",
|
||||
col!(["Second", self.second]),
|
||||
" ",
|
||||
col!(["Beat", "00B 0b 00/00"]),
|
||||
]))
|
||||
} else {
|
||||
add(&col!([Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED "), ""]))
|
||||
}
|
||||
}));
|
||||
|
||||
render!(|self: TransportTui|{
|
||||
let bg = TuiTheme::border_bg();
|
||||
let border_style = Style::default().bg(bg).fg(TuiTheme::border_fg(false));
|
||||
lay!([
|
||||
Tui::fill_x(Lozenge(border_style)),
|
||||
Tui::bg(bg, Tui::outset_xy(1, 1, row!([
|
||||
WorldClock::from(self),
|
||||
" ",
|
||||
PlayClock::from(self),
|
||||
])))
|
||||
])//Tui::to_south(world, timer)))
|
||||
});
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
impl HasClock for TransportTui {
|
||||
fn clock (&self) -> &ClockModel {
|
||||
&self.clock
|
||||
|
|
@ -49,6 +148,12 @@ pub enum TransportFocus {
|
|||
Quant,
|
||||
}
|
||||
|
||||
impl From<&TransportTui> for Option<TransportFocus> {
|
||||
fn from (state: &TransportTui) -> Self {
|
||||
Some(state.focus.inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusWrap<TransportFocus> for TransportFocus {
|
||||
fn wrap <'a, W: Render<Tui>> (self, focus: TransportFocus, content: &'a W)
|
||||
-> impl Render<Tui> + 'a
|
||||
|
|
@ -95,6 +200,127 @@ impl StatusBar for TransportStatusBar {
|
|||
}
|
||||
}
|
||||
|
||||
render!(|self: TransportStatusBar|{
|
||||
"todo"
|
||||
});
|
||||
render!(|self: TransportStatusBar|"todo");
|
||||
|
||||
impl Handle<Tui> for TransportTui {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
TransportCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum TransportCommand {
|
||||
Focus(FocusCommand),
|
||||
Clock(ClockCommand),
|
||||
}
|
||||
|
||||
impl<T: TransportControl> Command<T> for TransportCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
Ok(match self {
|
||||
Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus),
|
||||
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TransportControl: HasClock + FocusGrid + HasEnter {
|
||||
fn transport_focused (&self) -> Option<TransportFocus>;
|
||||
}
|
||||
|
||||
impl TransportControl for TransportTui {
|
||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
||||
Some(self.focus.inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for SequencerTui {
|
||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
||||
match self.focus.inner() {
|
||||
SequencerFocus::Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for ArrangerTui {
|
||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
||||
match self.focus.inner() {
|
||||
ArrangerFocus::Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
||||
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
||||
to_transport_command(state, input)
|
||||
.or_else(||to_focus_command(input).map(TransportCommand::Focus))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_transport_command <T> (state: &T, input: &TuiInput) -> Option<TransportCommand>
|
||||
where
|
||||
T: TransportControl
|
||||
{
|
||||
Some(match input.event() {
|
||||
key!(Left) => Focus(FocusCommand::Prev),
|
||||
key!(Right) => Focus(FocusCommand::Next),
|
||||
key!(Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
ClockCommand::Play(None)
|
||||
} else {
|
||||
ClockCommand::Pause(None)
|
||||
}),
|
||||
key!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
ClockCommand::Play(Some(0))
|
||||
} else {
|
||||
ClockCommand::Pause(Some(0))
|
||||
}),
|
||||
_ => match state.transport_focused().unwrap() {
|
||||
TransportFocus::Bpm => match input.event() {
|
||||
key!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)),
|
||||
key!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)),
|
||||
key!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)),
|
||||
key!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::Quant => match input.event() {
|
||||
key!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())),
|
||||
key!(Char('.')) => Clock(SetQuant(state.clock().quant.next())),
|
||||
key!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())),
|
||||
key!(Char('>')) => Clock(SetQuant(state.clock().quant.next())),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::Sync => match input.event() {
|
||||
key!(Char(',')) => Clock(SetSync(state.clock().sync.prev())),
|
||||
key!(Char('.')) => Clock(SetSync(state.clock().sync.next())),
|
||||
key!(Char('<')) => Clock(SetSync(state.clock().sync.prev())),
|
||||
key!(Char('>')) => Clock(SetSync(state.clock().sync.next())),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::Clock => match input.event() {
|
||||
key!(Char(',')) => todo!("transport seek bar"),
|
||||
key!(Char('.')) => todo!("transport seek bar"),
|
||||
key!(Char('<')) => todo!("transport seek beat"),
|
||||
key!(Char('>')) => todo!("transport seek beat"),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::PlayPause => match input.event() {
|
||||
key!(Enter) => Clock(
|
||||
if state.clock().is_stopped() {
|
||||
ClockCommand::Play(None)
|
||||
} else {
|
||||
ClockCommand::Pause(None)
|
||||
}
|
||||
),
|
||||
key!(Shift-Enter) => Clock(
|
||||
if state.clock().is_stopped() {
|
||||
ClockCommand::Play(Some(0))
|
||||
} else {
|
||||
ClockCommand::Pause(Some(0))
|
||||
}
|
||||
),
|
||||
_ => return None,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use crate::{*, api::ClockCommand::{Play, Pause}};
|
|||
use super::ctrl_phrase_editor::PhraseCommand::Show;
|
||||
use KeyCode::{Char, Enter};
|
||||
use SequencerCommand::*;
|
||||
use super::app_transport::TransportCommand;
|
||||
|
||||
impl Handle<Tui> for SequencerTui {
|
||||
fn handle (&mut self, i: &TuiInput) -> Perhaps<bool> {
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
use crate::*;
|
||||
use crate::api::ClockCommand::{SetBpm, SetQuant, SetSync};
|
||||
use TransportCommand::{Focus, Clock};
|
||||
use KeyCode::{Enter, Left, Right, Char};
|
||||
|
||||
impl Handle<Tui> for TransportTui {
|
||||
fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
|
||||
TransportCommand::execute_with_state(self, from)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum TransportCommand {
|
||||
Focus(FocusCommand),
|
||||
Clock(ClockCommand),
|
||||
}
|
||||
|
||||
impl<T: TransportControl> Command<T> for TransportCommand {
|
||||
fn execute (self, state: &mut T) -> Perhaps<Self> {
|
||||
Ok(match self {
|
||||
Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus),
|
||||
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TransportControl: HasClock + FocusGrid + HasEnter {
|
||||
fn transport_focused (&self) -> Option<TransportFocus>;
|
||||
}
|
||||
|
||||
impl TransportControl for TransportTui {
|
||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
||||
Some(self.focus.inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for SequencerTui {
|
||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
||||
match self.focus.inner() {
|
||||
SequencerFocus::Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TransportControl for ArrangerTui {
|
||||
fn transport_focused (&self) -> Option<TransportFocus> {
|
||||
match self.focus.inner() {
|
||||
ArrangerFocus::Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TransportControl> InputToCommand<Tui, T> for TransportCommand {
|
||||
fn input_to_command (state: &T, input: &TuiInput) -> Option<Self> {
|
||||
to_transport_command(state, input)
|
||||
.or_else(||to_focus_command(input).map(TransportCommand::Focus))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_transport_command <T> (state: &T, input: &TuiInput) -> Option<TransportCommand>
|
||||
where
|
||||
T: TransportControl
|
||||
{
|
||||
Some(match input.event() {
|
||||
key!(Left) => Focus(FocusCommand::Prev),
|
||||
key!(Right) => Focus(FocusCommand::Next),
|
||||
key!(Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
ClockCommand::Play(None)
|
||||
} else {
|
||||
ClockCommand::Pause(None)
|
||||
}),
|
||||
key!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() {
|
||||
ClockCommand::Play(Some(0))
|
||||
} else {
|
||||
ClockCommand::Pause(Some(0))
|
||||
}),
|
||||
_ => match state.transport_focused().unwrap() {
|
||||
TransportFocus::Bpm => match input.event() {
|
||||
key!(Char(',')) => Clock(SetBpm(state.clock().bpm().get() - 1.0)),
|
||||
key!(Char('.')) => Clock(SetBpm(state.clock().bpm().get() + 1.0)),
|
||||
key!(Char('<')) => Clock(SetBpm(state.clock().bpm().get() - 0.001)),
|
||||
key!(Char('>')) => Clock(SetBpm(state.clock().bpm().get() + 0.001)),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::Quant => match input.event() {
|
||||
key!(Char(',')) => Clock(SetQuant(state.clock().quant.prev())),
|
||||
key!(Char('.')) => Clock(SetQuant(state.clock().quant.next())),
|
||||
key!(Char('<')) => Clock(SetQuant(state.clock().quant.prev())),
|
||||
key!(Char('>')) => Clock(SetQuant(state.clock().quant.next())),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::Sync => match input.event() {
|
||||
key!(Char(',')) => Clock(SetSync(state.clock().sync.prev())),
|
||||
key!(Char('.')) => Clock(SetSync(state.clock().sync.next())),
|
||||
key!(Char('<')) => Clock(SetSync(state.clock().sync.prev())),
|
||||
key!(Char('>')) => Clock(SetSync(state.clock().sync.next())),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::Clock => match input.event() {
|
||||
key!(Char(',')) => todo!("transport seek bar"),
|
||||
key!(Char('.')) => todo!("transport seek bar"),
|
||||
key!(Char('<')) => todo!("transport seek beat"),
|
||||
key!(Char('>')) => todo!("transport seek beat"),
|
||||
_ => return None,
|
||||
},
|
||||
TransportFocus::PlayPause => match input.event() {
|
||||
key!(Enter) => Clock(
|
||||
if state.clock().is_stopped() {
|
||||
ClockCommand::Play(None)
|
||||
} else {
|
||||
ClockCommand::Pause(None)
|
||||
}
|
||||
),
|
||||
key!(Shift-Enter) => Clock(
|
||||
if state.clock().is_stopped() {
|
||||
ClockCommand::Play(Some(0))
|
||||
} else {
|
||||
ClockCommand::Pause(Some(0))
|
||||
}
|
||||
),
|
||||
_ => return None,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -18,28 +18,43 @@ impl Tui {
|
|||
pub struct Bold<W: Render<Tui>>(pub bool, W);
|
||||
|
||||
impl<W: Render<Tui>> Render<Tui> for Bold<W> {
|
||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_bold(to.area(), self.0)) }
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
self.1.min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
to.fill_bold(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Foreground<W: Render<Tui>>(pub Color, W);
|
||||
|
||||
impl<W: Render<Tui>> Render<Tui> for Foreground<W> {
|
||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_fg(to.area(), self.0)) }
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
self.1.min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
to.fill_fg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Background<W: Render<Tui>>(pub Color, W);
|
||||
|
||||
impl<W: Render<Tui>> Render<Tui> for Background<W> {
|
||||
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> { Ok(Some([0,0])) }
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> { Ok(to.fill_bg(to.area(), self.0)) }
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
self.1.min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
to.fill_bg(to.area(), self.0);
|
||||
self.1.render(to)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Bordered<S: BorderStyle, W: Render<Tui>>(pub S, pub W);
|
||||
|
||||
render!(|self: Bordered<S: BorderStyle, W: Render<Tui>>|{
|
||||
Tui::fill_xy(Tui::under(Tui::inset_xy(1, 1, widget(&self.1)), Border(self.0)))
|
||||
Tui::fill_xy(lay!([Border(self.0), Tui::inset_xy(1, 1, widget(&self.1))]))
|
||||
});
|
||||
|
||||
pub struct Border<S: BorderStyle>(pub S);
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
use crate::{*, tui::ArrangerTui};
|
||||
|
||||
impl JackApi for ArrangerTui {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for ArrangerTui {
|
||||
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
// Start profiling cycle
|
||||
let t0 = self.perf.get_t0();
|
||||
|
||||
// Update transport clock
|
||||
if ClockAudio(self).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
|
||||
// Update MIDI sequencers
|
||||
if TracksAudio(
|
||||
&mut self.tracks,
|
||||
&mut self.note_buf,
|
||||
&mut self.midi_buf,
|
||||
Default::default(),
|
||||
).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
|
||||
// FIXME: one of these per playing track
|
||||
//self.now.set(0.);
|
||||
//if let ArrangerSelection::Clip(t, s) = self.selected {
|
||||
//let phrase = self.scenes().get(s).map(|scene|scene.clips.get(t));
|
||||
//if let Some(Some(Some(phrase))) = phrase {
|
||||
//if let Some(track) = self.tracks().get(t) {
|
||||
//if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase {
|
||||
//let phrase = phrase.read().unwrap();
|
||||
//if *playing.read().unwrap() == *phrase {
|
||||
//let pulse = self.current().pulse.get();
|
||||
//let start = started_at.pulse.get();
|
||||
//let now = (pulse - start) % phrase.length as f64;
|
||||
//self.now.set(now);
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
//}
|
||||
|
||||
// End profiling cycle
|
||||
self.perf.update(t0, scope);
|
||||
|
||||
return Control::Continue
|
||||
}
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
use crate::{*, tui::SequencerTui};
|
||||
|
||||
impl JackApi for SequencerTui {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for SequencerTui {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
// Start profiling cycle
|
||||
let t0 = self.perf.get_t0();
|
||||
|
||||
// Update transport clock
|
||||
if ClockAudio(self).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
|
||||
// Update MIDI sequencer
|
||||
if PlayerAudio(
|
||||
&mut self.player,
|
||||
&mut self.note_buf,
|
||||
&mut self.midi_buf,
|
||||
).process(client, scope) == Control::Quit {
|
||||
return Control::Quit
|
||||
}
|
||||
|
||||
// Update sequencer playhead indicator
|
||||
//self.now().set(0.);
|
||||
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
|
||||
//let phrase = phrase.read().unwrap();
|
||||
//if *playing.read().unwrap() == *phrase {
|
||||
//let pulse = self.current().pulse.get();
|
||||
//let start = started_at.pulse.get();
|
||||
//let now = (pulse - start) % phrase.length as f64;
|
||||
//self.now().set(now);
|
||||
//}
|
||||
//}
|
||||
|
||||
// End profiling cycle
|
||||
self.perf.update(t0, scope);
|
||||
|
||||
Control::Continue
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
impl JackApi for TransportTui {
|
||||
fn jack (&self) -> &Arc<RwLock<JackClient>> {
|
||||
&self.jack
|
||||
}
|
||||
}
|
||||
|
||||
impl Audio for TransportTui {
|
||||
fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
|
||||
ClockAudio(self).process(client, scope)
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
use crate::*;
|
||||
|
|
@ -1,561 +0,0 @@
|
|||
use crate::*;
|
||||
|
||||
// Layout for standalone arranger app.
|
||||
render!(|self: ArrangerTui|{
|
||||
let arranger_focused = self.arranger_focused();
|
||||
let border = Lozenge(Style::default().bg(TuiTheme::border_bg()).fg(TuiTheme::border_fg(arranger_focused)));
|
||||
col_up!([
|
||||
TransportView::from(self),
|
||||
col!([
|
||||
Tui::fixed_y(self.splits[0], lay!([
|
||||
border.wrap(Tui::grow_y(1, Layers::new(move |add|{
|
||||
match self.mode {
|
||||
ArrangerMode::Horizontal =>
|
||||
add(&arranger_content_horizontal(self))?,
|
||||
ArrangerMode::Vertical(factor) =>
|
||||
add(&arranger_content_vertical(self, factor))?
|
||||
};
|
||||
add(&self.size)
|
||||
}))),
|
||||
self.size,
|
||||
Tui::push_x(1, Tui::fg(
|
||||
TuiTheme::title_fg(arranger_focused),
|
||||
format!("[{}] Arranger", if self.entered {
|
||||
"■"
|
||||
} else {
|
||||
" "
|
||||
})
|
||||
))
|
||||
])),
|
||||
Split::right(
|
||||
self.splits[1],
|
||||
PhraseListView::from(self),
|
||||
PhraseView::from(self),
|
||||
)
|
||||
])
|
||||
])
|
||||
});
|
||||
|
||||
/// Display mode of arranger
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum ArrangerMode {
|
||||
/// Tracks are rows
|
||||
Horizontal,
|
||||
/// Tracks are columns
|
||||
Vertical(usize),
|
||||
}
|
||||
|
||||
/// Arranger display mode can be cycled
|
||||
impl ArrangerMode {
|
||||
/// Cycle arranger display mode
|
||||
pub fn to_next (&mut self) {
|
||||
*self = match self {
|
||||
Self::Horizontal => Self::Vertical(1),
|
||||
Self::Vertical(1) => Self::Vertical(2),
|
||||
Self::Vertical(2) => Self::Vertical(2),
|
||||
Self::Vertical(0) => Self::Horizontal,
|
||||
Self::Vertical(_) => Self::Vertical(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ArrangerViewState {
|
||||
fn arranger_focused (&self) -> bool;
|
||||
}
|
||||
impl ArrangerViewState for ArrangerTui {
|
||||
fn arranger_focused (&self) -> bool {
|
||||
self.focused() == ArrangerFocus::Arranger
|
||||
}
|
||||
}
|
||||
|
||||
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
|
||||
let mut widths = vec![];
|
||||
let mut total = 0;
|
||||
for track in tracks.iter() {
|
||||
let width = track.width;
|
||||
widths.push((width, total));
|
||||
total += width;
|
||||
}
|
||||
widths.push((0, total));
|
||||
widths
|
||||
}
|
||||
|
||||
fn any_size <E: Engine> (_: E::Size) -> Perhaps<E::Size>{
|
||||
Ok(Some([0.into(),0.into()].into()))
|
||||
}
|
||||
|
||||
fn custom_render <F: Fn(&mut TuiOutput)->Usually<()>+Send+Sync> (render: F) -> impl Render<Tui> {
|
||||
Widget::new(|_|Ok(Some([0u16,0u16].into())), render)
|
||||
}
|
||||
|
||||
pub fn arranger_content_vertical (
|
||||
view: &ArrangerTui,
|
||||
factor: usize
|
||||
) -> impl Render<Tui> + use<'_> {
|
||||
lay!([
|
||||
Tui::at_se(Tui::fill_xy(Tui::pull_x(1, Tui::fg(TuiTheme::title_fg(view.arranger_focused()),
|
||||
format!("{}x{}", view.size.w(), view.size.h()))
|
||||
))),
|
||||
Tui::bg(view.color.rgb, lay!(![
|
||||
ArrangerVerticalColumnSeparator::from(view),
|
||||
ArrangerVerticalRowSeparator::from((view, factor)),
|
||||
col!(![
|
||||
ArrangerVerticalHeader::from(view),
|
||||
ArrangerVerticalContent::from((view, factor)),
|
||||
]),
|
||||
ArrangerVerticalCursor::from((view, factor)),
|
||||
])),
|
||||
])
|
||||
}
|
||||
|
||||
struct ArrangerVerticalColumnSeparator {
|
||||
cols: Vec<(usize, usize)>,
|
||||
scenes_w: u16,
|
||||
sep_fg: Color,
|
||||
}
|
||||
impl From<&ArrangerTui> for ArrangerVerticalColumnSeparator {
|
||||
fn from (state: &ArrangerTui) -> Self {
|
||||
Self {
|
||||
cols: track_widths(state.tracks()),
|
||||
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
||||
sep_fg: TuiTheme::separator_fg(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self: ArrangerVerticalColumnSeparator|custom_render(move|to: &mut TuiOutput|{
|
||||
let style = Some(Style::default().fg(self.sep_fg));
|
||||
Ok(for x in self.cols.iter().map(|col|col.1) {
|
||||
let x = self.scenes_w + to.area().x() + x as u16;
|
||||
for y in to.area().y()..to.area().y2() {
|
||||
to.blit(&"▎", x, y, style);
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
struct ArrangerVerticalRowSeparator {
|
||||
rows: Vec<(usize, usize)>,
|
||||
sep_fg: Color,
|
||||
}
|
||||
impl From<(&ArrangerTui, usize)> for ArrangerVerticalRowSeparator {
|
||||
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
|
||||
Self {
|
||||
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
||||
sep_fg: TuiTheme::separator_fg(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render!(|self: ArrangerVerticalRowSeparator|custom_render(move|to: &mut TuiOutput|{
|
||||
Ok(for y in self.rows.iter().map(|row|row.1) {
|
||||
let y = to.area().y() + (y / PPQ) as u16 + 1;
|
||||
if y >= to.buffer.area.height { break }
|
||||
for x in to.area().x()..to.area().x2().saturating_sub(2) {
|
||||
if x < to.buffer.area.x && y < to.buffer.area.y {
|
||||
let cell = to.buffer.get_mut(x, y);
|
||||
cell.modifier = Modifier::UNDERLINED;
|
||||
cell.underline_color = self.sep_fg;
|
||||
}
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
struct ArrangerVerticalCursor {
|
||||
cols: Vec<(usize, usize)>,
|
||||
rows: Vec<(usize, usize)>,
|
||||
focused: bool,
|
||||
selected: ArrangerSelection,
|
||||
scenes_w: u16,
|
||||
header_h: u16,
|
||||
}
|
||||
impl From<(&ArrangerTui, usize)> for ArrangerVerticalCursor {
|
||||
fn from ((state, factor): (&ArrangerTui, usize)) -> Self {
|
||||
Self {
|
||||
cols: track_widths(state.tracks()),
|
||||
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
||||
focused: state.arranger_focused(),
|
||||
selected: state.selected,
|
||||
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
||||
header_h: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self: ArrangerVerticalCursor|custom_render(move|to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
let focused = self.focused;
|
||||
let selected = self.selected;
|
||||
let get_track_area = |t: usize| [
|
||||
self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(),
|
||||
self.cols[t].0 as u16, area.h(),
|
||||
];
|
||||
let get_scene_area = |s: usize| [
|
||||
area.x(), self.header_h + area.y() + (self.rows[s].1 / PPQ) as u16,
|
||||
area.w(), (self.rows[s].0 / PPQ) as u16
|
||||
];
|
||||
let get_clip_area = |t: usize, s: usize| [
|
||||
self.scenes_w + area.x() + self.cols[t].1 as u16,
|
||||
self.header_h + area.y() + (self.rows[s].1/PPQ) as u16,
|
||||
self.cols[t].0 as u16,
|
||||
(self.rows[s].0 / PPQ) as u16
|
||||
];
|
||||
let mut track_area: Option<[u16;4]> = None;
|
||||
let mut scene_area: Option<[u16;4]> = None;
|
||||
let mut clip_area: Option<[u16;4]> = None;
|
||||
let area = match selected {
|
||||
ArrangerSelection::Mix => area,
|
||||
ArrangerSelection::Track(t) => {
|
||||
track_area = Some(get_track_area(t));
|
||||
area
|
||||
},
|
||||
ArrangerSelection::Scene(s) => {
|
||||
scene_area = Some(get_scene_area(s));
|
||||
area
|
||||
},
|
||||
ArrangerSelection::Clip(t, s) => {
|
||||
track_area = Some(get_track_area(t));
|
||||
scene_area = Some(get_scene_area(s));
|
||||
clip_area = Some(get_clip_area(t, s));
|
||||
area
|
||||
},
|
||||
};
|
||||
let bg = TuiTheme::border_bg();
|
||||
if let Some([x, y, width, height]) = track_area {
|
||||
to.fill_fg([x, y, 1, height], bg);
|
||||
to.fill_fg([x + width, y, 1, height], bg);
|
||||
}
|
||||
if let Some([_, y, _, height]) = scene_area {
|
||||
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
|
||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
|
||||
}
|
||||
Ok(if focused {
|
||||
to.render_in(if let Some(clip_area) = clip_area { clip_area }
|
||||
else if let Some(track_area) = track_area { track_area.clip_h(self.header_h) }
|
||||
else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) }
|
||||
else { area.clip_w(self.scenes_w).clip_h(self.header_h) }, &CORNERS)?
|
||||
})
|
||||
}));
|
||||
|
||||
struct ArrangerVerticalHeader<'a> {
|
||||
tracks: &'a Vec<ArrangerTrack>,
|
||||
cols: Vec<(usize, usize)>,
|
||||
focused: bool,
|
||||
selected: ArrangerSelection,
|
||||
scenes_w: u16,
|
||||
header_h: u16,
|
||||
timebase: &'a Arc<Timebase>,
|
||||
current: &'a Arc<Moment>,
|
||||
}
|
||||
impl<'a> From<&'a ArrangerTui> for ArrangerVerticalHeader<'a> {
|
||||
fn from (state: &'a ArrangerTui) -> Self {
|
||||
Self {
|
||||
tracks: &state.tracks,
|
||||
cols: track_widths(state.tracks()),
|
||||
focused: state.arranger_focused(),
|
||||
selected: state.selected,
|
||||
scenes_w: 3 + ArrangerScene::longest_name(state.scenes()) as u16,
|
||||
header_h: 3,
|
||||
timebase: state.clock().timebase(),
|
||||
current: &state.clock().playhead,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self: ArrangerVerticalHeader<'a>|row!(
|
||||
(track, w) in self.tracks.iter().zip(self.cols.iter().map(|col|col.0)) => {
|
||||
// name and width of track
|
||||
let name = track.name().read().unwrap();
|
||||
let max_w = w.saturating_sub(1).min(name.len()).max(2);
|
||||
let name = format!("▎{}", &name[0..max_w]);
|
||||
let name = Tui::bold(true, name);
|
||||
// beats elapsed
|
||||
let elapsed = if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
|
||||
let length = phrase.read().unwrap().length;
|
||||
let elapsed = track.player.pulses_since_start().unwrap();
|
||||
let elapsed = self.timebase.format_beats_1_short(
|
||||
(elapsed as usize % length) as f64
|
||||
);
|
||||
format!("▎+{elapsed:>}")
|
||||
} else {
|
||||
String::from("▎")
|
||||
};
|
||||
// beats until switchover
|
||||
let until_next = track.player.next_phrase().as_ref().map(|(t, _)|{
|
||||
let target = t.pulse.get();
|
||||
let current = self.current.pulse.get();
|
||||
if target > current {
|
||||
let remaining = target - current;
|
||||
format!("▎-{:>}", self.timebase.format_beats_0_short(remaining))
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}).unwrap_or(String::from("▎"));
|
||||
let timer = Tui::to_south(until_next, elapsed);
|
||||
// name of active MIDI input
|
||||
let _input = format!("▎>{}", track.player.midi_ins().get(0)
|
||||
.map(|port|port.short_name())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into()));
|
||||
// name of active MIDI output
|
||||
let _output = format!("▎<{}", track.player.midi_outs().get(0)
|
||||
.map(|port|port.short_name())
|
||||
.transpose()?
|
||||
.unwrap_or("(none)".into()));
|
||||
Tui::push_x(self.scenes_w,
|
||||
Tui::bg(track.color().rgb,
|
||||
Tui::min_xy(w as u16, self.header_h,
|
||||
Tui::to_south(name, timer))))
|
||||
}
|
||||
));
|
||||
|
||||
struct ArrangerVerticalContent<'a> {
|
||||
size: &'a Measure<Tui>,
|
||||
scenes: &'a Vec<ArrangerScene>,
|
||||
tracks: &'a Vec<ArrangerTrack>,
|
||||
rows: Vec<(usize, usize)>,
|
||||
cols: Vec<(usize, usize)>,
|
||||
header_h: u16,
|
||||
}
|
||||
impl<'a> From<(&'a ArrangerTui, usize)> for ArrangerVerticalContent<'a> {
|
||||
fn from ((state, factor): (&'a ArrangerTui, usize)) -> Self {
|
||||
Self {
|
||||
size: &state.size,
|
||||
scenes: &state.scenes,
|
||||
tracks: &state.tracks,
|
||||
rows: ArrangerScene::ppqs(state.scenes(), factor),
|
||||
cols: track_widths(state.tracks()),
|
||||
header_h: 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self: ArrangerVerticalContent<'a>|Tui::fixed_y(
|
||||
(self.size.h() as u16).saturating_sub(self.header_h),
|
||||
col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => {
|
||||
let height = 1.max((pulses / PPQ) as u16);
|
||||
let playing = scene.is_playing(self.tracks);
|
||||
Tui::fixed_y(
|
||||
height,
|
||||
Tui::to_east(
|
||||
Tui::to_east(
|
||||
if playing { "▶ " } else { " " },
|
||||
Tui::bold(true, scene.name.read().unwrap().as_str())
|
||||
),
|
||||
row!((track, w) in self.cols.iter().map(|col|col.0).enumerate() => {
|
||||
Tui::fixed_xy(w as u16, height, Layers::new(move |add|{
|
||||
let mut bg = TuiTheme::border_bg();
|
||||
match (self.tracks.get(track), scene.clips.get(track)) {
|
||||
(Some(track), Some(Some(phrase))) => {
|
||||
let name = &(phrase as &Arc<RwLock<Phrase>>).read().unwrap().name;
|
||||
let name = format!("{}", name);
|
||||
let max_w = name.len().min((w as usize).saturating_sub(2));
|
||||
let color = phrase.read().unwrap().color;
|
||||
bg = color.dark.rgb;
|
||||
if let Some((_, Some(ref playing))) = track.player.play_phrase() {
|
||||
if *playing.read().unwrap() == *phrase.read().unwrap() {
|
||||
bg = color.light.rgb
|
||||
}
|
||||
};
|
||||
add(&Tui::fixed_x(w as u16, Tui::push_x(1, &name.as_str()[0..max_w])))?;
|
||||
},
|
||||
_ => {}
|
||||
};
|
||||
//add(&Background(bg))
|
||||
Ok(())
|
||||
}))
|
||||
})
|
||||
)
|
||||
)
|
||||
})
|
||||
));
|
||||
|
||||
pub fn arranger_content_horizontal (
|
||||
view: &ArrangerTui,
|
||||
) -> impl Render<Tui> + use<'_> {
|
||||
todo!()
|
||||
}
|
||||
//let focused = view.arranger_focused();
|
||||
//let _tracks = view.tracks();
|
||||
//lay!(
|
||||
//focused.then_some(Background(TuiTheme::border_bg())),
|
||||
//row!(
|
||||
//// name
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks, selected) = self;
|
||||
////let yellow = Some(Style::default().yellow().bold().not_dim());
|
||||
////let white = Some(Style::default().white().bold().not_dim());
|
||||
////let area = to.area();
|
||||
////let area = [area.x(), area.y(), 3 + 5.max(track_name_max_len(tracks)) as u16, area.h()];
|
||||
////let offset = 0; // track scroll offset
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
////to.blit(&"Mixer", area.x() + 1, area.y() + y, Some(DIM))?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2 + offset;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////let selected = selected.track() == Some(index);
|
||||
////let style = if selected { yellow } else { white };
|
||||
////to.blit(&format!(" {index:>02} "), area.x(), area.y() + y, style)?;
|
||||
////to.blit(&*track.name.read().unwrap(), area.x() + 4, area.y() + y, style)?;
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// monitor
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let on = Some(Style::default().not_dim().green().bold());
|
||||
////let off = Some(DIM);
|
||||
////area.x += 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" MON ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////let style = if track.monitoring { on } else { off };
|
||||
////to.blit(&" MON ", area.x(), area.y() + y, style)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// record
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let on = Some(Style::default().not_dim().red().bold());
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x += 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" REC ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////let style = if track.recording { on } else { off };
|
||||
////to.blit(&" REC ", area.x(), area.y() + y, style)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// overdub
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let on = Some(Style::default().not_dim().yellow().bold());
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x = area.x + 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" OVR ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(track) = tracks.get(index) {
|
||||
////to.blit(&" OVR ", area.x(), area.y() + y, if track.overdub {
|
||||
////on
|
||||
////} else {
|
||||
////off
|
||||
////})?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// erase
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x = area.x + 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" DEL ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(_) = tracks.get(index) {
|
||||
////to.blit(&" DEL ", area.x(), area.y() + y, off)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 4;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// gain
|
||||
//Widget::new(|_|{todo!()}, |_: &mut TuiOutput|{
|
||||
//todo!()
|
||||
////let Self(tracks) = self;
|
||||
////let mut area = to.area();
|
||||
////let off = Some(Style::default().dim());
|
||||
////area.x = area.x() + 1;
|
||||
////for y in 0..area.h() {
|
||||
////if y == 0 {
|
||||
//////" GAIN ".blit(to.buffer, area.x, area.y + y, style2)?;
|
||||
////} else if y % 2 == 0 {
|
||||
////let index = (y as usize - 2) / 2;
|
||||
////if let Some(_) = tracks.get(index) {
|
||||
////to.blit(&" +0.0 ", area.x(), area.y() + y, off)?;
|
||||
////} else {
|
||||
////area.height = y;
|
||||
////break
|
||||
////}
|
||||
////}
|
||||
////}
|
||||
////area.width = 7;
|
||||
////Ok(Some(area))
|
||||
//}),
|
||||
//// scenes
|
||||
//Widget::new(|_|{todo!()}, |to: &mut TuiOutput|{
|
||||
//let [x, y, _, height] = to.area();
|
||||
//let mut x2 = 0;
|
||||
//Ok(for (scene_index, scene) in view.scenes().iter().enumerate() {
|
||||
//let active_scene = view.selected.scene() == Some(scene_index);
|
||||
//let sep = Some(if active_scene {
|
||||
//Style::default().yellow().not_dim()
|
||||
//} else {
|
||||
//Style::default().dim()
|
||||
//});
|
||||
//for y in y+1..y+height {
|
||||
//to.blit(&"│", x + x2, y, sep);
|
||||
//}
|
||||
//let name = scene.name.read().unwrap();
|
||||
//let mut x3 = name.len() as u16;
|
||||
//to.blit(&*name, x + x2, y, sep);
|
||||
//for (i, clip) in scene.clips.iter().enumerate() {
|
||||
//let active_track = view.selected.track() == Some(i);
|
||||
//if let Some(clip) = clip {
|
||||
//let y2 = y + 2 + i as u16 * 2;
|
||||
//let label = format!("{}", clip.read().unwrap().name);
|
||||
//to.blit(&label, x + x2, y2, Some(if active_track && active_scene {
|
||||
//Style::default().not_dim().yellow().bold()
|
||||
//} else {
|
||||
//Style::default().not_dim()
|
||||
//}));
|
||||
//x3 = x3.max(label.len() as u16)
|
||||
//}
|
||||
//}
|
||||
//x2 = x2 + x3 + 1;
|
||||
//})
|
||||
//}),
|
||||
//)
|
||||
//)
|
||||
//}
|
||||
|
|
@ -58,7 +58,7 @@ render!(|self: PhraseListView<'a>|{
|
|||
}
|
||||
};
|
||||
let row2 = Tui::bold(true, row2);
|
||||
add(&Tui::bg(color.base.rgb, Tui::fill_x(Tui::to_south(row1, row2))))?;
|
||||
add(&Tui::bg(color.base.rgb, Tui::fill_x(col!([row1, row2]))))?;
|
||||
if *entered && i == *index {
|
||||
add(&CORNERS)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
//]))
|
||||
use crate::*;
|
||||
|
||||
render!(|self: SequencerTui|{
|
||||
Tui::to_north(
|
||||
SequencerStatusBar::from(self),
|
||||
Tui::to_south(
|
||||
TransportView::from(self),
|
||||
Tui::min_y(
|
||||
20,
|
||||
Tui::to_east(
|
||||
Tui::min_x(20, col!([
|
||||
Tui::fixed_y(4, PhraseSelector::play_phrase(
|
||||
&self.player,
|
||||
self.focused() == SequencerFocus::PhrasePlay,
|
||||
self.entered()
|
||||
)),
|
||||
Tui::fixed_y(4, PhraseSelector::next_phrase(
|
||||
&self.player,
|
||||
self.focused() == SequencerFocus::PhraseNext,
|
||||
self.entered()
|
||||
)),
|
||||
PhraseListView::from(self)
|
||||
])),
|
||||
PhraseView::from(self)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
});
|
||||
|
||||
render!(|self: SequencerStatusBar|{
|
||||
|
||||
lay!(|add|if self.width > 60 {
|
||||
add(&row!(![
|
||||
SequencerMode::from(self),
|
||||
SequencerStats::from(self),
|
||||
]))
|
||||
} else if self.width > 0 {
|
||||
add(&col!(![
|
||||
SequencerMode::from(self),
|
||||
SequencerStats::from(self),
|
||||
]))
|
||||
} else {
|
||||
Ok(())
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
struct SequencerMode {
|
||||
mode: &'static str,
|
||||
help: &'static [(&'static str, &'static str, &'static str)]
|
||||
}
|
||||
impl From<&SequencerStatusBar> for SequencerMode {
|
||||
fn from (state: &SequencerStatusBar) -> Self {
|
||||
Self {
|
||||
mode: state.mode,
|
||||
help: state.help,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self:SequencerMode|{
|
||||
let orange = Color::Rgb(255,128,0);
|
||||
let light = Color::Rgb(100,100,100);
|
||||
let yellow = Color::Rgb(255,255,0);
|
||||
let black = Color::Rgb(0,0,0);
|
||||
Tui::to_east(
|
||||
Tui::bg(orange, Tui::fg(black, Tui::bold(true, self.mode))),
|
||||
Tui::bg(light, row!((prefix, hotkey, suffix) in self.help.iter() => {
|
||||
row!([" ", prefix, Tui::fg(yellow, *hotkey), suffix])
|
||||
}))
|
||||
)
|
||||
});
|
||||
|
||||
struct SequencerStats<'a> {
|
||||
cpu: &'a Option<String>,
|
||||
size: &'a String,
|
||||
res: &'a String,
|
||||
}
|
||||
impl<'a> From<&'a SequencerStatusBar> for SequencerStats<'a> {
|
||||
fn from (state: &'a SequencerStatusBar) -> Self {
|
||||
Self {
|
||||
cpu: &state.cpu,
|
||||
size: &state.size,
|
||||
res: &state.res,
|
||||
}
|
||||
}
|
||||
}
|
||||
render!(|self:SequencerStats<'a>|{
|
||||
let orange = Color::Rgb(255,128,0);
|
||||
let dark = Color::Rgb(100,100,100);
|
||||
let cpu = &self.cpu;
|
||||
let res = &self.res;
|
||||
let size = &self.size;
|
||||
Tui::bg(dark, row!([
|
||||
Tui::fg(orange, cpu),
|
||||
Tui::fg(orange, res),
|
||||
Tui::fg(orange, size),
|
||||
]))
|
||||
});
|
||||
|
|
@ -1,155 +0,0 @@
|
|||
use crate::{*, tui::TransportTui};
|
||||
|
||||
impl Render<Tui> for TransportTui {
|
||||
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
TransportView::from(self).min_size(to)
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
TransportView::from(self).render(to)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
render!(|self: TransportView|{
|
||||
let Self { state, .. } = self;// selected, focused, bpm, sync, quant, beat, msu, } = self;
|
||||
let world = Tui::to_east("│World ", Tui::to_east(format!("│0 (0)"), //sample(chunk)
|
||||
Tui::to_east(format!("│00m00s000u"), /*msu*/ format!("│00B 0b 00/00"), /*bbt*/)));
|
||||
let timer = Tui::either(
|
||||
*state == Some(TransportState::Rolling),
|
||||
Tui::to_south(
|
||||
Tui::to_east("│",
|
||||
Tui::to_east(Tui::fg(Color::Rgb(0, 255, 0), "▶ PLAYING"),
|
||||
Tui::to_east("│0 (0)",
|
||||
Tui::to_east("│00m00s000u", "│00B 0b 00/00")))),
|
||||
|
||||
Tui::to_east("│Now ",
|
||||
Tui::to_east("│0 (0)",
|
||||
Tui::to_east("│00m00s000u", "│00B 0b 00/00")))
|
||||
),
|
||||
Tui::to_south(
|
||||
Tui::to_east("│", Tui::fg(Color::Rgb(255, 128, 0), "⏹ STOPPED")),
|
||||
""
|
||||
)
|
||||
);
|
||||
Tui::bg(Color::Rgb(40, 50, 30), Tui::fill_x(Tui::to_south(world, timer)))
|
||||
});
|
||||
//)?;
|
||||
//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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TransportTui> for Option<TransportFocus> {
|
||||
fn from (state: &TransportTui) -> Self {
|
||||
Some(state.focus.inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SequencerTui> for Option<TransportFocus> {
|
||||
fn from (state: &SequencerTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
SequencerFocus::Transport(focus) => Some(focus),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ArrangerTui> for Option<TransportFocus> {
|
||||
fn from (state: &ArrangerTui) -> Self {
|
||||
match state.focus.inner() {
|
||||
ArrangerFocus::Transport(focus) => Some(focus),
|
||||
_ => 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()),
|
||||
))
|
||||
});
|
||||
|
||||
//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, &Outset::X(1u16, {
|
||||
//row! {
|
||||
//"BPM ",
|
||||
//format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
|
||||
//}
|
||||
//})),
|
||||
//selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! {
|
||||
//"SYNC ", pulses_to_name(*sync as usize)
|
||||
//})),
|
||||
//selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! {
|
||||
//"QUANT ", pulses_to_name(*quant as usize)
|
||||
//})),
|
||||
//selected.wrap(TransportFocus::Clock, &{
|
||||
//row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1)
|
||||
//}).align_e().fill_x(),
|
||||
//).fill_x().bg(Color::Rgb(40, 50, 30))
|
||||
Loading…
Add table
Add a link
Reference in a new issue