unify edn_view entrypoint

This commit is contained in:
🪞👃🪞 2025-01-14 15:39:28 +01:00
parent df50bb9f47
commit 9cd6e9f195
16 changed files with 603 additions and 541 deletions

View file

@ -0,0 +1 @@
fn main () {}

View file

@ -4,7 +4,9 @@ use EdnItem::*;
pub struct EdnKeymap(pub Vec<EdnItem<String>>);
impl EdnKeymap {
pub fn command <C, D: Command<C>, E: EdnCommand<C>, I: EdnInput> (&self, state: &C, input: &I) -> Option<E> {
pub fn command <C, D: Command<C>, E: EdnCommand<C>, I: EdnInput> (
&self, state: &C, input: &I
) -> Option<E> {
for item in self.0.iter() {
if let Exp(items) = item {
match items.as_slice() {
@ -20,3 +22,13 @@ impl EdnKeymap {
None
}
}
impl<T: AsRef<str>> From<T> for EdnKeymap {
fn from (source: T) -> Self {
Self(EdnItem::<String>::read_all(source.as_ref()).expect("failed to load keymap"))
}
}
#[cfg(test)] #[test] fn test_edn_keymap () {
let keymap = EdnKeymap::from("");
}

View file

@ -1,18 +1,19 @@
use tek::*;
use tek_midi::*;
use std::sync::*;
struct ExampleClips(Vec<Arc<RwLock<Clip>>>);
struct ExampleClips(Arc<RwLock<Vec<Arc<RwLock<MidiClip>>>>>);
impl HasClips for ExampleClips {
fn phrases (&self) -> &Vec<Arc<RwLock<Clip>>> {
&self.0
fn clips (&self) -> RwLockReadGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
self.0.read().unwrap()
}
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<Clip>>> {
&mut self.0
fn clips_mut (&self) -> RwLockWriteGuard<'_, Vec<Arc<RwLock<MidiClip>>>> {
self.0.write().unwrap()
}
}
fn main () -> Usually<()> {
let mut phrases = ExampleClips(vec![]);
PoolClipCommand::Import(0, String::from("./example.mid")).execute(&mut phrases)?;
fn main () -> Result<(), Box<dyn std::error::Error>> {
let mut clips = ExampleClips(Arc::new(vec![].into()));
PoolClipCommand::Import(0, String::from("./example.mid")).execute(&mut clips)?;
Ok(())
}

View file

@ -7,11 +7,25 @@ pub trait HasEditor {
fn editor_h (&self) -> usize { 0 }
}
#[macro_export] macro_rules! has_editor {
(|$self:ident: $Struct:ident|{
editor = $e0:expr;
editor_w = $e1:expr;
editor_h = $e2:expr;
is_editing = $e3:expr;
}) => {
impl HasEditor for $Struct {
fn editor (&$self) -> &Option<MidiEditor> { &$e0 }
fn editor_mut (&mut $self) -> &Option<MidiEditor> { &mut $e0 }
fn editor_w (&$self) -> usize { $e1 }
fn editor_h (&$self) -> usize { $e2 }
fn is_editing (&$self) -> bool { $e3 }
}
};
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
fn editor (&$self) -> &MidiEditor { &$cb }
}
}
};
}
/// Contains state for viewing and editing a clip
pub struct MidiEditor {
@ -32,7 +46,7 @@ impl Default for MidiEditor {
mode: PianoHorizontal::new(None),
size: Measure::new(),
keymap: EdnKeymap(
EdnItem::<String>::read_all(include_str!("../edn/midi-keys.edn"))
EdnItem::<String>::read_all(include_str!("midi_editor_keys.edn"))
.expect("failed to load keymap for MidiEditor")
)
}

View file

@ -1,7 +1,8 @@
use tek::*;
use tek_tui::{*, tek_input::*, tek_output::*};
use tek_edn::*;
use std::sync::{Arc, RwLock};
use crossterm::event::{*, KeyCode::*};
use crate::ratatui::style::Color;
const EDN: &'static [&'static str] = &[
include_str!("edn01.edn"),
@ -25,26 +26,15 @@ fn main () -> Usually<()> {
Ok(())
}
pub struct Example(usize, Measure<TuiOut>);
#[derive(Debug)] pub struct Example(usize, Measure<TuiOut>);
impl EdnViewData<TuiOut> for &Example {
fn get_content <'a> (&'a self, item: EdnItem<&'a str>) -> RenderBox<'a, TuiOut> {
use EdnItem::*;
match item {
Nil => Box::new(()),
Exp(items) => Box::new(EdnView::from_items(*self, items.as_slice())),
//Sym(name) => self.get_sym(name), TODO
Sym(":title") => Tui::bg(Color::Rgb(60,10,10), Push::y(1,
Align::n(format!("Example {}/{}:", self.0 + 1, EDN.len())))).boxed(),
Sym(":code") => Tui::bg(Color::Rgb(10,60,10), Push::y(2,
Align::n(format!("{}", EDN[self.0])))).boxed(),
Sym(":hello-world") => "Hello world!".boxed(),
Sym(":hello") => Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed(),
Sym(":world") => Tui::bg(Color::Rgb(100, 10, 10), "world").boxed(),
_ => panic!("no content for {item:?}")
}
}
}
edn_provide_content!(|self: Example|{
":title" => Tui::bg(Color::Rgb(60,10,10), Push::y(1, Align::n(format!("Example {}/{}:", self.0 + 1, EDN.len())))).boxed(),
":code" => Tui::bg(Color::Rgb(10,60,10), Push::y(2, Align::n(format!("{}", EDN[self.0])))).boxed(),
":hello-world" => "Hello world!".boxed(),
":hello" => Tui::bg(Color::Rgb(10, 100, 10), "Hello").boxed(),
":world" => Tui::bg(Color::Rgb(100, 10, 10), "world").boxed(),
});
impl Content<TuiOut> for Example {
fn content (&self) -> impl Render<TuiOut> {

View file

@ -1,11 +1,42 @@
use crate::*;
use std::marker::PhantomData;
use EdnItem::*;
#[macro_export] macro_rules! edn_view {
($Output:ty: |$self:ident: $App:ty| $content:expr; {
$( $type:ty { $($sym:literal => $value:expr),* } );*
}) => {
impl Content<$Output> for $App {
fn content(&$self) -> impl Render<$Output> { $content }
}
$(
impl<'a> EdnProvide<'a, $type> for $App {
fn get <S: AsRef<str>> (&'a $self, edn: &'a EdnItem<S>) -> Option<$type> {
Some(match edn.to_ref() {
$(EdnItem::Sym($sym) => $value,)*
_ => return None
})
}
}
)*
}
}
/// Implements `EdnProvide` for content components and expressions
#[macro_export] macro_rules! edn_provide_content {
(|$self:ident:$Stat:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
impl<'a> EdnProvide<'a, Box<dyn Render<TuiOut> + 'a>> for $State {
fn get <S: AsRef<str>> (&'a $self, edn: &'a EdnItem<S>) -> Option<Box<dyn Render<TuiOut> + 'a>> {
(|$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
impl<'a, E: Output> EdnProvide<'a, Box<dyn Render<E> + 'a>> for $State {
fn get <S: AsRef<str>> (&'a $self, edn: &'a EdnItem<S>) -> Option<Box<dyn Render<E> + 'a>> {
Some(match edn.to_ref() {
$(EdnItem::Sym($pat) => $expr),*,
_ => return None
})
}
}
};
($Output:ty: |$self:ident:$State:ty|{ $($pat:pat => $expr:expr),* $(,)? }) => {
impl<'a> EdnProvide<'a, Box<dyn Render<$Output> + 'a>> for $State {
fn get <S: AsRef<str>> (&'a $self, edn: &'a EdnItem<S>) -> Option<Box<dyn Render<$Output> + 'a>> {
Some(match edn.to_ref() {
$(EdnItem::Sym($pat) => $expr),*,
_ => return None
@ -15,21 +46,23 @@ use EdnItem::*;
}
}
/// Renders from EDN source and context.
#[derive(Default)]
pub enum EdnView<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> {
#[default]
Inert,
_Unused(PhantomData<&'a E>),
#[derive(Default)] pub enum EdnView<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> {
#[default] Inert,
Ok(T, EdnItem<&'a str>),
//render: Box<dyn Fn(&'a T)->Box<dyn Render<E> + Send + Sync + 'a> + Send + Sync + 'a>
Err(String)
Err(String),
_Unused(PhantomData<&'a E>),
}
impl<'a, E: Output, T: EdnViewData<'a, E> + std::fmt::Debug> std::fmt::Debug for EdnView<'a, E, T> {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
match self {
Self::Inert | Self::_Unused(_) => write!(f, "EdnView::Inert"),
Self::Ok(state, view) => write!(f, "EdnView::Ok(state={state:?} view={view:?}"),
Self::Err(error) => write!(f, "EdnView::Err({error})"),
Self::Inert | Self::_Unused(_) =>
write!(f, "EdnView::Inert"),
Self::Ok(state, view) =>
write!(f, "EdnView::Ok(state={state:?} view={view:?}"),
Self::Err(error) =>
write!(f, "EdnView::Err({error})"),
_ => unreachable!()
}
}
}

View file

@ -66,42 +66,35 @@ pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
#[cfg(test)] #[test] fn test_layout () -> Usually<()> {
use ::tek_tui::{*, tek_output::*};
let area: [u16;4] = [10, 10, 20, 20];
let unit = ();
assert_eq!(Content::<TuiOut>::layout(&unit, area), [20, 20, 0, 0]);
assert_eq!(Content::<TuiOut>::layout(&Fill::<()>::x(unit), area), [10, 20, 20, 0]);
assert_eq!(Render::<TuiOut>::layout(&Fill::<()>::x(unit), area), [10, 20, 20, 0]);
assert_eq!(Fill::<()>::y(unit).layout(area), [20, 10, 0, 20]);
assert_eq!(Fill::<()>::xy(unit).layout(area), area);
assert_eq!(Fixed::<TuiOut, u16>::x(4, unit).layout(area), [18, 20, 4, 0]);
assert_eq!(Fixed::<TuiOut, u16>::y(4, unit).layout(area), [20, 18, 0, 4]);
assert_eq!(Fixed::<TuiOut, u16>::xy(4, 4, unit).layout(area), [18, 18, 4, 4]);
let four = ||Fixed::<TuiOut, _>::xy(4, 4, unit);
assert_eq!(Align::nw(four()).layout(area), [10, 10, 4, 4]);
assert_eq!(Align::n(four()).layout(area), [18, 10, 4, 4]);
assert_eq!(Align::ne(four()).layout(area), [26, 10, 4, 4]);
assert_eq!(Align::e(four()).layout(area), [26, 18, 4, 4]);
assert_eq!(Align::se(four()).layout(area), [26, 26, 4, 4]);
assert_eq!(Align::s(four()).layout(area), [18, 26, 4, 4]);
assert_eq!(Align::sw(four()).layout(area), [10, 26, 4, 4]);
assert_eq!(Align::w(four()).layout(area), [10, 18, 4, 4]);
let two_by_four = ||Fixed::<TuiOut, _>::xy(4, 2, unit);
assert_eq!(Align::nw(two_by_four()).layout(area), [10, 10, 4, 2]);
assert_eq!(Align::n(two_by_four()).layout(area), [18, 10, 4, 2]);
assert_eq!(Align::ne(two_by_four()).layout(area), [26, 10, 4, 2]);
assert_eq!(Align::e(two_by_four()).layout(area), [26, 19, 4, 2]);
assert_eq!(Align::se(two_by_four()).layout(area), [26, 28, 4, 2]);
assert_eq!(Align::s(two_by_four()).layout(area), [18, 28, 4, 2]);
assert_eq!(Align::sw(two_by_four()).layout(area), [10, 28, 4, 2]);
assert_eq!(Align::w(two_by_four()).layout(area), [10, 19, 4, 2]);
fn test (area: [u16;4], item: &impl Content<TuiOut>, expected: [u16;4]) {
assert_eq!(Content::layout(item, area), expected);
assert_eq!(Render::layout(item, area), expected);
};
test(area, &(), [20, 20, 0, 0]);
test(area, &Fill::xy(()), area);
test(area, &Fill::x(()), [10, 20, 20, 0]);
test(area, &Fill::y(()), [20, 10, 0, 20]);
test(area, &Fixed::x(4, unit), [18, 20, 4, 0]);
test(area, &Fixed::y(4, unit), [20, 18, 0, 4]);
test(area, &Fixed::xy(4, 4, unit), [18, 18, 4, 4]);
let four = ||Fixed::<TuiOut, _, _>::xy(4, 4, unit);
test(area, &Align::nw(four()), [10, 10, 4, 4]);
test(area, &Align::n(four()), [18, 10, 4, 4]);
test(area, &Align::ne(four()), [26, 10, 4, 4]);
test(area, &Align::e(four()), [26, 18, 4, 4]);
test(area, &Align::se(four()), [26, 26, 4, 4]);
test(area, &Align::s(four()), [18, 26, 4, 4]);
test(area, &Align::sw(four()), [10, 26, 4, 4]);
test(area, &Align::w(four()), [10, 18, 4, 4]);
let two_by_four = ||Fixed::<TuiOut, _, _>::xy(4, 2, unit);
test(area, &Align::nw(two_by_four()), [10, 10, 4, 2]);
test(area, &Align::n(two_by_four()), [18, 10, 4, 2]);
test(area, &Align::ne(two_by_four()), [26, 10, 4, 2]);
test(area, &Align::e(two_by_four()), [26, 19, 4, 2]);
test(area, &Align::se(two_by_four()), [26, 28, 4, 2]);
test(area, &Align::s(two_by_four()), [18, 28, 4, 2]);
test(area, &Align::sw(two_by_four()), [10, 28, 4, 2]);
test(area, &Align::w(two_by_four()), [10, 19, 4, 2]);
Ok(())
}

View file

@ -1,3 +1,3 @@
(bsp/s (fixed/y 2 :toolbar)
(fill/x (align/c (bsp/w :pool
(bsp/n :outputs (bsp/n :inputs (bsp/n :tracks :scenes)))))))
(bsp/s :outputs (bsp/s :inputs (bsp/s :tracks :scenes)))))))

