apply from_jack!

This commit is contained in:
🪞👃🪞 2024-12-18 13:28:03 +01:00
parent 0496ed6251
commit efda18293d
7 changed files with 522 additions and 582 deletions

View file

@ -1,6 +1,18 @@
use crate::*;
/// Trait for things that have a JACK process callback.
/// Implement [TryFrom<&Arc<RwLock<JackClient>>>]: create app state from wrapped JACK handle.
#[macro_export] macro_rules! from_jack {
(|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc<RwLock<JackClient>>> for $Struct $(<$($L),*$($T),*>)? {
type Error = Box<dyn std::error::Error>;
fn try_from ($jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
Ok($cb)
}
}
};
}
/// Trait for thing that has a JACK process callback.
pub trait Audio: Send + Sync {
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
Control::Continue
@ -16,6 +28,7 @@ pub trait Audio: Send + Sync {
}
}
/// Implement [Audio]: provide JACK callbacks.
#[macro_export] macro_rules! audio {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
@ -24,17 +37,7 @@ pub trait Audio: Send + Sync {
}
}
#[macro_export] macro_rules! from_jack {
(|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc<RwLock<JackClient>>> for $Struct $(<$($L),*$($T),*>)? {
type Error = Box<dyn std::error::Error>;
fn try_from ($jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
Ok($cb)
}
}
};
}
/// Trait for thing that may receive MIDI.
pub trait HasMidiIns {
fn midi_ins (&self) -> &Vec<Port<MidiIn>>;
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>>;
@ -43,13 +46,15 @@ pub trait HasMidiIns {
}
}
/// Trait for thing that may output MIDI.
pub trait HasMidiOuts {
fn midi_outs (&self) -> &Vec<Port<MidiOut>>;
fn midi_outs_mut (&mut self) -> &mut Vec<Port<MidiOut>>;
fn midi_note (&mut self) -> &mut Vec<u8>;
fn has_midi_outs (&self) -> bool {
self.midi_outs().len() > 0
}
/// Buffer for serializing a MIDI event. FIXME rename
fn midi_note (&mut self) -> &mut Vec<u8>;
}
////////////////////////////////////////////////////////////////////////////////////

View file

