separate tui model and view layers

This commit is contained in:
🪞👃🪞 2024-11-25 17:57:20 +01:00
parent 1060afa4f3
commit 416acd9f7b
19 changed files with 1124 additions and 1095 deletions

View file

@ -14,7 +14,6 @@ use std::fmt::Debug;
submod! { submod! {
tui_apps tui_apps
tui_command tui_command
tui_content
tui_control tui_control
tui_debug tui_debug
tui_focus tui_focus
@ -24,10 +23,23 @@ submod! {
tui_impls tui_impls
tui_jack tui_jack
tui_menu tui_menu
tui_model
tui_select tui_select
tui_status tui_status
tui_theme tui_theme
tui_view
tui_widget tui_model_arranger
tui_model_clock
tui_model_file_browser
tui_model_phrase_editor
tui_model_phrase_length
tui_model_phrase_list
tui_model_phrase_player
tui_view_arranger
tui_view_file_browser
tui_view_phrase_editor
tui_view_phrase_length
tui_view_phrase_list
tui_view_sequencer
tui_view_transport
} }

View file

@ -1,370 +0,0 @@
use crate::*;
impl Content for TransportView {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let Self { state, selected, focused, bpm, sync, quant, beat, msu, } = self;
row!(
selected.wrap(TransportFocus::PlayPause, &Styled(
None,
match *state {
Some(TransportState::Rolling) => "▶ PLAYING",
Some(TransportState::Starting) => "READY ...",
Some(TransportState::Stopped) => "⏹ STOPPED",
_ => "???",
}
).min_xy(11, 2).push_x(1)),
selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, {
row! {
"BPM ",
format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
}
})),
selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! {
"QUANT ", pulses_to_name(*quant as usize)
})),
selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! {
"SYNC ", pulses_to_name(*sync 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))
}
}
impl Content for SequencerTui {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
lay!(
col!(
TransportView::from(self),
Split::right(20,
widget(&PhrasesView(self)),
widget(&PhraseView(self)),
).min_y(20)
),
self.perf.percentage()
.map(|cpu|format!("{cpu:.03}%"))
.fg(Color::Rgb(255,128,0))
.align_sw(),
)
}
}
/// 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),
}
}
}
/// Layout for standalone arranger app.
impl Content for ArrangerTui {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let arranger_focused = self.arranger_focused();
Split::down(
1,
TransportView::from(self),
Split::down(
self.splits[0],
lay!(
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)
})
.grow_y(1)
.border(Lozenge(Style::default()
.bg(TuiTheme::border_bg())
.fg(TuiTheme::border_fg(arranger_focused)))),
widget(&self.size),
widget(&format!("[{}] Arranger", if self.entered {
""
} else {
" "
}))
.fg(TuiTheme::title_fg(arranger_focused))
.push_x(1),
),
Split::right(
self.splits[1],
PhrasesView(self),
PhraseView(self),
)
)
)
}
}
// TODO: Display phrases always in order of appearance
impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let focused = self.0.phrases_focused();
let entered = self.0.phrases_entered();
let mode = self.0.phrases_mode();
let content = Stack::down(move|add|match mode {
Some(PhrasesMode::Import(_, ref browser)) => {
add(browser)
},
Some(PhrasesMode::Export(_, ref browser)) => {
add(browser)
},
_ => {
let phrases = self.0.phrases();
let selected = self.0.phrase_index();
for (i, phrase) in phrases.iter().enumerate() {
add(&Layers::new(|add|{
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
let mut length = PhraseLength::new(length, None);
if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
if focused && i == *phrase {
length.pulses = *new_length;
length.focus = Some(*focus);
}
}
let length = length.align_e().fill_x();
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
let mut row2 = format!(" {name}");
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
if focused && i == *phrase {
row2 = format!("{row2}");
}
};
let row2 = TuiStyle::bold(row2, true);
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
if focused && i == selected {
add(&CORNERS)?;
}
Ok(())
}))?;
}
Ok(())
}
});
let border_color = if focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
let title_color = if focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
let upper_left = format!("[{}] Phrases", if entered {""} else {" "});
let upper_right = format!("({})", self.0.phrases().len());
lay!(
content,
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
)
}
}
impl Content for FileBrowser {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Stack::down(|add|{
let mut i = 0;
for (_, name) in self.dirs.iter() {
if i >= self.scroll {
add(&TuiStyle::bold(name.as_str(), i == self.index))?;
}
i += 1;
}
for (_, name) in self.files.iter() {
if i >= self.scroll {
add(&TuiStyle::bold(name.as_str(), i == self.index))?;
}
i += 1;
}
add(&format!("{}/{i}", self.index))?;
Ok(())
})
}
}
impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let phrase = self.0.phrase_editing();
let size = self.0.size();
let focused = self.0.phrase_editor_focused();
let entered = self.0.phrase_editor_entered();
let keys = self.0.keys();
let buffer = self.0.buffer();
let note_len = self.0.note_len();
let note_axis = self.0.note_axis();
let time_axis = self.0.time_axis();
let FixedAxis { start: note_start, point: note_point, clamp: note_clamp }
= *note_axis.read().unwrap();
let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale }
= *time_axis.read().unwrap();
//let color = Color::Rgb(0,255,0);
//let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color);
let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{
Ok(if to.area().h() >= 2 {
to.buffer_update(to.area().set_w(5), &|cell, x, y|{
let y = y + (note_start / 2) as u16;
if x < keys.area.width && y < keys.area.height {
*cell = keys.get(x, y).clone()
}
});
})
}).fill_y();
let notes_bg_null = Color::Rgb(28, 35, 25);
let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
let area = to.area();
let h = area.h() as usize;
size.set_wh(area.w(), h);
let mut axis = note_axis.write().unwrap();
if let Some(point) = axis.point {
if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) {
axis.start += 2;
}
}
Ok(if to.area().h() >= 2 {
let area = to.area();
to.buffer_update(area, &move |cell, x, y|{
cell.set_bg(notes_bg_null);
let src_x = (x as usize + time_start) * time_scale;
let src_y = y as usize + note_start / 2;
if src_x < buffer.width && src_y < buffer.height - 1 {
buffer.get(src_x, buffer.height - src_y - 2).map(|src|{
cell.set_symbol(src.symbol());
cell.set_fg(src.fg);
cell.set_bg(src.bg);
});
}
});
})
}).fill_x();
let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
Ok(if focused && entered {
let area = to.area();
if let (Some(time), Some(note)) = (time_point, note_point) {
let x1 = area.x() + (time / time_scale) as u16;
let x2 = x1 + (note_len / time_scale) as u16;
let y = area.y() + note.saturating_sub(note_start) as u16 / 2;
let c = if note % 2 == 0 { "" } else { "" };
for x in x1..x2 {
to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0))));
}
}
})
});
let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30));
let playhead_active = playhead_inactive.clone().yellow().bold().not_dim();
let playhead = CustomWidget::new(
|to:[u16;2]|Ok(Some(to.clip_h(1))),
move|to: &mut TuiOutput|{
if let Some(_) = phrase {
let now = self.0.now().get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
let time_clamp = time_clamp
.expect("time_axis of sequencer expected to be clamped");
for x in 0..(time_clamp/time_scale).saturating_sub(time_start) {
let this_step = time_start + (x + 0) * time_scale;
let next_step = time_start + (x + 1) * time_scale;
let x = to.area().x() + x as u16;
let active = this_step <= now && now < next_step;
let character = if active { "|" } else { "·" };
let style = if active { playhead_active } else { playhead_inactive };
to.blit(&character, x, to.area.y(), Some(style));
}
}
Ok(())
}
).push_x(6).align_sw();
let border_color = if focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)};
let title_color = if focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
let note_area = lay!(notes, cursor).fill_x();
let piano_roll = row!(keys, note_area).fill_x();
let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border);
let content = lay!(content, playhead);
let mut upper_left = format!("[{}] Sequencer", if entered {""} else {" "});
if let Some(phrase) = phrase {
upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name);
}
let mut lower_right = format!("{}", size.format());
lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale));
//lower_right = format!("Zoom: {} (+{}:{}*{}|{})",
//pulses_to_name(time_scale),
//time_start, time_point.unwrap_or(0),
//time_scale, time_clamp.unwrap_or(0),
//);
if focused && entered {
lower_right = format!("┤Note: {} {}├─{lower_right}",
note_axis.read().unwrap().point.unwrap(),
pulses_to_name(note_len));
//lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}",
//pulses_to_name(*note_len),
//note_start,
//note_point.unwrap_or(0),
//note_clamp.unwrap_or(0),
//);
}
let upper_right = if let Some(phrase) = phrase {
format!("┤Length: {}", phrase.read().unwrap().length)
} else {
String::new()
};
lay!(
content,
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(),
)
}
}
impl Content for PhraseLength {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Layers::new(move|add|{
match self.focus {
None => add(&row!(
" ", self.bars_string(),
".", self.beats_string(),
".", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Bar) => add(&row!(
"[", self.bars_string(),
"]", self.beats_string(),
".", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Beat) => add(&row!(
" ", self.bars_string(),
"[", self.beats_string(),
"]", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Tick) => add(&row!(
" ", self.bars_string(),
".", self.beats_string(),
"[", self.ticks_string(),
"]"
)),
}
})
}
}

View file

@ -174,68 +174,6 @@ macro_rules! impl_phrase_editor_control {
} }
} }
} }
macro_rules! impl_phrases_view_state {
($Struct:ident $(:: $field:ident)* [$self1:ident: $focus:expr] [$self2:ident: $enter:expr]) => {
impl PhrasesViewState for $Struct {
fn phrases_focused (&$self1) -> bool {
$focus
}
fn phrases_entered (&$self2) -> bool {
$enter
}
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self$(.$field)*.phrases
}
fn phrase_index (&self) -> usize {
self$(.$field)*.phrase.load(Ordering::Relaxed)
}
fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self$(.$field)*.mode
}
}
}
}
macro_rules! impl_phrase_view_state {
($Struct:ident $(:: $field:ident)* [$self1:ident : $focused:expr] [$self2:ident : $entered:expr]) => {
impl PhraseViewState for $Struct {
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>> {
&self$(.$field)*.phrase
}
fn phrase_editor_focused (&$self1) -> bool {
$focused
//self$(.$field)*.focus.is_focused()
}
fn phrase_editor_entered (&$self2) -> bool {
$entered
//self$(.$field)*.focus.is_entered()
}
fn phrase_editor_size (&self) -> &Measure<Tui> {
todo!()
}
fn keys (&self) -> &Buffer {
&self$(.$field)*.keys
}
fn buffer (&self) -> &BigBuffer {
&self$(.$field)*.buffer
}
fn note_len (&self) -> usize {
self$(.$field)*.note_len
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
&self$(.$field)*.note_axis
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
&self$(.$field)*.time_axis
}
fn now (&self) -> &Arc<Pulse> {
&self$(.$field)*.now
}
fn size (&self) -> &Measure<Tui> {
&self$(.$field)*.size
}
}
}
}
impl_jack_api!(TransportTui::jack); impl_jack_api!(TransportTui::jack);
impl_jack_api!(SequencerTui::jack); impl_jack_api!(SequencerTui::jack);
@ -268,28 +206,3 @@ impl_phrase_editor_control!(ArrangerTui
[self: todo!()] [self: todo!()]
[self, phrase: todo!()] [self, phrase: todo!()]
); );
impl_phrases_view_state!(PhrasesModel
[self: false]
[self: false]
);
impl_phrases_view_state!(SequencerTui::phrases
[self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)]
[self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)]
);
impl_phrases_view_state!(ArrangerTui::phrases
[self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)]
[self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)]
);
impl_phrase_view_state!(PhraseEditorModel
[self: true]
[self: true]
);
impl_phrase_view_state!(SequencerTui::editor
[self: self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)]
[self: self.entered() && self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)]
);
impl_phrase_view_state!(ArrangerTui::editor
[self: self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)]
[self: self.entered() && self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)])
;