View file

View file

@ -62,74 +62,434 @@ impl HasSampler for App {
fn sampler_mut (&mut self) -> &mut Option<Sampler> { &mut self.sampler }
fn sample_index (&self) -> usize { self.editor.as_ref().map(|e|e.note_point()).unwrap_or(0) }
}
impl HasEditor for App {
fn editor (&self) -> &Option<MidiEditor> { &self.editor }
fn editor_mut (&mut self) -> &Option<MidiEditor> { &mut self.editor }
has_editor!(|self: App|{
editor = self.editor;
editor_w = {
let size = self.size.w();
let editor = self.editor.as_ref().expect("missing editor");
let time_len = editor.time_len().get();
let time_zoom = editor.time_zoom().get().max(1);
(5 + (time_len / time_zoom)).min(size.saturating_sub(20)).max(16)
};
editor_h = 15;
is_editing = self.editing.load(Relaxed);
});
edn_view!(TuiOut: |self: App| self.size.of(EdnView::from_source(self, self.edn.as_ref())); {
bool {};
usize {};
isize {};
Color {};
Selection {};
Arc<RwLock<MidiClip>> {};
Option<Arc<RwLock<MidiClip>>> {};
u16 {
":sidebar-w" => self.sidebar_w(),
":sample-h" => if self.compact() { 0 } else { 5 },
":samples-w" => if self.compact() { 4 } else { 11 },
":samples-y" => if self.compact() { 1 } else { 0 },
":pool-w" => if self.compact() { 5 } else {
let w = self.size.w();
if w > 60 { 20 } else if w > 40 { 15 } else { 10 } } };
Box<dyn Render<TuiOut> + 'a> {
":editor" => (&self.editor).boxed(),
":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(),
":sample" => self.view_sample(self.is_editing()).boxed(),
":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(),
":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
":toolbar" => ClockView::new(true, &self.clock).boxed(),
":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(),
":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(),
":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(),
":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16,
self.scene_header(), self.scene_cells(self.is_editing())).boxed() }});
//content!(TuiOut:|self: App|self.size.of(EdnView::from_source(self, self.edn.as_ref())));
//edn_provide!(bool: |self: App| { _ => return None });
//edn_provide!(usize: |self: App| { _ => return None });
//edn_provide!(isize: |self: App| { _ => return None });
//edn_provide!(Color: |self: App| { _ => return None });
//edn_provide!(Selection: |self: App| { _ => return None });
//edn_provide!(Arc<RwLock<MidiClip>>: |self: App| { _ => return None });
//edn_provide!(Option<Arc<RwLock<MidiClip>>>: |self: App| { _ => return None });
//edn_provide!('a: Box<dyn Render<TuiOut> + 'a>: |self: App|{
//":editor" => (&self.editor).boxed(),
//":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(),
//":sample" => self.view_sample(self.is_editing()).boxed(),
//":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(),
//":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
//":toolbar" => ClockView::new(true, &self.clock).boxed(),
//":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(),
//":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(),
//":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(),
//":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16,
//self.scene_header(), self.scene_cells(self.is_editing())).boxed() });
//edn_provide!(u16: |self: App|{
//":sidebar-w" => self.sidebar_w(),
//":sample-h" => if self.compact() { 0 } else { 5 },
//":samples-w" => if self.compact() { 4 } else { 11 },
//":samples-y" => if self.compact() { 1 } else { 0 },
//":pool-w" => if self.compact() { 5 } else {
//let w = self.size.w();
//if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
//}
//});
impl App {
pub fn sequencer (
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
player: Option<MidiPlayer>, midi_froms: &[PortConnection], midi_tos: &[PortConnection],
) -> Self {
Self {
edn: include_str!("./sequencer-view.edn").to_string(),
jack: jack.clone(),
pool: Some(pool),
editor: Some(editor),
player: player,
editing: false.into(),
midi_buf: vec![vec![];65536],
color: ItemPalette::random(),
..Default::default()
}
}
pub fn groovebox (
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
player: Option<MidiPlayer>, midi_froms: &[PortConnection], midi_tos: &[PortConnection],
sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]],
) -> Self {
Self {
edn: include_str!("./groovebox-view.edn").to_string(),
sampler: Some(sampler),
..Self::sequencer(
jack, pool, editor,
player, midi_froms, midi_tos
)
}
}
pub fn arranger (
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
midi_froms: &[PortConnection], midi_tos: &[PortConnection],
sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]],
scenes: usize, tracks: usize, track_width: usize,
) -> Self {
let mut arranger = Self {
edn: include_str!("./arranger-view.edn").to_string(),
..Self::groovebox(
jack, pool, editor,
None, midi_froms, midi_tos,
sampler, audio_froms, audio_tos
)
};
arranger.scenes_add(scenes);
arranger.tracks_add(tracks, track_width, &[], &[]);
arranger
}
fn compact (&self) -> bool { false }
fn is_editing (&self) -> bool { self.editing.load(Relaxed) }
fn editor_h (&self) -> usize { 15 }
fn editor_w (&self) -> usize {
let editor = self.editor.as_ref().expect("missing editor");
(5 + (editor.time_len().get() / editor.time_zoom().get()))
.min(self.size.w().saturating_sub(20))
.max(16)
fn editor (&self) -> impl Content<TuiOut> + '_ { &self.editor }
fn w (&self) -> u16 { self.tracks_sizes(self.is_editing(), self.editor_w()).last().map(|x|x.3 as u16).unwrap_or(0) }
fn pool (&self) -> impl Content<TuiOut> + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) }
fn sidebar_w (&self) -> u16 {
let w = self.size.w();
let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
let w = if self.is_editing() { 8 } else { w };
w
}
fn row <'a> (
&'a self, w: u16, h: u16, a: impl Content<TuiOut> + 'a, b: impl Content<TuiOut> + 'a
) -> impl Content<TuiOut> + 'a {
Fixed::y(h, Bsp::e(
Fixed::xy(self.sidebar_w() as u16, h, a),
Fill::x(Align::c(Fixed::xy(w, h, b)))
))
}
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.scene()?.clips.get(self.selected().track()?)?.clone()
}
fn toggle_loop (&mut self) {
if let Some(clip) = self.clip() {
clip.write().unwrap().toggle_loop()
}
}
//fn randomize_color (&mut self) {
//match self.selected {
//Selection::Mix => { self.color = ItemPalette::random() },
//Selection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
//Selection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
//Selection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] {
//clip.write().unwrap().color = ItemPalette::random();
//}
//}
//}
fn track_add (
&mut self,
name: Option<&str>,
color: Option<ItemPalette>
) -> Usually<&mut Track> {
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
let track = Track {
width: (name.len() + 2).max(9),
color: color.unwrap_or_else(ItemPalette::random),
player: MidiPlayer::from(self.clock()),
name,
..Default::default()
};
self.tracks_mut().push(track);
let len = self.tracks().len();
let index = len - 1;
for scene in self.scenes_mut().iter_mut() {
while scene.clips.len() < len {
scene.clips.push(None);
}
}
Ok(&mut self.tracks_mut()[index])
}
fn tracks_add (
&mut self,
count: usize,
width: usize,
midi_from: &[PortConnection],
midi_to: &[PortConnection],
) -> Usually<()> {
let jack = self.jack().clone();
let track_color_1 = ItemColor::random();
let track_color_2 = ItemColor::random();
for i in 0..count {
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
let mut track = self.track_add(None, Some(color))?;
track.width = width;
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
track.player.midi_ins.push(port);
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
track.player.midi_outs.push(port);
}
Ok(())
}
fn track_del (&mut self, index: usize) {
self.tracks_mut().remove(index);
for scene in self.scenes_mut().iter_mut() {
scene.clips.remove(index);
}
}
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
-> Usually<&mut Scene>
{
let scene = Scene {
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
clips: vec![None;self.tracks().len()],
color: color.unwrap_or_else(ItemPalette::random),
};
self.scenes_mut().push(scene);
let index = self.scenes().len() - 1;
Ok(&mut self.scenes_mut()[index])
}
fn scenes_add (&mut self, n: usize) -> Usually<()> {
let scene_color_1 = ItemColor::random();
let scene_color_2 = ItemColor::random();
for i in 0..n {
let _scene = self.scene_add(None, Some(
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
))?;
}
Ok(())
}
fn scene_cells <'a> (&'a self, editing: bool) -> BoxThunk<'a, TuiOut> {
let tracks = move||self.tracks_sizes(self.is_editing(), self.editor_w());
let scenes = ||self.scenes_sizes(self.is_editing(), 2, 15);
let selected_track = self.selected().track();
let selected_scene = self.selected().scene();
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
let w = (x2 - x1) as u16;
let color: ItemPalette = track.color.dark.into();
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
let h = (y2 - y1) as u16;
let color = scene.color;
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
let c = c.read().unwrap();
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
} else {
("".to_string(), TuiTheme::g(64), TuiTheme::g(32))
};
let last = last_color.read().unwrap().clone();
let active = editing && selected_scene == Some(s) && selected_track == Some(t);
let editor = Thunk::new(||self.editor());
let cell = Thunk::new(move||phat_sel_3(
selected_track == Some(t) && selected_scene == Some(s),
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) {
None
} else {
Some(bg.into())
},
bg.into(),
bg.into(),
));
let cell = Either::new(active, editor, cell);
*last_color.write().unwrap() = bg.into();
map_south(
y1 as u16,
h + 1,
Fill::x(Fixed::y(h + 1, cell))
)
});
let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed());
Fixed::x(w, map_east(x1 as u16, w, column))
}))).boxed()).into()
}
fn activate (&mut self) -> Usually<()> {
let selected = self.selected().clone();
match selected {
Selection::Scene(s) => {
let mut clips = vec![];
for (t, _) in self.tracks().iter().enumerate() {
clips.push(self.scenes()[s].clips[t].clone());
}
for (t, track) in self.tracks_mut().iter_mut().enumerate() {
if track.player.play_clip.is_some() || clips[t].is_some() {
track.player.enqueue_next(clips[t].as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
},
Selection::Clip(t, s) => {
let clip = self.scenes()[s].clips[t].clone();
self.tracks_mut()[t].player.enqueue_next(clip.as_ref());
},
_ => {}
}
Ok(())
}
}
edn_provide!(u16: |self: App|{
":sidebar-w" => self.sidebar_w(),
":sample-h" => if self.compact() { 0 } else { 5 },
":samples-w" => if self.compact() { 4 } else { 11 },
":samples-y" => if self.compact() { 1 } else { 0 },
":pool-w" => if self.compact() { 5 } else {
let w = self.size.w();
if w > 60 { 20 } else if w > 40 { 15 } else { 10 }
handle!(TuiIn: |self: App, input| Ok(None));
#[derive(Clone, Debug)] pub enum AppCommand {
Clear,
Clip(ClipCommand),
Clock(ClockCommand),
Color(ItemPalette),
Compact(Option<bool>),
Editor(MidiEditCommand),
Enqueue(Option<Arc<RwLock<MidiClip>>>),
History(isize),
Pool(PoolCommand),
Sampler(SamplerCommand),
Scene(SceneCommand),
Select(Selection),
StopAll,
Track(TrackCommand),
Zoom(Option<usize>),
}
edn_command!(AppCommand: |state: App| {
("clear" [] Self::Clear)
("stop-all" [] Self::StopAll)
("compact" [c: bool ] Self::Compact(c))
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
("history" [d: isize] Self::History(d.unwrap_or(0)))
("zoom" [z: usize] Self::Zoom(z))
("select" [s: Selection] Self::Select(s.expect("no selection")))
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b)))
("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b)))
("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b)))
("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b)))
("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b)))
("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b)))
("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b)))
});
command!(|self: AppCommand, state: App|match self {
Self::Clear => { todo!() },
Self::Zoom(_) => { todo!(); },
Self::History(delta) => { todo!("undo/redo") },
Self::Select(s) => { state.selected = s; None },
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
Self::Scene(cmd) => match cmd {
SceneCommand::Add => { state.scene_add(None, None)?; None }
SceneCommand::Del(index) => { state.scene_del(index); None },
SceneCommand::SetColor(index, color) => {
let old = state.scenes[index].color;
state.scenes[index].color = color;
Some(SceneCommand::SetColor(index, old))
},
SceneCommand::Enqueue(scene) => {
for track in 0..state.tracks.len() {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
}
None
},
_ => None
}.map(Self::Scene),
Self::Track(cmd) => match cmd {
TrackCommand::Add => { state.track_add(None, None)?; None },
TrackCommand::Del(index) => { state.track_del(index); None },
TrackCommand::Stop(track) => { state.tracks[track].player.enqueue_next(None); None },
TrackCommand::SetColor(index, color) => {
let old = state.tracks[index].color;
state.tracks[index].color = color;
Some(TrackCommand::SetColor(index, old))
},
_ => None
}.map(Self::Track),
Self::Clip(cmd) => match cmd {
ClipCommand::Get(track, scene) => { todo!() },
ClipCommand::Put(track, scene, clip) => {
let old = state.scenes[scene].clips[track].clone();
state.scenes[scene].clips[track] = clip;
Some(ClipCommand::Put(track, scene, old))
},
ClipCommand::Enqueue(track, scene) => {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
None
},
_ => None
}.map(Self::Clip),
Self::Editor(cmd) =>
state.editor.as_mut().map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(),
Self::Sampler(cmd) =>
state.sampler.as_mut().map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(),
Self::Enqueue(clip) =>
state.player.as_mut().map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(),
Self::StopAll => {
for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); }
None
},
Self::Color(palette) => {
let old = state.color;
state.color = palette;
Some(Self::Color(old))
},
Self::Pool(cmd) => if let Some(pool) = state.pool.as_mut() {
let undo = cmd.clone().delegate(pool, Self::Pool)?;
if let Some(editor) = state.editor.as_mut() {
match cmd {
// autoselect: automatically load selected clip in editor
// autocolor: update color in all places simultaneously
PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) =>
editor.set_clip(pool.clip().as_ref()),
_ => {}
}
};
undo
} else {
None
},
Self::Compact(compact) => match compact {
Some(compact) => {
if state.compact != compact {
state.compact = compact;
Some(Self::Compact(Some(!compact)))
} else {
None
}
},
None => {
state.compact = !state.compact;
Some(Self::Compact(Some(!state.compact)))
}
}
});
edn_provide!(bool: |self: App| { _ => return None });
edn_provide!(usize: |self: App| { _ => return None });
edn_provide!(isize: |self: App| { _ => return None });
edn_provide!(Color: |self: App| { _ => return None });
edn_provide!(Selection: |self: App| { _ => return None });
edn_provide!(Arc<RwLock<MidiClip>>: |self: App| { _ => return None });
edn_provide!(Option<Arc<RwLock<MidiClip>>>: |self: App| { _ => return None });
edn_provide!('a: Box<dyn Render<TuiOut> + 'a>: |self: App|{
":editor" => (&self.editor).boxed(),
":pool" => self.pool.as_ref().map(|pool|PoolView(self.compact(), pool)).boxed(),
":sample" => self.view_sample(self.is_editing()).boxed(),
":sampler" => self.view_sampler(self.is_editing(), &self.editor).boxed(),
":status" => self.editor.as_ref().map(|e|Bsp::e(e.clip_status(), e.edit_status())).boxed(),
":toolbar" => ClockView::new(true, &self.clock).boxed(),
":tracks" => self.row(self.w(), 3, self.track_header(), self.track_cells()).boxed(),
":inputs" => self.row(self.w(), 3, self.input_header(), self.input_cells()).boxed(),
":outputs" => self.row(self.w(), 3, self.output_header(), self.output_cells()).boxed(),
":scenes" => self.row(self.w(), self.size.h().saturating_sub(9) as u16,
self.scene_header(), self.scene_cells(self.is_editing())).boxed(),
});
content!(TuiOut: |self: App| self.size.of(EdnView::from_source(self, self.edn.as_ref())));
handle!(TuiIn: |self: App, input| Ok(None));
#[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32);
content!(TuiOut: |self: Meter<'a>| col!(
Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)),
Fixed::xy(if self.1 >= 0.0 { 13 }
else if self.1 >= -1.0 { 12 }
else if self.1 >= -2.0 { 11 }
else if self.1 >= -3.0 { 10 }
else if self.1 >= -4.0 { 9 }
else if self.1 >= -6.0 { 8 }
else if self.1 >= -9.0 { 7 }
else if self.1 >= -12.0 { 6 }
else if self.1 >= -15.0 { 5 }
else if self.1 >= -20.0 { 4 }
else if self.1 >= -25.0 { 3 }
else if self.1 >= -30.0 { 2 }
else if self.1 >= -40.0 { 1 }
else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red }
else if self.1 >= -3.0 { Color::Yellow }
else { Color::Green }, ()))));
#[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]);
content!(TuiOut: |self: Meters<'a>| col!(
format!("L/{:>+9.3}", self.0[0]),
format!("R/{:>+9.3}", self.0[1])
));
/// Represents the current user selection in the arranger
#[derive(PartialEq, Clone, Copy, Debug, Default)] pub enum Selection {
/// The whole mix is selected
@ -209,6 +569,25 @@ impl Track {
}
}
}
#[derive(Clone, Debug)] pub enum TrackCommand {
Add,
Del(usize),
Stop(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
}
edn_command!(TrackCommand: |state: App| {
("add" [] Self::Add)
("size" [a: usize] Self::SetSize(a.unwrap()))
("zoom" [a: usize] Self::SetZoom(a.unwrap()))
("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random()))
("del" [a: usize] Self::Del(a.unwrap()))
("stop" [a: usize] Self::Stop(a.unwrap()))
("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap()))
});
command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") });
impl HasTracks for App {
fn midi_ins (&self) -> &Vec<JackPort<MidiIn>> { &self.midi_ins }
fn midi_outs (&self) -> &Vec<JackPort<MidiOut>> { &self.midi_outs }
@ -449,202 +828,6 @@ pub trait HasScenes: HasSelection + HasEditor + Send + Sync {
}).into()
}
}
impl App {
pub fn sequencer (
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
player: Option<MidiPlayer>, midi_froms: &[PortConnection], midi_tos: &[PortConnection],
) -> Self {
Self {
edn: include_str!("./sequencer-view.edn").to_string(),
jack: jack.clone(),
pool: Some(pool),
editor: Some(editor),
player: player,
editing: false.into(),
midi_buf: vec![vec![];65536],
color: ItemPalette::random(),
..Default::default()
}
}
pub fn groovebox (
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
player: Option<MidiPlayer>, midi_froms: &[PortConnection], midi_tos: &[PortConnection],
sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]],
) -> Self {
Self {
edn: include_str!("./groovebox-view.edn").to_string(),
sampler: Some(sampler),
..Self::sequencer(
jack, pool, editor,
player, midi_froms, midi_tos
)
}
}
pub fn arranger (
jack: &Arc<RwLock<JackConnection>>, pool: MidiPool, editor: MidiEditor,
midi_froms: &[PortConnection], midi_tos: &[PortConnection],
sampler: Sampler, audio_froms: &[&[PortConnection]], audio_tos: &[&[PortConnection]],
scenes: usize, tracks: usize, track_width: usize,
) -> Self {
let mut arranger = Self {
edn: include_str!("./arranger-view.edn").to_string(),
..Self::groovebox(
jack, pool, editor,
None, midi_froms, midi_tos,
sampler, audio_froms, audio_tos
)
};
arranger.scenes_add(scenes);
arranger.tracks_add(tracks, track_width, &[], &[]);
arranger
}
fn compact (&self) -> bool { false }
fn editor (&self) -> impl Content<TuiOut> + '_ { &self.editor }
fn w (&self) -> u16 { self.tracks_sizes(self.is_editing(), self.editor_w()).last().map(|x|x.3 as u16).unwrap_or(0) }
fn pool (&self) -> impl Content<TuiOut> + use<'_> { self.pool.as_ref().map(|pool|Align::e(Fixed::x(self.sidebar_w(), PoolView(self.compact(), pool)))) }
fn row <'a> (
&'a self, w: u16, h: u16, a: impl Content<TuiOut> + 'a, b: impl Content<TuiOut> + 'a
) -> impl Content<TuiOut> + 'a {
Fixed::y(h, Bsp::e(
Fixed::xy(self.sidebar_w() as u16, h, a),
Fill::x(Align::c(Fixed::xy(w, h, b)))
))
}
fn is_editing (&self) -> bool {
self.editing.load(Relaxed)
}
fn sidebar_w (&self) -> u16 {
let w = self.size.w();
let w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
let w = if self.is_editing() { 8 } else { w };
w
}
}
#[derive(Clone, Debug)] pub enum AppCommand {
Clear,
Clip(ClipCommand),
Clock(ClockCommand),
Color(ItemPalette),
Compact(Option<bool>),
Editor(MidiEditCommand),
Enqueue(Option<Arc<RwLock<MidiClip>>>),
History(isize),
Pool(PoolCommand),
Sampler(SamplerCommand),
Scene(SceneCommand),
Select(Selection),
StopAll,
Track(TrackCommand),
Zoom(Option<usize>),
}
edn_command!(AppCommand: |state: App| {
("clear" [] Self::Clear)
("stop-all" [] Self::StopAll)
("compact" [c: bool ] Self::Compact(c))
("color" [c: Color] Self::Color(c.map(ItemPalette::from).unwrap_or_default()))
("history" [d: isize] Self::History(d.unwrap_or(0)))
("zoom" [z: usize] Self::Zoom(z))
("select" [s: Selection] Self::Select(s.expect("no selection")))
("enqueue" [c: Arc<RwLock<MidiClip>>] Self::Enqueue(c))
("clock" [a, ..b] Self::Clock(ClockCommand::from_edn(state, &a.to_ref(), b)))
("track" [a, ..b] Self::Track(TrackCommand::from_edn(state, &a.to_ref(), b)))
("scene" [a, ..b] Self::Scene(SceneCommand::from_edn(state, &a.to_ref(), b)))
("clip" [a, ..b] Self::Clip(ClipCommand::from_edn(state, &a.to_ref(), b)))
("pool" [a, ..b] Self::Pool(PoolCommand::from_edn(state.pool.as_ref().expect("no pool"), &a.to_ref(), b)))
("editor" [a, ..b] Self::Editor(MidiEditCommand::from_edn(state.editor.as_ref().expect("no editor"), &a.to_ref(), b)))
("sampler" [a, ..b] Self::Sampler(SamplerCommand::from_edn(state.sampler.as_ref().expect("no sampler"), &a.to_ref(), b)))
});
command!(|self: AppCommand, state: App|match self {
Self::Clear => { todo!() },
Self::Zoom(_) => { todo!(); },
Self::History(delta) => { todo!("undo/redo") },
Self::Select(s) => { state.selected = s; None },
Self::Clock(cmd) => cmd.delegate(state, Self::Clock)?,
Self::Scene(cmd) => match cmd {
SceneCommand::Add => { state.scene_add(None, None)?; None }
SceneCommand::Del(index) => { state.scene_del(index); None },
SceneCommand::SetColor(index, color) => {
let old = state.scenes[index].color;
state.scenes[index].color = color;
Some(SceneCommand::SetColor(index, old))
},
SceneCommand::Enqueue(scene) => {
for track in 0..state.tracks.len() {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
}
None
},
_ => None
}.map(Self::Scene),
Self::Track(cmd) => match cmd {
TrackCommand::Add => { state.track_add(None, None)?; None },
TrackCommand::Del(index) => { state.track_del(index); None },
TrackCommand::Stop(track) => { state.tracks[track].player.enqueue_next(None); None },
TrackCommand::SetColor(index, color) => {
let old = state.tracks[index].color;
state.tracks[index].color = color;
Some(TrackCommand::SetColor(index, old))
},
_ => None
}.map(Self::Track),
Self::Clip(cmd) => match cmd {
ClipCommand::Get(track, scene) => { todo!() },
ClipCommand::Put(track, scene, clip) => {
let old = state.scenes[scene].clips[track].clone();
state.scenes[scene].clips[track] = clip;
Some(ClipCommand::Put(track, scene, old))
},
ClipCommand::Enqueue(track, scene) => {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
None
},
_ => None
}.map(Self::Clip),
Self::Editor(cmd) =>
state.editor.as_mut().map(|editor|cmd.delegate(editor, Self::Editor)).transpose()?.flatten(),
Self::Sampler(cmd) =>
state.sampler.as_mut().map(|sampler|cmd.delegate(sampler, Self::Sampler)).transpose()?.flatten(),
Self::Enqueue(clip) =>
state.player.as_mut().map(|player|{player.enqueue_next(clip.as_ref());None}).flatten(),
Self::StopAll => {
for track in 0..state.tracks.len() { state.tracks[track].player.enqueue_next(None); }
None
},
Self::Color(palette) => {
let old = state.color;
state.color = palette;
Some(Self::Color(old))
},
Self::Pool(cmd) => if let Some(pool) = state.pool.as_mut() {
let undo = cmd.clone().delegate(pool, Self::Pool)?;
if let Some(editor) = state.editor.as_mut() {
match cmd {
// autoselect: automatically load selected clip in editor
// autocolor: update color in all places simultaneously
PoolCommand::Select(_) | PoolCommand::Clip(PoolClipCommand::SetColor(_, _)) =>
editor.set_clip(pool.clip().as_ref()),
_ => {}
}
};
undo
} else {
None
},
Self::Compact(compact) => match compact {
Some(compact) => {
if state.compact != compact {
state.compact = compact;
Some(Self::Compact(Some(!compact)))
} else {
None
}
},
None => {
state.compact = !state.compact;
Some(Self::Compact(Some(!state.compact)))
}
}
});
#[derive(Clone, Debug)] pub enum ClipCommand {
Get(usize, usize),
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
@ -654,45 +837,20 @@ command!(|self: AppCommand, state: App|match self {
SetColor(usize, usize, ItemPalette),
}
edn_command!(ClipCommand: |state: App| {
("get" [a: usize
,b: usize] Self::Get(a.unwrap(), b.unwrap()))
("put" [a: usize
,b: usize
,c: Option<Arc<RwLock<MidiClip>>>] Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))
("enqueue" [a: usize
,b: usize] Self::Enqueue(a.unwrap(), b.unwrap()))
("edit" [a: Option<Arc<RwLock<MidiClip>>>] Self::Edit(a.unwrap()))
("loop" [a: usize
,b: usize
,c: bool] Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))
("color" [a: usize
,b: usize] Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random()))
("get" [a: usize ,b: usize]
Self::Get(a.unwrap(), b.unwrap()))
("put" [a: usize, b: usize, c: Option<Arc<RwLock<MidiClip>>>]
Self::Put(a.unwrap(), b.unwrap(), c.unwrap()))
("enqueue" [a: usize, b: usize]
Self::Enqueue(a.unwrap(), b.unwrap()))
("edit" [a: Option<Arc<RwLock<MidiClip>>>]
Self::Edit(a.unwrap()))
("loop" [a: usize, b: usize, c: bool]
Self::SetLoop(a.unwrap(), b.unwrap(), c.unwrap()))
("color" [a: usize, b: usize]
Self::SetColor(a.unwrap(), b.unwrap(), ItemPalette::random()))
});
command!(|self: ClipCommand, state: App|match self { _ => todo!("clip command") });
#[derive(Clone, Debug)] pub enum TrackCommand {
Add,
Del(usize),
Stop(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
}
edn_command!(TrackCommand: |state: App| {
("add" [] Self::Add)
("size" [a: usize] Self::SetSize(a.unwrap()))
("zoom" [a: usize] Self::SetZoom(a.unwrap()))
("color" [a: usize] Self::SetColor(a.unwrap(), ItemPalette::random()))
("del" [a: usize] Self::Del(a.unwrap()))
("stop" [a: usize] Self::Stop(a.unwrap()))
("swap" [a: usize, b: usize] Self::Swap(a.unwrap(), b.unwrap()))
});
command!(|self: TrackCommand, state: App|match self { _ => todo!("track command") });
audio!(|self: App, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
@ -815,170 +973,30 @@ fn help_tag <'a> (before: &'a str, key: &'a str, after: &'a str) -> impl Content
let hi = TuiTheme::orange();
Tui::bold(true, row!(Tui::fg(lo, before), Tui::fg(hi, key), Tui::fg(lo, after)))
}
impl<T> Arrangement for T where T:
HasEditor + HasTracks + HasScenes + HasSelection + HasClock + HasJack {}
pub trait Arrangement: HasEditor + HasTracks + HasScenes + HasSelection + HasClock + HasJack {
fn clip (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.scene()?.clips.get(self.selected().track()?)?.clone()
}
fn toggle_loop (&mut self) {
if let Some(clip) = self.clip() {
clip.write().unwrap().toggle_loop()
}
}
//fn randomize_color (&mut self) {
//match self.selected {
//Selection::Mix => { self.color = ItemPalette::random() },
//Selection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
//Selection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
//Selection::Clip(t, s) => if let Some(clip) = &self.scenes[s].clips[t] {
//clip.write().unwrap().color = ItemPalette::random();
//}
//}
//}
fn track_add (
&mut self,
name: Option<&str>,
color: Option<ItemPalette>
) -> Usually<&mut Track> {
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
let track = Track {
width: (name.len() + 2).max(9),
color: color.unwrap_or_else(ItemPalette::random),
player: MidiPlayer::from(self.clock()),
name,
..Default::default()
};
self.tracks_mut().push(track);
let len = self.tracks().len();
let index = len - 1;
for scene in self.scenes_mut().iter_mut() {
while scene.clips.len() < len {
scene.clips.push(None);
}
}
Ok(&mut self.tracks_mut()[index])
}
fn tracks_add (
&mut self,
count: usize,
width: usize,
midi_from: &[PortConnection],
midi_to: &[PortConnection],
) -> Usually<()> {
let jack = self.jack().clone();
let track_color_1 = ItemColor::random();
let track_color_2 = ItemColor::random();
for i in 0..count {
let color = track_color_1.mix(track_color_2, i as f32 / count as f32).into();
let mut track = self.track_add(None, Some(color))?;
track.width = width;
let port = JackPort::<MidiIn>::new(&jack, &format!("{}I", &track.name), midi_from)?;
track.player.midi_ins.push(port);
let port = JackPort::<MidiOut>::new(&jack, &format!("{}O", &track.name), midi_to)?;
track.player.midi_outs.push(port);
}
Ok(())
}
fn track_del (&mut self, index: usize) {
self.tracks_mut().remove(index);
for scene in self.scenes_mut().iter_mut() {
scene.clips.remove(index);
}
}
fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
-> Usually<&mut Scene>
{
let scene = Scene {
name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
clips: vec![None;self.tracks().len()],
color: color.unwrap_or_else(ItemPalette::random),
};
self.scenes_mut().push(scene);
let index = self.scenes().len() - 1;
Ok(&mut self.scenes_mut()[index])
}
fn scenes_add (&mut self, n: usize) -> Usually<()> {
let scene_color_1 = ItemColor::random();
let scene_color_2 = ItemColor::random();
for i in 0..n {
let _scene = self.scene_add(None, Some(
scene_color_1.mix(scene_color_2, i as f32 / n as f32).into()
))?;
}
Ok(())
}
fn scene_cells <'a> (&'a self, editing: bool) -> BoxThunk<'a, TuiOut> {
let tracks = move||self.tracks_sizes(self.is_editing(), self.editor_w());
let scenes = ||self.scenes_sizes(self.is_editing(), 2, 15);
let selected_track = self.selected().track();
let selected_scene = self.selected().scene();
(move||Fill::y(Align::c(Map::new(tracks, move|(_, track, x1, x2), t| {
let w = (x2 - x1) as u16;
let color: ItemPalette = track.color.dark.into();
let last_color = Arc::new(RwLock::new(ItemPalette::from(Color::Rgb(0, 0, 0))));
let cells = Map::new(scenes, move|(_, scene, y1, y2), s| {
let h = (y2 - y1) as u16;
let color = scene.color;
let (name, fg, bg) = if let Some(c) = &scene.clips[t] {
let c = c.read().unwrap();
(c.name.to_string(), c.color.lightest.rgb, c.color.base.rgb)
} else {
("".to_string(), TuiTheme::g(64), TuiTheme::g(32))
};
let last = last_color.read().unwrap().clone();
let active = editing && selected_scene == Some(s) && selected_track == Some(t);
let editor = Thunk::new(||self.editor());
let cell = Thunk::new(move||phat_sel_3(
selected_track == Some(t) && selected_scene == Some(s),
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
Tui::fg(fg, Push::x(1, Tui::bold(true, name.to_string()))),
if selected_track == Some(t) && selected_scene.map(|s|s+1) == Some(s) {
None
} else {
Some(bg.into())
},
bg.into(),
bg.into(),
));
let cell = Either::new(active, editor, cell);
*last_color.write().unwrap() = bg.into();
map_south(
y1 as u16,
h + 1,
Fill::x(Fixed::y(h + 1, cell))
)
});
let column = Fixed::x(w, Tui::bg(Color::Reset, Align::y(cells)).boxed());
Fixed::x(w, map_east(x1 as u16, w, column))
}))).boxed()).into()
}
fn activate (&mut self) -> Usually<()> {
let selected = self.selected().clone();
match selected {
Selection::Scene(s) => {
let mut clips = vec![];
for (t, _) in self.tracks().iter().enumerate() {
clips.push(self.scenes()[s].clips[t].clone());
}
for (t, track) in self.tracks_mut().iter_mut().enumerate() {
if track.player.play_clip.is_some() || clips[t].is_some() {
track.player.enqueue_next(clips[t].as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
},
Selection::Clip(t, s) => {
let clip = self.scenes()[s].clips[t].clone();
self.tracks_mut()[t].player.enqueue_next(clip.as_ref());
},
_ => {}
}
Ok(())
}
}
#[derive(Debug, Default)] struct Meter<'a>(pub &'a str, pub f32);
content!(TuiOut: |self: Meter<'a>| col!(
Field(TuiTheme::g(128).into(), self.0, format!("{:>+9.3}", self.1)),
Fixed::xy(if self.1 >= 0.0 { 13 }
else if self.1 >= -1.0 { 12 }
else if self.1 >= -2.0 { 11 }
else if self.1 >= -3.0 { 10 }
else if self.1 >= -4.0 { 9 }
else if self.1 >= -6.0 { 8 }
else if self.1 >= -9.0 { 7 }
else if self.1 >= -12.0 { 6 }
else if self.1 >= -15.0 { 5 }
else if self.1 >= -20.0 { 4 }
else if self.1 >= -25.0 { 3 }
else if self.1 >= -30.0 { 2 }
else if self.1 >= -40.0 { 1 }
else { 0 }, 1, Tui::bg(if self.1 >= 0.0 { Color::Red }
else if self.1 >= -3.0 { Color::Yellow }
else { Color::Green }, ()))));
#[derive(Debug, Default)] struct Meters<'a>(pub &'a[f32]);
content!(TuiOut: |self: Meters<'a>| col!(
format!("L/{:>+9.3}", self.0[0]),
format!("R/{:>+9.3}", self.0[1])
));
#[cfg(test)] fn test_tek () {
// TODO
}

View file