@ -2,35 +2,6 @@ use crate::*;
use crate::api::ArrangerTrackCommand;
use crate::api::ArrangerSceneCommand;
impl TryFrom<&Arc<RwLock<JackClient>>> for ArrangerTui {
type Error = Box<dyn std::error::Error>;
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
clock: ClockModel::from(jack),
phrases: PhraseListModel::default(),
editor: PhraseEditorModel::default(),
selected: ArrangerSelection::Clip(0, 0),
scenes: vec![],
tracks: vec![],
color: TuiTheme::bg().into(),
history: vec![],
mode: ArrangerMode::Vertical(2),
name: Arc::new(RwLock::new(String::new())),
size: Measure::new(),
cursor: (0, 0),
splits: [20, 20],
entered: false,
menu_bar: None,
status_bar: None,
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)),
})
}
}
/// Root view for standalone `tek_arranger`
pub struct ArrangerTui {
pub jack: Arc<RwLock<JackClient>>,
@ -55,7 +26,29 @@ pub struct ArrangerTui {
pub focus: FocusState<ArrangerFocus>,
pub perf: PerfModel,
}
from_jack!(|jack| ArrangerTui Self {
jack: jack.clone(),
clock: ClockModel::from(jack),
phrases: PhraseListModel::default(),
editor: PhraseEditorModel::default(),
selected: ArrangerSelection::Clip(0, 0),
scenes: vec![],
tracks: vec![],
color: TuiTheme::bg().into(),
history: vec![],
mode: ArrangerMode::Vertical(2),
name: Arc::new(RwLock::new(String::new())),
size: Measure::new(),
cursor: (0, 0),
splits: [20, 20],
entered: false,
menu_bar: None,
status_bar: None,
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
focus: FocusState::Entered(ArrangerFocus::Transport(TransportFocus::PlayPause)),
});
has_clock!(|self:ArrangerTui|&self.clock);
has_phrases!(|self:ArrangerTui|self.phrases.phrases);
has_editor!(|self:ArrangerTui|self.editor);
@ -146,7 +139,6 @@ impl HasPhraseList for ArrangerTui {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
#[derive(Clone, Debug)]
pub enum ArrangerCommand {
Focus(FocusCommand<ArrangerFocus>),
@ -163,7 +155,6 @@ pub enum ArrangerCommand {
Phrases(PhrasesCommand),
Editor(PhraseCommand),
}
impl Command<ArrangerTui> for ArrangerCommand {
fn execute (self, state: &mut ArrangerTui) -> Perhaps<Self> {
use ArrangerCommand::*;
@ -184,28 +175,24 @@ impl Command<ArrangerTui> for ArrangerCommand {
})
}
}
impl Command<ArrangerTui> for ArrangerSceneCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerTrackCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
impl Command<ArrangerTui> for ArrangerClipCommand {
fn execute (self, _state: &mut ArrangerTui) -> Perhaps<Self> {
//todo!();
Ok(None)
}
}
pub trait ArrangerControl: TransportControl<ArrangerFocus> {
fn selected (&self) -> ArrangerSelection;
fn selected_mut (&mut self) -> &mut ArrangerSelection;
@ -214,7 +201,6 @@ pub trait ArrangerControl: TransportControl<ArrangerFocus> {
fn toggle_loop (&mut self);
fn randomize_color (&mut self);
}
impl ArrangerControl for ArrangerTui {
fn selected (&self) -> ArrangerSelection {
self.selected
@ -272,8 +258,6 @@ impl InputToCommand<Tui, ArrangerTui> for ArrangerCommand {
.or_else(||to_focus_command(input).map(ArrangerCommand::Focus))
}
}
fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
use ArrangerCommand as Cmd;
use KeyCode::Char;
@ -318,7 +302,6 @@ fn to_arranger_command (state: &ArrangerTui, input: &TuiInput) -> Option<Arrange
}
})
}
fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Right, Delete};
use ArrangerCommand as Cmd;
@ -335,7 +318,6 @@ fn to_arranger_mix_command (input: &TuiInput) -> Option<ArrangerCommand> {
_ => return None
})
}
fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
@ -354,7 +336,6 @@ fn to_arranger_track_command (input: &TuiInput, t: usize) -> Option<ArrangerComm
_ => return None
})
}
fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Right, Enter, Delete};
use ArrangerCommand as Cmd;
@ -374,7 +355,6 @@ fn to_arranger_scene_command (input: &TuiInput, s: usize) -> Option<ArrangerComm
_ => return None
})
}
fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<ArrangerCommand> {
use KeyCode::{Char, Up, Down, Left, Right, Delete};
use ArrangerCommand as Cmd;
@ -396,7 +376,6 @@ fn to_arranger_clip_command (input: &TuiInput, t: usize, s: usize) -> Option<Arr
_ => return None
})
}
impl TransportControl<ArrangerFocus> for ArrangerTui {
fn transport_focused (&self) -> Option<TransportFocus> {
match self.focus.inner() {
@ -440,7 +419,7 @@ impl From<&ArrangerTui> for Option<TransportFocus> {
}
}
impl_focus!(ArrangerTui ArrangerFocus [
impl_focus!(ArrangerTui ArrangerFocus [
//&[
//Menu,
//Menu,
@ -482,116 +461,6 @@ pub enum ArrangerStatus {
PhraseEdit,
}
impl StatusBar for ArrangerStatus {
type State = (ArrangerFocus, ArrangerSelection, bool);
fn hotkey_fg () -> Color where Self: Sized {
TuiTheme::HOTKEY_FG
}
fn update (&mut self, (focused, selected, entered): &Self::State) {
*self = match focused {
//ArrangerFocus::Menu => { todo!() },
ArrangerFocus::Transport(_) => ArrangerStatus::Transport,
ArrangerFocus::Arranger => match selected {
ArrangerSelection::Mix => ArrangerStatus::ArrangerMix,
ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack,
ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene,
ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip,
},
ArrangerFocus::Phrases => ArrangerStatus::PhrasePool,
ArrangerFocus::PhraseEditor => match entered {
true => ArrangerStatus::PhraseEdit,
false => ArrangerStatus::PhraseView,
},
}
}
}
render!(<Tui>|self: ArrangerStatus|{
let label = match self {
Self::Transport => "TRANSPORT",
Self::ArrangerMix => "PROJECT",
Self::ArrangerTrack => "TRACK",
Self::ArrangerScene => "SCENE",
Self::ArrangerClip => "CLIP",
Self::PhrasePool => "SEQ LIST",
Self::PhraseView => "VIEW SEQ",
Self::PhraseEdit => "EDIT SEQ",
};
let status_bar_bg = TuiTheme::status_bar_bg();
let mode_bg = TuiTheme::mode_bg();
let mode_fg = TuiTheme::mode_fg();
let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} "))));
let commands = match self {
Self::ArrangerMix => Self::command(&[
["", "c", "olor"],
["", "<>", "resize"],
["", "+-", "zoom"],
["", "n", "ame/number"],
["", "Enter", " stop all"],
]),
Self::ArrangerClip => Self::command(&[
["", "g", "et"],
["", "s", "et"],
["", "a", "dd"],
["", "i", "ns"],
["", "d", "up"],
["", "e", "dit"],
["", "c", "olor"],
["re", "n", "ame"],
["", ",.", "select"],
["", "Enter", " launch"],
]),
Self::ArrangerTrack => Self::command(&[
["re", "n", "ame"],
["", ",.", "resize"],
["", "<>", "move"],
["", "i", "nput"],
["", "o", "utput"],
["", "m", "ute"],
["", "s", "olo"],
["", "Del", "ete"],
["", "Enter", " stop"],
]),
Self::ArrangerScene => Self::command(&[
["re", "n", "ame"],
["", "Del", "ete"],
["", "Enter", " launch"],
]),
Self::PhrasePool => Self::command(&[
["", "a", "ppend"],
["", "i", "nsert"],
["", "d", "uplicate"],
["", "Del", "ete"],
["", "c", "olor"],
["re", "n", "ame"],
["leng", "t", "h"],
["", ",.", "move"],
["", "+-", "resize view"],
]),
Self::PhraseView => Self::command(&[
["", "enter", " edit"],
["", "arrows/pgup/pgdn", " scroll"],
["", "+=", "zoom"],
]),
Self::PhraseEdit => Self::command(&[
["", "esc", " exit"],
["", "a", "ppend"],
["", "s", "et"],
["", "][", "length"],
["", "+-", "zoom"],
]),
_ => Self::command(&[])
};
//let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}"));
Tui::bg(status_bar_bg, Fill::w(row!([mode, commands])))
});
/// Display mode of arranger
#[derive(Clone, PartialEq)]
pub enum ArrangerMode {
@ -912,201 +781,6 @@ render!(<Tui>|self: ArrangerVerticalContent<'a>|Fixed::h(
})
));
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;
//})
//}),
//)
//)
//}
impl HasScenes<ArrangerScene> for ArrangerTui {
fn scenes (&self) -> &Vec<ArrangerScene> {
&self.scenes
@ -1307,3 +981,198 @@ pub enum ArrangerClipCommand {
//Ok(None)
//}
//}
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

@ -1,50 +1,37 @@
use crate::*;
use super::*;
use KeyCode::Char;
impl TryFrom<&Arc<RwLock<JackClient>>> for GrooveboxTui {
type Error = Box<dyn std::error::Error>;
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
let mut sequencer = SequencerTui::try_from(jack)?;
sequencer.status = false;
Ok(Self {
sequencer,
sampler: SamplerTui::try_from(jack)?,
split: 16,
focus: GrooveboxFocus::Sampler,
})
}
}
pub struct GrooveboxTui {
pub sequencer: SequencerTui,
pub sampler: SamplerTui,
pub split: u16,
pub focus: GrooveboxFocus
}
from_jack!(|jack|GrooveboxTui {
let mut sequencer = SequencerTui::try_from(jack)?;
sequencer.status = false;
Self {
sequencer,
sampler: SamplerTui::try_from(jack)?,
split: 16,
focus: GrooveboxFocus::Sampler,
}
});
pub enum GrooveboxFocus {
Sequencer,
Sampler
}
audio!(|self:GrooveboxTui,_client,_process|Control::Continue);
render!(<Tui>|self:GrooveboxTui|Bsp::n(
Fixed::h(2, SequencerStatusBar::from(&self.sequencer)),
Fixed::h(2, SequencerStatus::from(&self.sequencer)),
Fill::h(Bsp::s(&self.sequencer, &self.sampler)),
));
pub enum GrooveboxCommand {
Sequencer(SequencerCommand),
Sampler(SamplerCommand),
}
handle!(<Tui>|self:GrooveboxTui,input|GrooveboxCommand::execute_with_state(self, input));
input_to_command!(GrooveboxCommand: <Tui>|state:GrooveboxTui,input|match input.event() {
// load sample
key_pat!(Char('l')) => GrooveboxCommand::Sampler(SamplerCommand::Import(FileBrowserCommand::Begin)),
_ => match state.focus {
GrooveboxFocus::Sequencer =>
GrooveboxCommand::Sequencer(SequencerCommand::input_to_command(&state.sequencer, input)?),
@ -52,7 +39,6 @@ input_to_command!(GrooveboxCommand: <Tui>|state:GrooveboxTui,input|match input.e
GrooveboxCommand::Sampler(SamplerCommand::input_to_command(&state.sampler, input)?),
}
});
command!(|self:GrooveboxCommand,state:GrooveboxTui|match self {
GrooveboxCommand::Sequencer(command) =>
command.execute(&mut state.sequencer)?.map(GrooveboxCommand::Sequencer),

View file

@ -1,5 +1,6 @@
use crate::*;
use super::*;
use KeyCode::Char;
use std::fs::File;
use symphonia::core::codecs::CODEC_TYPE_NULL;
use symphonia::core::errors::Error;
@ -54,8 +55,11 @@ pub enum SamplerCommand {
}
input_to_command!(SamplerCommand:<Tui>|state:SamplerTui,input|match state.mode {
Some(SamplerMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
_ => todo!()
//_ => match input.event() {
_ => match input.event() {
// load sample
key_pat!(Char('l')) => Self::Import(FileBrowserCommand::Begin),
_ => return None
}
//key_pat!(KeyCode::Up) => state.cursor.0 = if state.cursor.0 == 0 {
//mapped.len() + unmapped.len() - 1
//} else {

View file

@ -3,33 +3,6 @@ use KeyCode::{Tab, BackTab, Char};
use SequencerCommand::*;
use PhraseCommand::*;
use PhrasePoolCommand::*;
/// 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> {
let clock = ClockModel::from(jack);
let phrase = Arc::new(RwLock::new(Phrase::new(
"New", true, 4 * clock.timebase.ppq.get() as usize,
None, Some(ItemColor::random().into())
)));
Ok(Self {
_jack: jack.clone(),
phrases: PhraseListModel::from(&phrase),
editor: PhraseEditorModel::from(&phrase),
player: PhrasePlayerModel::from((&clock, &phrase)),
clock,
size: Measure::new(),
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
show_pool: true,
status: true,
})
}
}
/// Root view for standalone `tek_sequencer`.
pub struct SequencerTui {
_jack: Arc<RwLock<JackClient>>,
@ -44,17 +17,91 @@ pub struct SequencerTui {
pub(crate) midi_buf: Vec<Vec<Vec<u8>>>,
pub(crate) perf: PerfModel,
}
#[derive(Clone, Debug)]
pub enum SequencerCommand {
from_jack!(|jack|SequencerTui {
let clock = ClockModel::from(jack);
let phrase = Arc::new(RwLock::new(Phrase::new(
"New", true, 4 * clock.timebase.ppq.get() as usize,
None, Some(ItemColor::random().into())
)));
Self {
_jack: jack.clone(),
phrases: PhraseListModel::from(&phrase),
editor: PhraseEditorModel::from(&phrase),
player: PhrasePlayerModel::from((&clock, &phrase)),
clock,
size: Measure::new(),
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
show_pool: true,
status: true,
}
});
render!(<Tui>|self: SequencerTui|{
let w = self.size.w();
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
let pool_w = if self.show_pool { phrase_w } else { 0 };
let pool = Fill::h(Align::e(PhraseListView(&self.phrases)));
let with_pool = move|x|Tui::split_w(false, pool_w, pool, x);
let status = SequencerStatus::from(self);
let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x);
let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x);
let with_size = |x|lay!([self.size, x]);
let editor = with_editbar(with_pool(Fill::wh(&self.editor)));
let color = self.player.play_phrase().as_ref().map(|(_,p)|
p.as_ref().map(|p|p.read().unwrap().color)
).flatten().clone();
let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling()));
let transport = Fixed::h(2, TransportView::from((self, color, true)));
let toolbar = row!([play, col!([
PhraseSelector::play_phrase(&self.player),
PhraseSelector::next_phrase(&self.player),
]), transport]);
Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ]))))
});
audio!(|self:SequencerTui, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if Control::Quit == ClockAudio(self).process(client, scope) {
return Control::Quit
}
// Update MIDI sequencer
if Control::Quit == PlayerAudio(
&mut self.player, &mut self.note_buf, &mut self.midi_buf
).process(client, scope) {
return Control::Quit
}
// End profiling cycle
self.perf.update(t0, scope);
Control::Continue
});
impl HasPhraseList for SequencerTui {
fn phrases_focused (&self) -> bool {
true
}
fn phrases_entered (&self) -> bool {
true
}
fn phrases_mode (&self) -> &Option<PhraseListMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
has_size!(<Tui>|self:SequencerTui|&self.size);
has_clock!(|self:SequencerTui|&self.clock);
has_phrases!(|self:SequencerTui|self.phrases.phrases);
has_editor!(|self:SequencerTui|self.editor);
handle!(<Tui>|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input));
#[derive(Clone, Debug)] pub enum SequencerCommand {
Clock(ClockCommand),
Phrases(PhrasesCommand),
Editor(PhraseCommand),
Enqueue(Option<Arc<RwLock<Phrase>>>),
ShowPool(bool),
}
handle!(<Tui>|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input));
input_to_command!(SequencerCommand: <Tui>|state:SequencerTui,input|match input.event() {
// Transport: Play/pause
key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() {
@ -98,7 +145,9 @@ input_to_command!(SequencerCommand: <Tui>|state:SequencerTui,input|match input.e
});
command!(|self: SequencerCommand, state: SequencerTui|match self {
Self::Phrases(cmd) => {
let mut default = |cmd: PhrasesCommand|cmd.execute(&mut state.phrases).map(|x|x.map(Phrases));
let mut default = |cmd: PhrasesCommand|cmd
.execute(&mut state.phrases)
.map(|x|x.map(Phrases));
match cmd {
// autoselect: automatically load selected phrase in editor
PhrasesCommand::Select(_) => {
@ -131,124 +180,3 @@ command!(|self: SequencerCommand, state: SequencerTui|match self {
None
}
});
has_size!(<Tui>|self:SequencerTui|&self.size);
has_clock!(|self:SequencerTui|&self.clock);
has_phrases!(|self:SequencerTui|self.phrases.phrases);
has_editor!(|self:SequencerTui|self.editor);
render!(<Tui>|self: SequencerTui|{
let w = self.size.w();
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
let pool_w = if self.show_pool { phrase_w } else { 0 };
let pool = Fill::h(Align::e(PhraseListView(&self.phrases)));
let with_pool = move|x|Tui::split_w(false, pool_w, pool, x);
let status = SequencerStatusBar::from(self);
let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x);
let with_editbar = |x|Tui::split_n(false, 3, PhraseEditStatus(&self.editor), x);
let with_size = |x|lay!([self.size, x]);
let editor = with_editbar(with_pool(Fill::wh(&self.editor)));
let color = self.player.play_phrase().as_ref().map(|(_,p)|p.as_ref().map(|p|p.read().unwrap().color)).flatten().clone();
let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling()));
let transport = Fixed::h(2, TransportView::from((self, color, true)));
let toolbar = row!([play, col!([
PhraseSelector::play_phrase(&self.player),
PhraseSelector::next_phrase(&self.player),
]), transport]);
Tui::min_y(15, with_size(with_status(col!([ toolbar, editor, ]))))
});
audio!(|self:SequencerTui, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if Control::Quit == ClockAudio(self).process(client, scope) {
return Control::Quit
}
// Update MIDI sequencer
if Control::Quit == PlayerAudio(
&mut self.player, &mut self.note_buf, &mut self.midi_buf
).process(client, scope) {
return Control::Quit
}
// End profiling cycle
self.perf.update(t0, scope);
Control::Continue
});
impl HasPhraseList for SequencerTui {
fn phrases_focused (&self) -> bool {
true
}
fn phrases_entered (&self) -> bool {
true
}
fn phrases_mode (&self) -> &Option<PhraseListMode> {
&self.phrases.mode
}
fn phrase_index (&self) -> usize {
self.phrases.phrase.load(Ordering::Relaxed)
}
}
/// Status bar for sequencer app
#[derive(Clone)]
pub struct SequencerStatusBar {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) res: String,
pub(crate) playing: bool,
}
impl StatusBar for SequencerStatusBar {
type State = SequencerTui;
fn hotkey_fg () -> Color {
TuiTheme::HOTKEY_FG
}
fn update (&mut self, _: &SequencerTui) {
todo!()
}
}
impl From<&SequencerTui> for SequencerStatusBar {
fn from (state: &SequencerTui) -> Self {
let samples = state.clock.chunk.load(Ordering::Relaxed);
let rate = state.clock.timebase.sr.get() as f64;
let buffer = samples as f64 / rate;
let width = state.size.w();
Self {
width,
playing: state.clock.is_rolling(),
cpu: state.perf.percentage().map(|cpu|format!("{cpu:.01}%")),
size: format!("{}x{}│", width, state.size.h()),
res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.),
}
}
}
render!(<Tui>|self: SequencerStatusBar|Fixed::h(2, lay!([
{
let single = |binding, command|row!([" ", col!([
Tui::fg(TuiTheme::yellow(), binding),
command
])]);
let double = |(b1, c1), (b2, c2)|col!([
row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]),
row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]),
]);
Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([
single("SPACE", "play/pause"),
double(("", "cursor"), ("C-✣", "scroll"), ),
double(("a", "append"), ("s", "set note"),),
double((",.", "note"), ("<>", "triplet"), ),
double(("[]", "phrase"), ("{}", "order"), ),
double(("q", "enqueue"), ("e", "edit"), ),
]))
},
Fill::wh(Align::se({
Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), row!([
&self.cpu,
&self.res,
&self.size,
]))
})),
])));