View file

@ -1,379 +1 @@
use crate::*; use crate::*;
#[derive(Clone)]
pub struct ClockModel {
/// JACK transport handle.
pub(crate) transport: Arc<Transport>,
/// Playback state
pub(crate) playing: Arc<RwLock<Option<TransportState>>>,
/// Global sample and usec at which playback started
pub(crate) started: Arc<RwLock<Option<(usize, usize)>>>,
/// Current moment in time
pub(crate) current: Arc<Instant>,
/// Note quantization factor
pub(crate) quant: Arc<Quantize>,
/// Launch quantization factor
pub(crate) sync: Arc<LaunchSync>,
/// TODO: Enable metronome?
pub(crate) metronome: bool,
}
impl From<&Arc<Transport>> for ClockModel {
fn from (transport: &Arc<Transport>) -> Self {
Self {
current: Instant::default().into(),
playing: RwLock::new(None).into(),
quant: Quantize::default().into(),
started: RwLock::new(None).into(),
sync: LaunchSync::default().into(),
transport: transport.clone(),
metronome: false,
}
}
}
/// Contains state for playing a phrase
pub struct PhrasePlayerModel {
/// State of clock and playhead
pub(crate) clock: ClockModel,
/// Start time and phrase being played
pub(crate) play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Start time and next phrase
pub(crate) next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Play input through output.
pub(crate) monitoring: bool,
/// Write input to sequence.
pub(crate) recording: bool,
/// Overdub input to sequence.
pub(crate) overdub: bool,
/// Send all notes off
pub(crate) reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
pub midi_ins: Vec<Port<MidiIn>>,
/// Play from current sequence to MIDI ports
pub midi_outs: Vec<Port<MidiOut>>,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
/// MIDI output buffer
pub note_buf: Vec<u8>,
}
impl From<&ClockModel> for PhrasePlayerModel {
fn from (clock: &ClockModel) -> Self {
Self {
clock: clock.clone(),
midi_ins: vec![],
midi_outs: vec![],
note_buf: vec![0;8],
reset: true,
recording: false,
monitoring: false,
overdub: false,
play_phrase: None,
next_phrase: None,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
}
}
}
/// Contains state for viewing and editing a phrase
pub struct PhraseEditorModel {
/// Phrase being played
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
/// Length of note that will be inserted, in pulses
pub(crate) note_len: usize,
/// The full piano keys are rendered to this buffer
pub(crate) keys: Buffer,
/// The full piano roll is rendered to this buffer
pub(crate) buffer: BigBuffer,
/// Cursor/scroll/zoom in pitch axis
pub(crate) note_axis: RwLock<FixedAxis<usize>>,
/// Cursor/scroll/zoom in time axis
pub(crate) time_axis: RwLock<ScaledAxis<usize>>,
/// Display mode
pub(crate) mode: bool,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
/// Current position of global playhead
pub(crate) now: Arc<Pulse>,
/// Width and height of notes area at last render
pub(crate) size: Measure<Tui>
}
impl Default for PhraseEditorModel {
fn default () -> Self {
Self {
phrase: None,
note_len: 24,
keys: keys_vert(),
buffer: Default::default(),
mode: false,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
now: Pulse::default().into(),
size: Measure::new(),
note_axis: RwLock::new(FixedAxis {
start: 12,
point: Some(36),
clamp: Some(127)
}),
time_axis: RwLock::new(ScaledAxis {
start: 00,
point: Some(00),
clamp: Some(000),
scale: 24
}),
}
}
}
#[derive(Debug)]
pub struct PhrasesModel {
/// Collection of phrases
pub(crate) phrases: Vec<Arc<RwLock<Phrase>>>,
/// Selected phrase
pub(crate) phrase: AtomicUsize,
/// Scroll offset
pub(crate) scroll: usize,
/// Mode switch
pub(crate) mode: Option<PhrasesMode>,
}
impl Default for PhrasesModel {
fn default () -> Self {
Self {
phrases: vec![RwLock::new(Phrase::default()).into()],
phrase: 0.into(),
scroll: 0,
mode: None,
}
}
}
/// Modes for phrase pool
#[derive(Debug, Clone)]
pub enum PhrasesMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
/// Load phrase from disk
Import(usize, FileBrowser),
/// Save phrase to disk
Export(usize, FileBrowser),
}
/// Browses for phrase to import/export
#[derive(Debug, Clone)]
pub struct FileBrowser {
pub cwd: PathBuf,
pub dirs: Vec<(OsString, String)>,
pub files: Vec<(OsString, String)>,
pub filter: String,
pub index: usize,
pub scroll: usize,
pub size: Measure<Tui>
}
impl FileBrowser {
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
let mut dirs = vec![];
let mut files = vec![];
for entry in std::fs::read_dir(&cwd)? {
let entry = entry?;
let name = entry.file_name();
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
let meta = entry.metadata()?;
if meta.is_dir() {
dirs.push((name, format!("📁 {decoded}")));
} else if meta.is_file() {
files.push((name, format!("📄 {decoded}")));
}
}
Ok(Self {
cwd,
dirs,
files,
filter: "".to_string(),
index: 0,
scroll: 0,
size: Measure::new(),
})
}
pub fn len (&self) -> usize {
self.dirs.len() + self.files.len()
}
pub fn is_dir (&self) -> bool {
self.index < self.dirs.len()
}
pub fn is_file (&self) -> bool {
self.index >= self.dirs.len()
}
pub fn path (&self) -> PathBuf {
self.cwd.join(if self.is_dir() {
&self.dirs[self.index].0
} else if self.is_file() {
&self.files[self.index - self.dirs.len()].0
} else {
unreachable!()
})
}
pub fn chdir (&self) -> Usually<Self> {
Self::new(Some(self.path()))
}
}
/// Displays and edits phrase length.
pub struct PhraseLength {
/// Pulses per beat (quaver)
pub ppq: usize,
/// Beats per bar
pub bpb: usize,
/// Length of phrase in pulses
pub pulses: usize,
/// Selected subdivision
pub focus: Option<PhraseLengthFocus>,
}
impl PhraseLength {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
pub fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
pub fn ticks (&self) -> usize {
self.pulses % self.ppq
}
pub fn bars_string (&self) -> String {
format!("{}", self.bars())
}
pub fn beats_string (&self) -> String {
format!("{}", self.beats())
}
pub fn ticks_string (&self) -> String {
format!("{:>02}", self.ticks())
}
}
impl HasScenes<ArrangerScene> for ArrangerTui {
fn scenes (&self) -> &Vec<ArrangerScene> {
&self.scenes
}
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
&mut self.scenes
}
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
-> Usually<&mut ArrangerScene>
{
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
let scene = ArrangerScene {
name: Arc::new(name.into()),
clips: vec![None;self.tracks().len()],
color: color.unwrap_or_else(||ItemColor::random()),
};
self.scenes_mut().push(scene);
let index = self.scenes().len() - 1;
Ok(&mut self.scenes_mut()[index])
}
fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
}
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
}
}
#[derive(Default, Debug, Clone)]
pub struct ArrangerScene {
/// Name of scene
pub(crate) name: Arc<RwLock<String>>,
/// Clips in scene, one per track
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
/// Identifying color of scene
pub(crate) color: ItemColor,
}
impl ArrangerSceneApi for ArrangerScene {
fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
&self.clips
}
fn color (&self) -> ItemColor {
self.color
}
}
impl HasTracks<ArrangerTrack> for ArrangerTui {
fn tracks (&self) -> &Vec<ArrangerTrack> {
&self.tracks
}
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
&mut self.tracks
}
}
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
-> Usually<&mut ArrangerTrack>
{
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
let track = ArrangerTrack {
width: name.len() + 2,
name: Arc::new(name.into()),
color: color.unwrap_or_else(||ItemColor::random()),
player: PhrasePlayerModel::from(&self.clock),
};
self.tracks_mut().push(track);
let index = self.tracks().len() - 1;
Ok(&mut self.tracks_mut()[index])
}
fn track_del (&mut self, index: usize) {
self.tracks_mut().remove(index);
for scene in self.scenes_mut().iter_mut() {
scene.clips.remove(index);
}
}
}
#[derive(Debug)]
pub struct ArrangerTrack {
/// Name of track
pub(crate) name: Arc<RwLock<String>>,
/// Preferred width of track column
pub(crate) width: usize,
/// Identifying color of track
pub(crate) color: ItemColor,
/// MIDI player state
pub(crate) player: PhrasePlayerModel,
}
impl ArrangerTrackApi for ArrangerTrack {
/// Name of track
fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
/// Preferred width of track column
fn width (&self) -> usize {
self.width
}
/// Preferred width of track column
fn width_mut (&mut self) -> &mut usize {
&mut self.width
}
/// Identifying color of track
fn color (&self) -> ItemColor {
self.color
}
}

