WorldClock/PlayClock

This commit is contained in:
🪞👃🪞 2024-12-09 21:38:42 +01:00
parent f5128829d6
commit 26a9efaa86
19 changed files with 1216 additions and 3039 deletions

View file

@ -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" }

View file

@ -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`

View file

@ -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 => {

View file

@ -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))

View file

@ -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;
//})
//}),
//)
//)
//}

View file

@ -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),
]))
});

View file

@ -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,
},
}
})
}

View file

@ -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> {

View file

@ -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,
},
}
})
}

View file

@ -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);

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -1 +0,0 @@
use crate::*;

View file

@ -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;
//})
//}),
//)
//)
//}

View file

@ -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)?;
}

View file

@ -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),
]))
});

View file

@ -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))