View file

@ -3,7 +3,6 @@ use crate::api::ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync};
use TransportCommand::{Focus, Clock};
use FocusCommand::{Next, Prev};
use KeyCode::{Enter, Left, Right, Char};
/// Transport clock app.
pub struct TransportTui {
pub jack: Arc<RwLock<JackClient>>,
@ -12,21 +11,17 @@ pub struct TransportTui {
pub cursor: (usize, usize),
pub focus: FocusState<TransportFocus>,
}
/// Create app state from JACK handle.
impl TryFrom<&Arc<RwLock<JackClient>>> for TransportTui {
type Error = Box<dyn std::error::Error>;
fn try_from (jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
clock: ClockModel::from(jack),
size: Measure::new(),
cursor: (0, 0),
focus: FocusState::Entered(TransportFocus::PlayPause)
})
}
}
from_jack!(|jack|TransportTui Self {
jack: jack.clone(),
clock: ClockModel::from(jack),
size: Measure::new(),
cursor: (0, 0),
focus: FocusState::Entered(TransportFocus::PlayPause)
});
has_clock!(|self:TransportTui|&self.clock);
audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope));
handle!(<Tui>|self:TransportTui,from|TransportCommand::execute_with_state(self, from));
render!(<Tui>|self: TransportTui|TransportView::from((self, None, true)));
impl std::fmt::Debug for TransportTui {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("TransportTui")
@ -36,12 +31,6 @@ impl std::fmt::Debug for TransportTui {
.finish()
}
}
has_clock!(|self:TransportTui|&self.clock);
audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope));
handle!(<Tui>|self:TransportTui,from|TransportCommand::execute_with_state(self, from));
render!(<Tui>|self: TransportTui|TransportView::from((self, None, true)));
pub struct TransportView {
color: ItemPalette,
focused: bool,
@ -187,21 +176,6 @@ impl FocusWrap<TransportFocus> for Option<TransportFocus> {
}
}
#[derive(Copy, Clone)]
pub struct TransportStatusBar;
impl StatusBar for TransportStatusBar {
type State = ();
fn hotkey_fg () -> Color {
TuiTheme::HOTKEY_FG
}
fn update (&mut self, _: &()) {
todo!()
}
}
render!(<Tui>|self: TransportStatusBar|"todo");
pub trait TransportControl<T>: HasClock + {
fn transport_focused (&self) -> Option<TransportFocus>;
}