View file

@ -0,0 +1,114 @@
use crate::*;
impl HasScenes<ArrangerScene> for ArrangerTui {
fn scenes (&self) -> &Vec<ArrangerScene> {
&self.scenes
}
fn scenes_mut (&mut self) -> &mut Vec<ArrangerScene> {
&mut self.scenes
}
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
-> Usually<&mut ArrangerScene>
{
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
let scene = ArrangerScene {
name: Arc::new(name.into()),
clips: vec![None;self.tracks().len()],
color: color.unwrap_or_else(||ItemColor::random()),
};
self.scenes_mut().push(scene);
let index = self.scenes().len() - 1;
Ok(&mut self.scenes_mut()[index])
}
fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().map(|s|self.scenes().get(s)).flatten()
}
fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
self.selected.scene().map(|s|self.scenes_mut().get_mut(s)).flatten()
}
}
#[derive(Default, Debug, Clone)]
pub struct ArrangerScene {
/// Name of scene
pub(crate) name: Arc<RwLock<String>>,
/// Clips in scene, one per track
pub(crate) clips: Vec<Option<Arc<RwLock<Phrase>>>>,
/// Identifying color of scene
pub(crate) color: ItemColor,
}
impl ArrangerSceneApi for ArrangerScene {
fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
fn clips (&self) -> &Vec<Option<Arc<RwLock<Phrase>>>> {
&self.clips
}
fn color (&self) -> ItemColor {
self.color
}
}
impl HasTracks<ArrangerTrack> for ArrangerTui {
fn tracks (&self) -> &Vec<ArrangerTrack> {
&self.tracks
}
fn tracks_mut (&mut self) -> &mut Vec<ArrangerTrack> {
&mut self.tracks
}
}
impl ArrangerTracksApi<ArrangerTrack> for ArrangerTui {
fn track_add (&mut self, name: Option<&str>, color: Option<ItemColor>)
-> Usually<&mut ArrangerTrack>
{
let name = name.map_or_else(||self.track_default_name(), |x|x.to_string());
let track = ArrangerTrack {
width: name.len() + 2,
name: Arc::new(name.into()),
color: color.unwrap_or_else(||ItemColor::random()),
player: PhrasePlayerModel::from(&self.clock),
};
self.tracks_mut().push(track);
let index = self.tracks().len() - 1;
Ok(&mut self.tracks_mut()[index])
}
fn track_del (&mut self, index: usize) {
self.tracks_mut().remove(index);
for scene in self.scenes_mut().iter_mut() {
scene.clips.remove(index);
}
}
}
#[derive(Debug)]
pub struct ArrangerTrack {
/// Name of track
pub(crate) name: Arc<RwLock<String>>,
/// Preferred width of track column
pub(crate) width: usize,
/// Identifying color of track
pub(crate) color: ItemColor,
/// MIDI player state
pub(crate) player: PhrasePlayerModel,
}
impl ArrangerTrackApi for ArrangerTrack {
/// Name of track
fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
/// Preferred width of track column
fn width (&self) -> usize {
self.width
}
/// Preferred width of track column
fn width_mut (&mut self) -> &mut usize {
&mut self.width
}
/// Identifying color of track
fn color (&self) -> ItemColor {
self.color
}
}

View file

@ -0,0 +1,33 @@
use crate::*;
#[derive(Clone)]
pub struct ClockModel {
/// JACK transport handle.
pub(crate) transport: Arc<Transport>,
/// Playback state
pub(crate) playing: Arc<RwLock<Option<TransportState>>>,
/// Global sample and usec at which playback started
pub(crate) started: Arc<RwLock<Option<(usize, usize)>>>,
/// Current moment in time
pub(crate) current: Arc<Instant>,
/// Note quantization factor
pub(crate) quant: Arc<Quantize>,
/// Launch quantization factor
pub(crate) sync: Arc<LaunchSync>,
/// TODO: Enable metronome?
pub(crate) metronome: bool,
}
impl From<&Arc<Transport>> for ClockModel {
fn from (transport: &Arc<Transport>) -> Self {
Self {
current: Instant::default().into(),
playing: RwLock::new(None).into(),
quant: Quantize::default().into(),
started: RwLock::new(None).into(),
sync: LaunchSync::default().into(),
transport: transport.clone(),
metronome: false,
}
}
}

View file

@ -0,0 +1,62 @@
use crate::*;
/// Browses for phrase to import/export
#[derive(Debug, Clone)]
pub struct FileBrowser {
pub cwd: PathBuf,
pub dirs: Vec<(OsString, String)>,
pub files: Vec<(OsString, String)>,
pub filter: String,
pub index: usize,
pub scroll: usize,
pub size: Measure<Tui>
}
impl FileBrowser {
pub fn new (cwd: Option<PathBuf>) -> Usually<Self> {
let cwd = if let Some(cwd) = cwd { cwd } else { std::env::current_dir()? };
let mut dirs = vec![];
let mut files = vec![];
for entry in std::fs::read_dir(&cwd)? {
let entry = entry?;
let name = entry.file_name();
let decoded = name.clone().into_string().unwrap_or_else(|_|"<unreadable>".to_string());
let meta = entry.metadata()?;
if meta.is_dir() {
dirs.push((name, format!("📁 {decoded}")));
} else if meta.is_file() {
files.push((name, format!("📄 {decoded}")));
}
}
Ok(Self {
cwd,
dirs,
files,
filter: "".to_string(),
index: 0,
scroll: 0,
size: Measure::new(),
})
}
pub fn len (&self) -> usize {
self.dirs.len() + self.files.len()
}
pub fn is_dir (&self) -> bool {
self.index < self.dirs.len()
}
pub fn is_file (&self) -> bool {
self.index >= self.dirs.len()
}
pub fn path (&self) -> PathBuf {
self.cwd.join(if self.is_dir() {
&self.dirs[self.index].0
} else if self.is_file() {
&self.files[self.index - self.dirs.len()].0
} else {
unreachable!()
})
}
pub fn chdir (&self) -> Usually<Self> {
Self::new(Some(self.path()))
}
}

View file

@ -0,0 +1,54 @@
use crate::*;
/// Contains state for viewing and editing a phrase
pub struct PhraseEditorModel {
/// Phrase being played
pub(crate) phrase: Option<Arc<RwLock<Phrase>>>,
/// Length of note that will be inserted, in pulses
pub(crate) note_len: usize,
/// The full piano keys are rendered to this buffer
pub(crate) keys: Buffer,
/// The full piano roll is rendered to this buffer
pub(crate) buffer: BigBuffer,
/// Cursor/scroll/zoom in pitch axis
pub(crate) note_axis: RwLock<FixedAxis<usize>>,
/// Cursor/scroll/zoom in time axis
pub(crate) time_axis: RwLock<ScaledAxis<usize>>,
/// Display mode
pub(crate) mode: bool,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
/// Current position of global playhead
pub(crate) now: Arc<Pulse>,
/// Width and height of notes area at last render
pub(crate) size: Measure<Tui>
}
impl Default for PhraseEditorModel {
fn default () -> Self {
Self {
phrase: None,
note_len: 24,
keys: keys_vert(),
buffer: Default::default(),
mode: false,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
now: Pulse::default().into(),
size: Measure::new(),
note_axis: RwLock::new(FixedAxis {
start: 12,
point: Some(36),
clamp: Some(127)
}),
time_axis: RwLock::new(ScaledAxis {
start: 00,
point: Some(00),
clamp: Some(000),
scale: 24
}),
}
}
}

View file

@ -0,0 +1,37 @@
use crate::*;
/// Displays and edits phrase length.
pub struct PhraseLength {
/// Pulses per beat (quaver)
pub ppq: usize,
/// Beats per bar
pub bpb: usize,
/// Length of phrase in pulses
pub pulses: usize,
/// Selected subdivision
pub focus: Option<PhraseLengthFocus>,
}
impl PhraseLength {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
pub fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
pub fn ticks (&self) -> usize {
self.pulses % self.ppq
}
pub fn bars_string (&self) -> String {
format!("{}", self.bars())
}
pub fn beats_string (&self) -> String {
format!("{}", self.beats())
}
pub fn ticks_string (&self) -> String {
format!("{:>02}", self.ticks())
}
}

View file