View file

@ -1,6 +1,6 @@
use crate::*;
pub trait StatusBar: Render<Tui> {
pub trait Bar: Render<Tui> {
type State: Send + Sync;
fn hotkey_fg () -> Color where Self: Sized;
fn update (&mut self, state: &Self::State) where Self: Sized;
@ -21,3 +21,177 @@ pub trait StatusBar: Render<Tui> {
Bsp::n(state.into(), content)
}
}
/// Status bar for sequencer app
#[derive(Clone)]
pub struct SequencerStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) res: String,
pub(crate) playing: bool,
}
impl Bar for SequencerStatus {
type State = SequencerTui;
fn hotkey_fg () -> Color {
TuiTheme::HOTKEY_FG
}
fn update (&mut self, _: &SequencerTui) {
todo!()
}
}
impl From<&SequencerTui> for SequencerStatus {
fn from (state: &SequencerTui) -> Self {
let samples = state.clock.chunk.load(Ordering::Relaxed);
let rate = state.clock.timebase.sr.get() as f64;
let buffer = samples as f64 / rate;
let width = state.size.w();
Self {
width,
playing: state.clock.is_rolling(),
cpu: state.perf.percentage().map(|cpu|format!("{cpu:.01}%")),
size: format!("{}x{}│", width, state.size.h()),
res: format!("│{}s│{:.1}kHz│{:.1}ms│", samples, rate / 1000., buffer * 1000.),
}
}
}
render!(<Tui>|self: SequencerStatus|Fixed::h(2, lay!([
{
let single = |binding, command|row!([" ", col!([
Tui::fg(TuiTheme::yellow(), binding),
command
])]);
let double = |(b1, c1), (b2, c2)|col!([
row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]),
row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]),
]);
Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([
single("SPACE", "play/pause"),
double(("", "cursor"), ("C-✣", "scroll"), ),
double(("a", "append"), ("s", "set note"),),
double((",.", "note"), ("<>", "triplet"), ),
double(("[]", "phrase"), ("{}", "order"), ),
double(("q", "enqueue"), ("e", "edit"), ),
]))
},
Fill::wh(Align::se({
Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), row!([
&self.cpu,
&self.res,
&self.size,
]))
})),
])));
impl Bar for ArrangerStatus {
type State = (ArrangerFocus, ArrangerSelection, bool);
fn hotkey_fg () -> Color where Self: Sized {
TuiTheme::HOTKEY_FG
}
fn update (&mut self, (focused, selected, entered): &Self::State) {
*self = match focused {
//ArrangerFocus::Menu => { todo!() },
ArrangerFocus::Transport(_) => ArrangerStatus::Transport,
ArrangerFocus::Arranger => match selected {
ArrangerSelection::Mix => ArrangerStatus::ArrangerMix,
ArrangerSelection::Track(_) => ArrangerStatus::ArrangerTrack,
ArrangerSelection::Scene(_) => ArrangerStatus::ArrangerScene,
ArrangerSelection::Clip(_, _) => ArrangerStatus::ArrangerClip,
},
ArrangerFocus::Phrases => ArrangerStatus::PhrasePool,
ArrangerFocus::PhraseEditor => match entered {
true => ArrangerStatus::PhraseEdit,
false => ArrangerStatus::PhraseView,
},
}
}
}
render!(<Tui>|self: ArrangerStatus|{
let label = match self {
Self::Transport => "TRANSPORT",
Self::ArrangerMix => "PROJECT",
Self::ArrangerTrack => "TRACK",
Self::ArrangerScene => "SCENE",
Self::ArrangerClip => "CLIP",
Self::PhrasePool => "SEQ LIST",
Self::PhraseView => "VIEW SEQ",
Self::PhraseEdit => "EDIT SEQ",
};
let status_bar_bg = TuiTheme::status_bar_bg();
let mode_bg = TuiTheme::mode_bg();
let mode_fg = TuiTheme::mode_fg();
let mode = Tui::fg(mode_fg, Tui::bg(mode_bg, Tui::bold(true, format!(" {label} "))));
let commands = match self {
Self::ArrangerMix => Self::command(&[
["", "c", "olor"],
["", "<>", "resize"],
["", "+-", "zoom"],
["", "n", "ame/number"],
["", "Enter", " stop all"],
]),
Self::ArrangerClip => Self::command(&[
["", "g", "et"],
["", "s", "et"],
["", "a", "dd"],
["", "i", "ns"],
["", "d", "up"],
["", "e", "dit"],
["", "c", "olor"],
["re", "n", "ame"],
["", ",.", "select"],
["", "Enter", " launch"],
]),
Self::ArrangerTrack => Self::command(&[
["re", "n", "ame"],
["", ",.", "resize"],
["", "<>", "move"],
["", "i", "nput"],
["", "o", "utput"],
["", "m", "ute"],
["", "s", "olo"],
["", "Del", "ete"],
["", "Enter", " stop"],
]),
Self::ArrangerScene => Self::command(&[
["re", "n", "ame"],
["", "Del", "ete"],
["", "Enter", " launch"],
]),
Self::PhrasePool => Self::command(&[
["", "a", "ppend"],
["", "i", "nsert"],
["", "d", "uplicate"],
["", "Del", "ete"],
["", "c", "olor"],
["re", "n", "ame"],
["leng", "t", "h"],
["", ",.", "move"],
["", "+-", "resize view"],
]),
Self::PhraseView => Self::command(&[
["", "enter", " edit"],
["", "arrows/pgup/pgdn", " scroll"],
["", "+=", "zoom"],
]),
Self::PhraseEdit => Self::command(&[
["", "esc", " exit"],
["", "a", "ppend"],
["", "s", "et"],
["", "][", "length"],
["", "+-", "zoom"],
]),
_ => Self::command(&[])
};
//let commands = commands.iter().reduce(String::new(), |s, (a, b, c)| format!("{s} {a}{b}{c}"));
Tui::bg(status_bar_bg, Fill::w(row!([mode, commands])))
});