@ -0,0 +1,37 @@
use crate::*;
#[derive(Debug)]
pub struct PhrasesModel {
/// Collection of phrases
pub(crate) phrases: Vec<Arc<RwLock<Phrase>>>,
/// Selected phrase
pub(crate) phrase: AtomicUsize,
/// Scroll offset
pub(crate) scroll: usize,
/// Mode switch
pub(crate) mode: Option<PhrasesMode>,
}
impl Default for PhrasesModel {
fn default () -> Self {
Self {
phrases: vec![RwLock::new(Phrase::default()).into()],
phrase: 0.into(),
scroll: 0,
mode: None,
}
}
}
/// Modes for phrase pool
#[derive(Debug, Clone)]
pub enum PhrasesMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
/// Load phrase from disk
Import(usize, FileBrowser),
/// Save phrase to disk
Export(usize, FileBrowser),
}

View file

@ -0,0 +1,48 @@
use crate::*;
/// Contains state for playing a phrase
pub struct PhrasePlayerModel {
/// State of clock and playhead
pub(crate) clock: ClockModel,
/// Start time and phrase being played
pub(crate) play_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Start time and next phrase
pub(crate) next_phrase: Option<(Instant, Option<Arc<RwLock<Phrase>>>)>,
/// Play input through output.
pub(crate) monitoring: bool,
/// Write input to sequence.
pub(crate) recording: bool,
/// Overdub input to sequence.
pub(crate) overdub: bool,
/// Send all notes off
pub(crate) reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
pub midi_ins: Vec<Port<MidiIn>>,
/// Play from current sequence to MIDI ports
pub midi_outs: Vec<Port<MidiOut>>,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
/// MIDI output buffer
pub note_buf: Vec<u8>,
}
impl From<&ClockModel> for PhrasePlayerModel {
fn from (clock: &ClockModel) -> Self {
Self {
clock: clock.clone(),
midi_ins: vec![],
midi_outs: vec![],
note_buf: vec![0;8],
reset: true,
recording: false,
monitoring: false,
overdub: false,
play_phrase: None,
next_phrase: None,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
}
}
}

View file

@ -1,62 +1,70 @@
use crate::*; use crate::*;
pub struct TransportView { /// Layout for standalone arranger app.
pub(crate) state: Option<TransportState>, impl Content for ArrangerTui {
pub(crate) selected: Option<TransportFocus>, type Engine = Tui;
pub(crate) focused: bool, fn content (&self) -> impl Widget<Engine = Tui> {
pub(crate) bpm: f64, let arranger_focused = self.arranger_focused();
pub(crate) sync: f64, Split::down(
pub(crate) quant: f64, 1,
pub(crate) beat: String, TransportView::from(self),
pub(crate) msu: String, Split::down(
} self.splits[0],
impl<'a, T> From<&'a T> for TransportView lay!(
where Layers::new(move |add|{
T: ClockApi, match self.mode {
Option<TransportFocus>: From<&'a T> ArrangerMode::Horizontal =>
{ add(&arranger_content_horizontal(self))?,
fn from (state: &'a T) -> Self { ArrangerMode::Vertical(factor) =>
let selected = state.into(); add(&arranger_content_vertical(self, factor))?
Self { };
selected, add(&self.size)
focused: selected.is_some(), })
state: state.transport_state().read().unwrap().clone(), .grow_y(1)
bpm: state.bpm().get(), .border(Lozenge(Style::default()
sync: state.sync().get(), .bg(TuiTheme::border_bg())
quant: state.quant().get(), .fg(TuiTheme::border_fg(arranger_focused)))),
beat: state.current().format_beat(), widget(&self.size),
msu: state.current().usec.format_msu(), widget(&format!("[{}] Arranger", if self.entered {
} ""
} } else {
} " "
impl From<&TransportTui> for Option<TransportFocus> { }))
fn from (state: &TransportTui) -> Self { .fg(TuiTheme::title_fg(arranger_focused))
match state.focus.inner() { .push_x(1),
AppFocus::Content(focus) => Some(focus), ),
_ => None Split::right(
} self.splits[1],
} PhrasesView(self),
} PhraseView(self),
impl From<&SequencerTui> for Option<TransportFocus> { )
fn from (state: &SequencerTui) -> Self { )
match state.focus.inner() { )
AppFocus::Content(SequencerFocus::Transport(focus)) => Some(focus),
_ => None
}
}
}
impl From<&ArrangerTui> for Option<TransportFocus> {
fn from (state: &ArrangerTui) -> Self {
match state.focus.inner() {
AppFocus::Content(ArrangerFocus::Transport(focus)) => Some(focus),
_ => None
}
} }
} }
pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T); /// Display mode of arranger
#[derive(Clone, PartialEq)]
pub enum ArrangerMode {
/// Tracks are rows
Horizontal,
/// Tracks are columns
Vertical(usize),
}
pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T); /// 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 { pub trait ArrangerViewState {
fn arranger_focused (&self) -> bool; fn arranger_focused (&self) -> bool;
@ -67,28 +75,6 @@ impl ArrangerViewState for ArrangerTui {
} }
} }
pub trait PhrasesViewState: Send + Sync {
fn phrases_focused (&self) -> bool;
fn phrases_entered (&self) -> bool;
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>>;
fn phrase_index (&self) -> usize;
fn phrases_mode (&self) -> &Option<PhrasesMode>;
}
pub trait PhraseViewState: Send + Sync {
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>>;
fn phrase_editor_focused (&self) -> bool;
fn phrase_editor_size (&self) -> &Measure<Tui>;
fn phrase_editor_entered (&self) -> bool;
fn keys (&self) -> &Buffer;
fn buffer (&self) -> &BigBuffer;
fn note_len (&self) -> usize;
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
fn now (&self) -> &Arc<Pulse>;
fn size (&self) -> &Measure<Tui>;
}
fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> { fn track_widths (tracks: &[ArrangerTrack]) -> Vec<(usize, usize)> {
let mut widths = vec![]; let mut widths = vec![];
let mut total = 0; let mut total = 0;
@ -480,153 +466,3 @@ pub fn arranger_content_horizontal (
) )
) )
} }
/// Colors of piano keys
const KEY_COLORS: [(Color, Color);6] = [
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
];
pub(crate) fn keys_vert () -> Buffer {
let area = [0, 0, 5, 64];
let mut buffer = Buffer::empty(Rect {
x: area.x(), y: area.y(), width: area.w(), height: area.h()
});
buffer_update(&mut buffer, area, &|cell, x, y| {
let y = 63 - y;
match x {
0 => {
cell.set_char('▀');
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
cell.set_fg(fg);
cell.set_bg(bg);
},
1 => {
cell.set_char('▀');
cell.set_fg(Color::White);
cell.set_bg(Color::White);
},
2 => if y % 6 == 0 {
cell.set_char('C');
},
3 => if y % 6 == 0 {
cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]);
},
_ => {}
}
});
buffer
}
const NTH_OCTAVE: [&'static str; 11] = [
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
];
impl PhraseEditorModel {
pub fn put (&mut self) {
if let (Some(phrase), Some(time), Some(note)) = (
&self.phrase,
self.time_axis.read().unwrap().point,
self.note_axis.read().unwrap().point,
) {
let mut phrase = phrase.write().unwrap();
let key: u7 = u7::from((127 - note) as u8);
let vel: u7 = 100.into();
let start = time;
let end = (start + self.note_len) % phrase.length;
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
self.buffer = Self::redraw(&phrase);
}
}
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
pub fn show (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
if let Some(phrase) = phrase {
self.phrase = Some(phrase.clone());
self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length);
self.buffer = Self::redraw(&*phrase.read().unwrap());
} else {
self.phrase = None;
self.time_axis.write().unwrap().clamp = Some(0);
self.buffer = Default::default();
}
}
fn redraw (phrase: &Phrase) -> BigBuffer {
let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq);
Self::fill_seq_fg(&mut buf, &phrase);
buf
}
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) {
for x in 0..buf.width {
// Only fill as far as phrase length
if x as usize >= length { break }
// Fill each row with background characters
for y in 0 .. buf.height {
buf.get_mut(x, y).map(|cell|{
cell.set_char(if ppq == 0 {
'·'
} else if x % (4 * ppq) == 0 {
'│'
} else if x % ppq == 0 {
'╎'
} else {
'·'
});
cell.set_fg(Color::Rgb(48, 64, 56));
cell.modifier = Modifier::DIM;
});
}
}
}
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) {
let mut notes_on = [false;128];
for x in 0..buf.width {
if x as usize >= phrase.length {
break
}
if let Some(notes) = phrase.notes.get(x as usize) {
if phrase.percussive {
for note in notes {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,
_ => {}
}
}
} else {
for note in notes {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,
MidiMessage::NoteOff { key, .. } =>
notes_on[key.as_int() as usize] = false,
_ => {}
}
}
}
for y in 0..buf.height {
if y >= 64 {
break
}
if let Some(block) = half_block(
notes_on[y as usize * 2],
notes_on[y as usize * 2 + 1],
) {
buf.get_mut(x, y).map(|cell|{
cell.set_char(block);
cell.set_fg(Color::White);
});
}
}
if phrase.percussive {
notes_on.fill(false);
}
}
}
}
}

View file

@ -0,0 +1,24 @@
use crate::*;
impl Content for FileBrowser {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Stack::down(|add|{
let mut i = 0;
for (_, name) in self.dirs.iter() {
if i >= self.scroll {
add(&TuiStyle::bold(name.as_str(), i == self.index))?;
}
i += 1;
}
for (_, name) in self.files.iter() {
if i >= self.scroll {
add(&TuiStyle::bold(name.as_str(), i == self.index))?;
}
i += 1;
}
add(&format!("{}/{i}", self.index))?;
Ok(())
})
}
}

View file

@ -0,0 +1,365 @@
use crate::*;
impl Widget for PhraseEditorModel {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
PhraseView(self).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
PhraseView(self).render(to)
}
}
pub struct PhraseView<'a, T: PhraseViewState>(pub &'a T);
impl<'a, T: PhraseViewState> Content for PhraseView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let phrase = self.0.phrase_editing();
let size = self.0.size();
let focused = self.0.phrase_editor_focused();
let entered = self.0.phrase_editor_entered();
let keys = self.0.keys();
let buffer = self.0.buffer();
let note_len = self.0.note_len();
let note_axis = self.0.note_axis();
let time_axis = self.0.time_axis();
let FixedAxis { start: note_start, point: note_point, clamp: note_clamp }
= *note_axis.read().unwrap();
let ScaledAxis { start: time_start, point: time_point, clamp: time_clamp, scale: time_scale }
= *time_axis.read().unwrap();
//let color = Color::Rgb(0,255,0);
//let color = phrase.as_ref().map(|p|p.read().unwrap().color.base.rgb).unwrap_or(color);
let keys = CustomWidget::new(|to:[u16;2]|Ok(Some(to.clip_w(5))), move|to: &mut TuiOutput|{
Ok(if to.area().h() >= 2 {
to.buffer_update(to.area().set_w(5), &|cell, x, y|{
let y = y + (note_start / 2) as u16;
if x < keys.area.width && y < keys.area.height {
*cell = keys.get(x, y).clone()
}
});
})
}).fill_y();
let notes_bg_null = Color::Rgb(28, 35, 25);
let notes = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
let area = to.area();
let h = area.h() as usize;
size.set_wh(area.w(), h);
let mut axis = note_axis.write().unwrap();
if let Some(point) = axis.point {
if point.saturating_sub(axis.start) > (h * 2).saturating_sub(1) {
axis.start += 2;
}
}
Ok(if to.area().h() >= 2 {
let area = to.area();
to.buffer_update(area, &move |cell, x, y|{
cell.set_bg(notes_bg_null);
let src_x = (x as usize + time_start) * time_scale;
let src_y = y as usize + note_start / 2;
if src_x < buffer.width && src_y < buffer.height - 1 {
buffer.get(src_x, buffer.height - src_y - 2).map(|src|{
cell.set_symbol(src.symbol());
cell.set_fg(src.fg);
cell.set_bg(src.bg);
});
}
});
})
}).fill_x();
let cursor = CustomWidget::new(|to|Ok(Some(to)), move|to: &mut TuiOutput|{
Ok(if focused && entered {
let area = to.area();
if let (Some(time), Some(note)) = (time_point, note_point) {
let x1 = area.x() + (time / time_scale) as u16;
let x2 = x1 + (note_len / time_scale) as u16;
let y = area.y() + note.saturating_sub(note_start) as u16 / 2;
let c = if note % 2 == 0 { "" } else { "" };
for x in x1..x2 {
to.blit(&c, x, y, Some(Style::default().fg(Color::Rgb(0,255,0))));
}
}
})
});
let playhead_inactive = Style::default().fg(Color::Rgb(255,255,255)).bg(Color::Rgb(40,50,30));
let playhead_active = playhead_inactive.clone().yellow().bold().not_dim();
let playhead = CustomWidget::new(
|to:[u16;2]|Ok(Some(to.clip_h(1))),
move|to: &mut TuiOutput|{
if let Some(_) = phrase {
let now = self.0.now().get() as usize; // TODO FIXME: self.now % phrase.read().unwrap().length;
let time_clamp = time_clamp
.expect("time_axis of sequencer expected to be clamped");
for x in 0..(time_clamp/time_scale).saturating_sub(time_start) {
let this_step = time_start + (x + 0) * time_scale;
let next_step = time_start + (x + 1) * time_scale;
let x = to.area().x() + x as u16;
let active = this_step <= now && now < next_step;
let character = if active { "|" } else { "·" };
let style = if active { playhead_active } else { playhead_inactive };
to.blit(&character, x, to.area.y(), Some(style));
}
}
Ok(())
}
).push_x(6).align_sw();
let border_color = if focused{Color::Rgb(100, 110, 40)}else{Color::Rgb(70, 80, 50)};
let title_color = if focused{Color::Rgb(150, 160, 90)}else{Color::Rgb(120, 130, 100)};
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
let note_area = lay!(notes, cursor).fill_x();
let piano_roll = row!(keys, note_area).fill_x();
let content = piano_roll.bg(Color::Rgb(40, 50, 30)).border(border);
let content = lay!(content, playhead);
let mut upper_left = format!("[{}] Sequencer", if entered {""} else {" "});
if let Some(phrase) = phrase {
upper_left = format!("{upper_left}: {}", phrase.read().unwrap().name);
}
let mut lower_right = format!("{}", size.format());
lower_right = format!("┤Zoom: {}├─{lower_right}", pulses_to_name(time_scale));
//lower_right = format!("Zoom: {} (+{}:{}*{}|{})",
//pulses_to_name(time_scale),
//time_start, time_point.unwrap_or(0),
//time_scale, time_clamp.unwrap_or(0),
//);
if focused && entered {
lower_right = format!("┤Note: {} {}├─{lower_right}",
note_axis.read().unwrap().point.unwrap(),
pulses_to_name(note_len));
//lower_right = format!("Note: {} (+{}:{}|{}) {upper_right}",
//pulses_to_name(*note_len),
//note_start,
//note_point.unwrap_or(0),
//note_clamp.unwrap_or(0),
//);
}
let upper_right = if let Some(phrase) = phrase {
format!("┤Length: {}", phrase.read().unwrap().length)
} else {
String::new()
};
lay!(
content,
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
TuiStyle::fg(lower_right.to_string(), title_color).pull_x(1).align_se().fill_xy(),
)
}
}
pub trait PhraseViewState: Send + Sync {
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>>;
fn phrase_editor_focused (&self) -> bool;
fn phrase_editor_size (&self) -> &Measure<Tui>;
fn phrase_editor_entered (&self) -> bool;
fn keys (&self) -> &Buffer;
fn buffer (&self) -> &BigBuffer;
fn note_len (&self) -> usize;
fn note_axis (&self) -> &RwLock<FixedAxis<usize>>;
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>>;
fn now (&self) -> &Arc<Pulse>;
fn size (&self) -> &Measure<Tui>;
}
macro_rules! impl_phrase_view_state {
($Struct:ident $(:: $field:ident)* [$self1:ident : $focused:expr] [$self2:ident : $entered:expr]) => {
impl PhraseViewState for $Struct {
fn phrase_editing (&self) -> &Option<Arc<RwLock<Phrase>>> {
&self$(.$field)*.phrase
}
fn phrase_editor_focused (&$self1) -> bool {
$focused
//self$(.$field)*.focus.is_focused()
}
fn phrase_editor_entered (&$self2) -> bool {
$entered
//self$(.$field)*.focus.is_entered()
}
fn phrase_editor_size (&self) -> &Measure<Tui> {
todo!()
}
fn keys (&self) -> &Buffer {
&self$(.$field)*.keys
}
fn buffer (&self) -> &BigBuffer {
&self$(.$field)*.buffer
}
fn note_len (&self) -> usize {
self$(.$field)*.note_len
}
fn note_axis (&self) -> &RwLock<FixedAxis<usize>> {
&self$(.$field)*.note_axis
}
fn time_axis (&self) -> &RwLock<ScaledAxis<usize>> {
&self$(.$field)*.time_axis
}
fn now (&self) -> &Arc<Pulse> {
&self$(.$field)*.now
}
fn size (&self) -> &Measure<Tui> {
&self$(.$field)*.size
}
}
}
}
impl_phrase_view_state!(PhraseEditorModel
[self: true]
[self: true]
);
impl_phrase_view_state!(SequencerTui::editor
[self: self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)]
[self: self.entered() && self.focused() == AppFocus::Content(SequencerFocus::PhraseEditor)]
);
impl_phrase_view_state!(ArrangerTui::editor
[self: self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)]
[self: self.entered() && self.focused() == AppFocus::Content(ArrangerFocus::PhraseEditor)])
;
/// Colors of piano keys
const KEY_COLORS: [(Color, Color);6] = [
(Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
(Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
];
pub(crate) fn keys_vert () -> Buffer {
let area = [0, 0, 5, 64];
let mut buffer = Buffer::empty(Rect {
x: area.x(), y: area.y(), width: area.w(), height: area.h()
});
buffer_update(&mut buffer, area, &|cell, x, y| {
let y = 63 - y;
match x {
0 => {
cell.set_char('▀');
let (fg, bg) = KEY_COLORS[((6 - y % 6) % 6) as usize];
cell.set_fg(fg);
cell.set_bg(bg);
},
1 => {
cell.set_char('▀');
cell.set_fg(Color::White);
cell.set_bg(Color::White);
},
2 => if y % 6 == 0 {
cell.set_char('C');
},
3 => if y % 6 == 0 {
cell.set_symbol(NTH_OCTAVE[(y / 6) as usize]);
},
_ => {}
}
});
buffer
}
const NTH_OCTAVE: [&'static str; 11] = [
"-2", "-1", "0", "1", "2", "3", "4", "5", "6", "7", "8",
];
impl PhraseEditorModel {
pub fn put (&mut self) {
if let (Some(phrase), Some(time), Some(note)) = (
&self.phrase,
self.time_axis.read().unwrap().point,
self.note_axis.read().unwrap().point,
) {
let mut phrase = phrase.write().unwrap();
let key: u7 = u7::from((127 - note) as u8);
let vel: u7 = 100.into();
let start = time;
let end = (start + self.note_len) % phrase.length;
phrase.notes[time].push(MidiMessage::NoteOn { key, vel });
phrase.notes[end].push(MidiMessage::NoteOff { key, vel });
self.buffer = Self::redraw(&phrase);
}
}
/// Select which pattern to display. This pre-renders it to the buffer at full resolution.
pub fn show (&mut self, phrase: Option<Arc<RwLock<Phrase>>>) {
if let Some(phrase) = phrase {
self.phrase = Some(phrase.clone());
self.time_axis.write().unwrap().clamp = Some(phrase.read().unwrap().length);
self.buffer = Self::redraw(&*phrase.read().unwrap());
} else {
self.phrase = None;
self.time_axis.write().unwrap().clamp = Some(0);
self.buffer = Default::default();
}
}
fn redraw (phrase: &Phrase) -> BigBuffer {
let mut buf = BigBuffer::new(usize::MAX.min(phrase.length), 65);
Self::fill_seq_bg(&mut buf, phrase.length, phrase.ppq);
Self::fill_seq_fg(&mut buf, &phrase);
buf
}
fn fill_seq_bg (buf: &mut BigBuffer, length: usize, ppq: usize) {
for x in 0..buf.width {
// Only fill as far as phrase length
if x as usize >= length { break }
// Fill each row with background characters
for y in 0 .. buf.height {
buf.get_mut(x, y).map(|cell|{
cell.set_char(if ppq == 0 {
'·'
} else if x % (4 * ppq) == 0 {
'│'
} else if x % ppq == 0 {
'╎'
} else {
'·'
});
cell.set_fg(Color::Rgb(48, 64, 56));
cell.modifier = Modifier::DIM;
});
}
}
}
fn fill_seq_fg (buf: &mut BigBuffer, phrase: &Phrase) {
let mut notes_on = [false;128];
for x in 0..buf.width {
if x as usize >= phrase.length {
break
}
if let Some(notes) = phrase.notes.get(x as usize) {
if phrase.percussive {
for note in notes {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,
_ => {}
}
}
} else {
for note in notes {
match note {
MidiMessage::NoteOn { key, .. } =>
notes_on[key.as_int() as usize] = true,
MidiMessage::NoteOff { key, .. } =>
notes_on[key.as_int() as usize] = false,
_ => {}
}
}
}
for y in 0..buf.height {
if y >= 64 {
break
}
if let Some(block) = half_block(
notes_on[y as usize * 2],
notes_on[y as usize * 2 + 1],
) {
buf.get_mut(x, y).map(|cell|{
cell.set_char(block);
cell.set_fg(Color::White);
});
}
}
if phrase.percussive {
notes_on.fill(false);
}
}
}
}
}

View file

@ -0,0 +1,35 @@
use crate::*;
impl Content for PhraseLength {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
Layers::new(move|add|{
match self.focus {
None => add(&row!(
" ", self.bars_string(),
".", self.beats_string(),
".", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Bar) => add(&row!(
"[", self.bars_string(),
"]", self.beats_string(),
".", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Beat) => add(&row!(
" ", self.bars_string(),
"[", self.beats_string(),
"]", self.ticks_string(),
" "
)),
Some(PhraseLengthFocus::Tick) => add(&row!(
" ", self.bars_string(),
".", self.beats_string(),
"[", self.ticks_string(),
"]"
)),
}
})
}
}

View file

@ -0,0 +1,116 @@
use crate::*;
impl Widget for PhrasesModel {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
PhrasesView(self).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
PhrasesView(self).render(to)
}
}
pub struct PhrasesView<'a, T: PhrasesViewState>(pub &'a T);
// TODO: Display phrases always in order of appearance
impl<'a, T: PhrasesViewState> Content for PhrasesView<'a, T> {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let focused = self.0.phrases_focused();
let entered = self.0.phrases_entered();
let mode = self.0.phrases_mode();
let content = Stack::down(move|add|match mode {
Some(PhrasesMode::Import(_, ref browser)) => {
add(browser)
},
Some(PhrasesMode::Export(_, ref browser)) => {
add(browser)
},
_ => {
let phrases = self.0.phrases();
let selected = self.0.phrase_index();
for (i, phrase) in phrases.iter().enumerate() {
add(&Layers::new(|add|{
let Phrase { ref name, color, length, .. } = *phrase.read().unwrap();
let mut length = PhraseLength::new(length, None);
if let Some(PhrasesMode::Length(phrase, new_length, focus)) = mode {
if focused && i == *phrase {
length.pulses = *new_length;
length.focus = Some(*focus);
}
}
let length = length.align_e().fill_x();
let row1 = lay!(format!(" {i}").align_w().fill_x(), length).fill_x();
let mut row2 = format!(" {name}");
if let Some(PhrasesMode::Rename(phrase, _)) = mode {
if focused && i == *phrase {
row2 = format!("{row2}");
}
};
let row2 = TuiStyle::bold(row2, true);
add(&col!(row1, row2).fill_x().bg(color.base.rgb))?;
if focused && i == selected {
add(&CORNERS)?;
}
Ok(())
}))?;
}
Ok(())
}
});
let border_color = if focused {Color::Rgb(100, 110, 40)} else {Color::Rgb(70, 80, 50)};
let border = Lozenge(Style::default().bg(Color::Rgb(40, 50, 30)).fg(border_color));
let content = content.fill_xy().bg(Color::Rgb(28, 35, 25)).border(border);
let title_color = if focused {Color::Rgb(150, 160, 90)} else {Color::Rgb(120, 130, 100)};
let upper_left = format!("[{}] Phrases", if entered {""} else {" "});
let upper_right = format!("({})", self.0.phrases().len());
lay!(
content,
TuiStyle::fg(upper_left.to_string(), title_color).push_x(1).align_nw().fill_xy(),
TuiStyle::fg(upper_right.to_string(), title_color).pull_x(1).align_ne().fill_xy(),
)
}
}
pub trait PhrasesViewState: Send + Sync {
fn phrases_focused (&self) -> bool;
fn phrases_entered (&self) -> bool;
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>>;
fn phrase_index (&self) -> usize;
fn phrases_mode (&self) -> &Option<PhrasesMode>;
}
macro_rules! impl_phrases_view_state {
($Struct:ident $(:: $field:ident)* [$self1:ident: $focus:expr] [$self2:ident: $enter:expr]) => {
impl PhrasesViewState for $Struct {
fn phrases_focused (&$self1) -> bool {
$focus
}
fn phrases_entered (&$self2) -> bool {
$enter
}
fn phrases (&self) -> &Vec<Arc<RwLock<Phrase>>> {
&self$(.$field)*.phrases
}
fn phrase_index (&self) -> usize {
self$(.$field)*.phrase.load(Ordering::Relaxed)
}
fn phrases_mode (&self) -> &Option<PhrasesMode> {
&self$(.$field)*.mode
}
}
}
}
impl_phrases_view_state!(PhrasesModel
[self: false]
[self: false]
);
impl_phrases_view_state!(SequencerTui::phrases
[self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)]
[self: self.focused() == AppFocus::Content(SequencerFocus::Phrases)]
);
impl_phrases_view_state!(ArrangerTui::phrases
[self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)]
[self: self.focused() == AppFocus::Content(ArrangerFocus::Phrases)]
);

View file

@ -0,0 +1,20 @@
use crate::*;
impl Content for SequencerTui {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
lay!(
col!(
TransportView::from(self),
Split::right(20,
widget(&PhrasesView(self)),
widget(&PhraseView(self)),
).min_y(20)
),
self.perf.percentage()
.map(|cpu|format!("{cpu:.03}%"))
.fg(Color::Rgb(255,128,0))
.align_sw(),
)
}
}

View file

@ -0,0 +1,102 @@
use crate::*;
impl Widget for TransportTui {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
TransportView::from(self).layout(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,
}
impl Content for TransportView {
type Engine = Tui;
fn content (&self) -> impl Widget<Engine = Tui> {
let Self { state, selected, focused, bpm, sync, quant, beat, msu, } = self;
row!(
selected.wrap(TransportFocus::PlayPause, &Styled(
None,
match *state {
Some(TransportState::Rolling) => "▶ PLAYING",
Some(TransportState::Starting) => "READY ...",
Some(TransportState::Stopped) => "⏹ STOPPED",
_ => "???",
}
).min_xy(11, 2).push_x(1)),
selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, {
row! {
"BPM ",
format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
}
})),
selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! {
"QUANT ", pulses_to_name(*quant as usize)
})),
selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! {
"SYNC ", pulses_to_name(*sync 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))
}
}
impl<'a, T> From<&'a T> for TransportView
where
T: ClockApi,
Option<TransportFocus>: From<&'a T>
{
fn from (state: &'a T) -> Self {
let selected = state.into();
Self {
selected,
focused: selected.is_some(),
state: state.transport_state().read().unwrap().clone(),
bpm: state.bpm().get(),
sync: state.sync().get(),
quant: state.quant().get(),
beat: state.current().format_beat(),
msu: state.current().usec.format_msu(),
}
}
}
impl From<&TransportTui> for Option<TransportFocus> {
fn from (state: &TransportTui) -> Self {
match state.focus.inner() {
AppFocus::Content(focus) => Some(focus),
_ => None
}
}
}
impl From<&SequencerTui> for Option<TransportFocus> {
fn from (state: &SequencerTui) -> Self {
match state.focus.inner() {
AppFocus::Content(SequencerFocus::Transport(focus)) => Some(focus),
_ => None
}
}
}
impl From<&ArrangerTui> for Option<TransportFocus> {
fn from (state: &ArrangerTui) -> Self {
match state.focus.inner() {
AppFocus::Content(ArrangerFocus::Transport(focus)) => Some(focus),
_ => None
}
}
}

View file

@ -1,31 +0,0 @@
use crate::*;
impl Widget for TransportTui {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
TransportView::from(self).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
TransportView::from(self).render(to)
}
}
impl Widget for PhrasesModel {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
PhrasesView(self).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
PhrasesView(self).render(to)
}
}
impl Widget for PhraseEditorModel {
type Engine = Tui;
fn layout (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
PhraseView(self).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
PhraseView(self).render(to)
}
}