use Arc<str> where applicable; use konst split_at

This commit is contained in:
🪞👃🪞 2025-01-08 00:24:40 +01:00
parent 411fc0c4bc
commit 305481adee
35 changed files with 286 additions and 273 deletions

View file

@ -55,7 +55,7 @@ fn add_tracks (jack: &JackConnection, app: &mut Arranger, cli: &ArrangerCli) ->
track_color_1.mix(track_color_2, i as f32 / n as f32).into() track_color_1.mix(track_color_2, i as f32 / n as f32).into()
))?; ))?;
track.width = cli.track_width; track.width = cli.track_width;
let name = track.name.read().unwrap(); let name = track.name.clone();
track.player.midi_ins.push( track.player.midi_ins.push(
jack.register_port(&format!("{}I", &name), MidiIn::default())? jack.register_port(&format!("{}I", &name), MidiIn::default())?
); );

11
edn/Cargo.lock generated
View file

@ -770,19 +770,18 @@ dependencies = [
"clojure-reader", "clojure-reader",
"itertools 0.14.0", "itertools 0.14.0",
"konst", "konst",
"tek_layout",
"tek_tui", "tek_tui",
] ]
[[package]] [[package]]
name = "tek_engine" name = "tek_input"
version = "0.2.0" version = "0.2.0"
[[package]] [[package]]
name = "tek_layout" name = "tek_output"
version = "0.2.0" version = "0.2.0"
dependencies = [ dependencies = [
"tek_engine", "tek_edn",
] ]
[[package]] [[package]]
@ -795,8 +794,8 @@ dependencies = [
"rand", "rand",
"ratatui", "ratatui",
"tek_edn", "tek_edn",
"tek_engine", "tek_input",
"tek_layout", "tek_output",
] ]
[[package]] [[package]]

View file

@ -12,7 +12,8 @@ pub enum Token<'a> {
impl<'a> Token<'a> { impl<'a> Token<'a> {
pub fn chomp (source: &'a str) -> Result<(&'a str, Self), ParseError> { pub fn chomp (source: &'a str) -> Result<(&'a str, Self), ParseError> {
use Token::*; use Token::*;
let mut state = Self::default(); use konst::string::{split_at, char_indices};
let mut state = Self::Nil;
for (index, c) in source.char_indices() { for (index, c) in source.char_indices() {
state = match state { state = match state {
// must begin expression // must begin expression
@ -29,21 +30,33 @@ impl<'a> Token<'a> {
Key(_, _, 0) => unreachable!(), Key(_, _, 0) => unreachable!(),
Num(source, index, length) => match c { Num(source, index, length) => match c {
'0'..='9' => Num(source, index, length + 1), '0'..='9' => Num(source, index, length + 1),
' '|'\n'|'\r'|'\t' => return Ok((&source[index+length..], Num(source, index, length))), ' '|'\n'|'\r'|'\t' => return Ok((
split_at(source, index+length).1,
Num(source, index, length)
)),
_ => return Err(ParseError::Unexpected(c)) _ => return Err(ParseError::Unexpected(c))
}, },
Sym(source, index, length) => match c { Sym(source, index, length) => match c {
'a'..='z'|'0'..='9'|'-' => Sym(source, index, length + 1), 'a'..='z'|'0'..='9'|'-' => Sym(source, index, length + 1),
' '|'\n'|'\r'|'\t' => return Ok((&source[index+length..], Sym(source, index, length))), ' '|'\n'|'\r'|'\t' => return Ok((
split_at(source, index+length).1,
Sym(source, index, length)
)),
_ => return Err(ParseError::Unexpected(c)) _ => return Err(ParseError::Unexpected(c))
}, },
Key(source, index, length) => match c { Key(source, index, length) => match c {
'a'..='z'|'0'..='9'|'-'|'/' => Key(source, index, length + 1), 'a'..='z'|'0'..='9'|'-'|'/' => Key(source, index, length + 1),
' '|'\n'|'\r'|'\t' => return Ok((&source[index+length..], Key(source, index, length))), ' '|'\n'|'\r'|'\t' => return Ok((
split_at(source, index+length).1,
Key(source, index, length)
)),
_ => return Err(ParseError::Unexpected(c)) _ => return Err(ParseError::Unexpected(c))
}, },
Exp(source, index, length, 0) => match c { Exp(source, index, length, 0) => match c {
' '|'\n'|'\r'|'\t' => return Ok((&source[index+length..], Exp(source, index, length, 0))), ' '|'\n'|'\r'|'\t' => return Ok((
split_at(source, index+length).1,
Exp(source, index, length, 0)
)),
_ => return Err(ParseError::Unexpected(c)) _ => return Err(ParseError::Unexpected(c))
}, },
Exp(source, index, length, depth) => match c { Exp(source, index, length, depth) => match c {

View file

@ -57,7 +57,7 @@ impl<E: Output> Measure<E> {
pub fn set_w (&self, w: impl Into<usize>) { self.x.store(w.into(), Relaxed) } pub fn set_w (&self, w: impl Into<usize>) { self.x.store(w.into(), Relaxed) }
pub fn set_h (&self, h: impl Into<usize>) { self.y.store(h.into(), Relaxed) } pub fn set_h (&self, h: impl Into<usize>) { self.y.store(h.into(), Relaxed) }
pub fn set_wh (&self, w: impl Into<usize>, h: impl Into<usize>) { self.set_w(w); self.set_h(h); } pub fn set_wh (&self, w: impl Into<usize>, h: impl Into<usize>) { self.set_w(w); self.set_h(h); }
pub fn format (&self) -> String { format!("{}x{}", self.w(), self.h()) } pub fn format (&self) -> Arc<str> { format!("{}x{}", self.w(), self.h()).into() }
pub fn new () -> Self { pub fn new () -> Self {
Self { Self {
_engine: PhantomData::default(), _engine: PhantomData::default(),

View file

@ -94,6 +94,11 @@ from_jack!(|jack| Arranger {
compact: true, compact: true,
} }
}); });
has_clock!(|self: Arranger|&self.clock);
has_phrases!(|self: Arranger|self.pool.phrases);
has_editor!(|self: Arranger|self.editor);
handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
//render!(TuiOut: (self: Arranger) => { //render!(TuiOut: (self: Arranger) => {
//let pool_w = if self.pool.visible { self.splits[1] } else { 0 }; //let pool_w = if self.pool.visible { self.splits[1] } else { 0 };
//let color = self.color; //let color = self.color;
@ -108,58 +113,54 @@ from_jack!(|jack| Arranger {
//Self::render_mode(self))))), Fill::y(&"fixme: self.editor")))))); //Self::render_mode(self))))), Fill::y(&"fixme: self.editor"))))));
//self.size.of(layout) //self.size.of(layout)
//}); //});
has_clock!(|self: Arranger|&self.clock);
has_phrases!(|self: Arranger|self.pool.phrases);
has_editor!(|self: Arranger|self.editor);
handle!(TuiIn: |self: Arranger, input|ArrangerCommand::execute_with_state(self, input.event()));
/// Status bar for arranger app ///// Status bar for arranger app
#[derive(Clone)] //#[derive(Clone)]
pub struct ArrangerStatus { //pub struct ArrangerStatus {
pub(crate) width: usize, //pub(crate) width: usize,
pub(crate) cpu: Option<String>, //pub(crate) cpu: Option<Arc<str>>,
pub(crate) size: String, //pub(crate) size: Arc<str>,
pub(crate) playing: bool, //pub(crate) playing: bool,
} //}
from!(|state:&Arranger|ArrangerStatus = { //from!(|state:&Arranger|ArrangerStatus = {
let samples = state.clock.chunk.load(Relaxed); //let samples = state.clock.chunk.load(Relaxed);
let rate = state.clock.timebase.sr.get(); //let rate = state.clock.timebase.sr.get();
let buffer = samples as f64 / rate; //let buffer = samples as f64 / rate;
let width = state.size.w(); //let width = state.size.w();
Self { //Self {
width, //width,
playing: state.clock.is_rolling(), //playing: state.clock.is_rolling(),
cpu: state.perf.percentage().map(|cpu|format!("{cpu:.01}%")), //cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%").into()),
size: format!("{}x{}│", width, state.size.h()), //size: format!("{}x{}│", width, state.size.h()).into(),
} //}
}); //});
render!(TuiOut: (self: ArrangerStatus) => Fixed::y(2, lay!( //render!(TuiOut: (self: ArrangerStatus) => Fixed::y(2, lay!(
Self::help(), //Self::help(),
Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))), //Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
))); //)));
impl ArrangerStatus { //impl ArrangerStatus {
fn help () -> impl Content<TuiOut> { //fn help () -> impl Content<TuiOut> {
let single = |binding, command|row!(" ", col!( //let single = |binding, command|row!(" ", col!(
Tui::fg(TuiTheme::yellow(), binding), //Tui::fg(TuiTheme::yellow(), binding),
command //command
)); //));
let double = |(b1, c1), (b2, c2)|col!( //let double = |(b1, c1), (b2, c2)|col!(
row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,), //row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,), //row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
); //);
Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!( //Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
single("SPACE", "play/pause"), //single("SPACE", "play/pause"),
single(" Ctrl", " scroll"), //single(" Ctrl", " scroll"),
single(" ▲▼▶◀", " cell"), //single(" ▲▼▶◀", " cell"),
double(("p", "put"), ("g", "get")), //double(("p", "put"), ("g", "get")),
double(("q", "enqueue"), ("e", "edit")), //double(("q", "enqueue"), ("e", "edit")),
single(" wsad", " note"), //single(" wsad", " note"),
double(("a", "append"), ("s", "set"),), //double(("a", "append"), ("s", "set"),),
double((",.", "length"), ("<>", "triplet"),), //double((",.", "length"), ("<>", "triplet"),),
double(("[]", "phrase"), ("{}", "order"),), //double(("[]", "phrase"), ("{}", "order"),),
)) //))
} //}
fn stats (&self) -> impl Content<TuiOut> + use<'_> { //fn stats (&self) -> impl Content<TuiOut> + use<'_> {
row!(&self.cpu, &self.size) //row!(&self.cpu, &self.size)
} //}
} //}

View file

@ -3,9 +3,8 @@ impl Arranger {
pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>) pub fn scene_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
-> Usually<&mut ArrangerScene> -> Usually<&mut ArrangerScene>
{ {
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
let scene = ArrangerScene { let scene = ArrangerScene {
name: Arc::new(name.into()), name: name.map_or_else(||self.scene_default_name(), |x|x.to_string().into()),
clips: vec![None;self.tracks.len()], clips: vec![None;self.tracks.len()],
color: color.unwrap_or_else(ItemPalette::random), color: color.unwrap_or_else(ItemPalette::random),
}; };
@ -16,8 +15,8 @@ impl Arranger {
pub fn scene_del (&mut self, index: usize) { pub fn scene_del (&mut self, index: usize) {
todo!("delete scene"); todo!("delete scene");
} }
fn scene_default_name (&self) -> String { fn scene_default_name (&self) -> Arc<str> {
format!("Sc{:3>}", self.scenes.len() + 1) format!("Sc{:3>}", self.scenes.len() + 1).into()
} }
pub fn selected_scene (&self) -> Option<&ArrangerScene> { pub fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().and_then(|s|self.scenes.get(s)) self.selected.scene().and_then(|s|self.scenes.get(s))
@ -28,14 +27,14 @@ impl Arranger {
} }
#[derive(Default, Debug, Clone)] pub struct ArrangerScene { #[derive(Default, Debug, Clone)] pub struct ArrangerScene {
/// Name of scene /// Name of scene
pub(crate) name: Arc<RwLock<String>>, pub(crate) name: Arc<str>,
/// Clips in scene, one per track /// Clips in scene, one per track
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>, pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
/// Identifying color of scene /// Identifying color of scene
pub(crate) color: ItemPalette, pub(crate) color: ItemPalette,
} }
impl ArrangerScene { impl ArrangerScene {
pub fn name (&self) -> &Arc<RwLock<String>> { pub fn name (&self) -> &Arc<str> {
&self.name &self.name
} }
pub fn clips (&self) -> &Vec<Option<Arc<RwLock<MidiClip>>>> { pub fn clips (&self) -> &Vec<Option<Arc<RwLock<MidiClip>>>> {
@ -45,7 +44,7 @@ impl ArrangerScene {
self.color self.color
} }
pub fn longest_name (scenes: &[Self]) -> usize { pub fn longest_name (scenes: &[Self]) -> usize {
scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) scenes.iter().map(|s|s.name.len()).fold(0, usize::max)
} }
/// Returns the pulse length of the longest phrase in the scene /// Returns the pulse length of the longest phrase in the scene
pub fn pulses (&self) -> usize { pub fn pulses (&self) -> usize {

View file

@ -21,17 +21,13 @@ impl ArrangerSelection {
&self, &self,
tracks: &[ArrangerTrack], tracks: &[ArrangerTrack],
scenes: &[ArrangerScene], scenes: &[ArrangerScene],
) -> String { ) -> Arc<str> {
format!("Selected: {}", match self { format!("Selected: {}", match self {
Self::Mix => "Everything".to_string(), Self::Mix => "Everything".to_string(),
Self::Track(t) => match tracks.get(*t) { Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
Some(track) => format!("T{t}: {}", &track.name.read().unwrap()), .unwrap_or_else(||"T??".into()),
None => "T??".into(), Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
}, .unwrap_or_else(||"S??".into()),
Self::Scene(s) => match scenes.get(*s) {
Some(scene) => format!("S{s}: {}", &scene.name.read().unwrap()),
None => "S??".into(),
},
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) { Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
(Some(_), Some(scene)) => match scene.clip(*t) { (Some(_), Some(scene)) => match scene.clip(*t) {
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name), Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
@ -39,7 +35,7 @@ impl ArrangerSelection {
}, },
_ => format!("T{t} S{s}: Empty"), _ => format!("T{t} S{s}: Empty"),
} }
}) }).into()
} }
pub fn track (&self) -> Option<usize> { pub fn track (&self) -> Option<usize> {
use ArrangerSelection::*; use ArrangerSelection::*;

View file

@ -1,17 +1,17 @@
use crate::*; use crate::*;
impl Arranger { impl Arranger {
pub fn track_next_name (&self) -> String { pub fn track_next_name (&self) -> Arc<str> {
format!("Tr{}", self.tracks.len() + 1) format!("Tr{:02}", self.tracks.len() + 1).into()
} }
pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>) pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
-> Usually<&mut ArrangerTrack> -> Usually<&mut ArrangerTrack>
{ {
let name = name.map_or_else(||self.track_next_name(), |x|x.to_string()); let name = name.map_or_else(||self.track_next_name(), |x|x.to_string().into());
let track = ArrangerTrack { let track = ArrangerTrack {
width: name.len() + 2, width: name.len() + 2,
name: Arc::new(name.into()),
color: color.unwrap_or_else(ItemPalette::random), color: color.unwrap_or_else(ItemPalette::random),
player: MidiPlayer::from(&self.clock), player: MidiPlayer::from(&self.clock),
name,
}; };
self.tracks.push(track); self.tracks.push(track);
let index = self.tracks.len() - 1; let index = self.tracks.len() - 1;
@ -26,7 +26,7 @@ impl Arranger {
} }
#[derive(Debug)] pub struct ArrangerTrack { #[derive(Debug)] pub struct ArrangerTrack {
/// Name of track /// Name of track
pub name: Arc<RwLock<String>>, pub name: Arc<str>,
/// Preferred width of track column /// Preferred width of track column
pub width: usize, pub width: usize,
/// Identifying color of track /// Identifying color of track
@ -38,7 +38,7 @@ has_clock!(|self:ArrangerTrack|self.player.clock());
has_player!(|self:ArrangerTrack|self.player); has_player!(|self:ArrangerTrack|self.player);
impl ArrangerTrack { impl ArrangerTrack {
/// Name of track /// Name of track
pub fn name (&self) -> &Arc<RwLock<String>> { pub fn name (&self) -> &Arc<str> {
&self.name &self.name
} }
/// Preferred width of track column /// Preferred width of track column
@ -54,7 +54,7 @@ impl ArrangerTrack {
self.color self.color
} }
fn longest_name (tracks: &[Self]) -> usize { fn longest_name (tracks: &[Self]) -> usize {
tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max) tracks.iter().map(|s|s.name.len()).fold(0, usize::max)
} }
fn width_inc (&mut self) { fn width_inc (&mut self) {
*self.width_mut() += 1; *self.width_mut() += 1;

View file

@ -3,27 +3,25 @@ pub(crate) const HEADER_H: u16 = 0; // 5
pub(crate) const SCENES_W_OFFSET: u16 = 0; pub(crate) const SCENES_W_OFFSET: u16 = 0;
render!(TuiOut: (self: Arranger) => { render!(TuiOut: (self: Arranger) => {
let toolbar = |x|Bsp::s(self.toolbar_view(), x); let toolbar = |x|Bsp::s(self.toolbar_view(), x);
let status = |x|Bsp::n(self.status_view(), x);
let pool = |x|Bsp::w(self.pool_view(), x); let pool = |x|Bsp::w(self.pool_view(), x);
let editing = |x|Bsp::n(self.editor_status_view(), x); let editing = |x|Bsp::n(Bsp::e(self.editor.clip_status(), self.editor.edit_status()), x);
let enclosed = |x|Outer(Style::default().fg(Color::Rgb(72,72,72))).enclose(x); let enclosed = |x|Outer(Style::default().fg(Color::Rgb(72,72,72))).enclose(x);
let editor = Fixed::y(20, enclosed(&self.editor));
let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16); let scenes_w = 16;//.max(SCENES_W_OFFSET + ArrangerScene::longest_name(&self.scenes) as u16);
let arrrrrr = Fixed::y(32, Map::new( let arrrrrr = Fixed::y(27, Map::new(
move||[ move||[
(0, 2, self.output_row_header(), self.output_row_cells()), (0, 2, self.output_row_header(), self.output_row_cells()),
(2, 2, self.elapsed_row_header(), self.elapsed_row_cells()), (2, 3, self.elapsed_row_header(), self.elapsed_row_cells()),
(4, 2, self.next_row_header(), self.next_row_cells()), (4, 3, self.next_row_header(), self.next_row_cells()),
(6, 3, self.track_row_header(), self.track_row_cells()), (6, 3, self.track_row_header(), self.track_row_cells()),
(9, 20, self.scene_row_headers(), self.scene_row_cells()), (8, 20, self.scene_row_headers(), self.scene_row_cells()),
(29, 2, self.input_row_header(), self.input_row_cells()), (25, 2, self.input_row_header(), self.input_row_cells()),
].into_iter(), ].into_iter(),
move|(y, h, header, cells), index|map_south(y, h, Fill::x(Align::w(Bsp::e( move|(y, h, header, cells), index|map_south(y, h, Fill::x(Align::w(Bsp::e(
Fixed::xy(scenes_w, h, header), Fixed::xy(scenes_w, h, header),
Fixed::xy(self.tracks.len() as u16*6, h, cells) Fixed::xy(self.tracks.len() as u16*6, h, cells)
)))))); ))))));
self.size.of(toolbar(status(pool(editing(Bsp::n(editor, arrrrrr)))))) self.size.of(toolbar(pool(editing(Bsp::s(arrrrrr, enclosed(&self.editor))))))
}); });
impl Arranger { impl Arranger {
@ -63,7 +61,7 @@ impl Arranger {
(move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { (move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| {
let w = (x2 - x1) as u16; let w = (x2 - x1) as u16;
let color: ItemPalette = track.color().dark.into(); let color: ItemPalette = track.color().dark.into();
let cell = Bsp::s(format!("MutSol"), Self::phat_hi(color.dark.rgb, color.darker.rgb)); let cell = Bsp::s(format!(" M S "), Self::phat_hi(color.dark.rgb, color.darker.rgb));
map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell))) map_east(x1 as u16, w, Fixed::x(w, Self::cell(color, cell)))
})).boxed()).into() })).boxed()).into()
} }
@ -73,19 +71,17 @@ impl Arranger {
} }
fn elapsed_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { fn elapsed_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { (move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| {
let color = track.color(); //let color = track.color();
let color: ItemPalette = track.color().dark.into(); let color: ItemPalette = track.color().dark.into();
let timebase = self.clock().timebase(); let timebase = self.clock().timebase();
let elapsed = { let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
let mut result = String::new();
if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() { if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
let length = phrase.read().unwrap().length; let length = phrase.read().unwrap().length;
let elapsed = track.player.pulses_since_start().unwrap() as usize; let elapsed = track.player.pulses_since_start().unwrap() as usize;
result = format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64)) format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
} } else {
result String::new()
}; });
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, elapsed);
let cell = Bsp::s(value, Self::phat_hi(color.dark.rgb, color.darker.rgb)); let cell = Bsp::s(value, Self::phat_hi(color.dark.rgb, color.darker.rgb));
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell)) Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, cell))
})).boxed()).into() })).boxed()).into()
@ -109,12 +105,14 @@ impl Arranger {
(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Track")).boxed()).into() (||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Track")).boxed()).into()
} }
fn track_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { fn track_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| { let iter = ||self.tracks_with_widths();
(move||Fixed::y(2, Map::new(iter, move|(_, track, x1, x2), i| {
let color = track.color(); let color = track.color();
let name = format!(" {}", &track.name);
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16, Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16,
Tui::fg_bg(color.lightest.rgb, color.base.rgb, Tui::fg_bg(color.lightest.rgb, color.base.rgb,
Self::phat_cell(color, color.darkest.rgb.into(), Tui::bold(true, format!("{}", track.name.read().unwrap())))))) Self::phat_cell(color, color.darkest.rgb.into(),
})).boxed()).into() Tui::bold(true, name))))) })).boxed()).into()
} }
fn input_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> { fn input_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
@ -138,7 +136,7 @@ impl Arranger {
move|(_, scene, y1, y2), i| { move|(_, scene, y1, y2), i| {
let h = (y2 - y1) as u16; let h = (y2 - y1) as u16;
let color = scene.color(); let color = scene.color();
let name = format!(" {}", scene.name.read().unwrap()); let name = format!("🭬{}", &scene.name);
let cell = Self::phat_cell(color, *last_color.read().unwrap(), name); let cell = Self::phat_cell(color, *last_color.read().unwrap(), name);
*last_color.write().unwrap() = color; *last_color.write().unwrap() = color;
map_south(y1 as u16, 2, Fill::x(cell)) map_south(y1 as u16, 2, Fill::x(cell))
@ -147,7 +145,7 @@ impl Arranger {
}).into() }).into()
} }
fn scene_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> { fn scene_row_cells <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
(||Tui::bg(Color::Rgb(20,40,60), "").boxed()).into() (||Tui::bg(self.color.darkest.rgb, "").boxed()).into()
} }
pub fn tracks_with_widths (&self) pub fn tracks_with_widths (&self)
@ -175,8 +173,7 @@ impl Arranger {
} }
/// name and width of track /// name and width of track
fn cell_name (track: &ArrangerTrack, _w: usize) -> impl Content<TuiOut> { fn cell_name (track: &ArrangerTrack, _w: usize) -> impl Content<TuiOut> {
let name = track.name().read().unwrap().clone(); Tui::bold(true, Tui::fg(track.color.lightest.rgb, track.name().clone()))
Tui::bold(true, Tui::fg(track.color.lightest.rgb, name))
} }
/// beats until switchover /// beats until switchover
fn cell_until_next (track: &ArrangerTrack, current: &Arc<Moment>) fn cell_until_next (track: &ArrangerTrack, current: &Arc<Moment>)
@ -207,7 +204,7 @@ impl Arranger {
Map::new(||self.scenes_with_heights(1), move|(_, scene, y1, y2), i| { Map::new(||self.scenes_with_heights(1), move|(_, scene, y1, y2), i| {
let h = (y2 - y1) as u16; let h = (y2 - y1) as u16;
let color = scene.color(); let color = scene.color();
let cell = Fixed::y(h, Fixed::x(scenes_w, Self::cell(color, scene.name.read().unwrap().clone()))); let cell = Fixed::y(h, Fixed::x(scenes_w, Self::cell(color, scene.name.clone())));
map_south(y1 as u16, 1, cell) map_south(y1 as u16, 1, cell)
}) })
} }
@ -235,7 +232,7 @@ impl Arranger {
scene.color.base.rgb, if playing { "" } else { " " } scene.color.base.rgb, if playing { "" } else { " " }
); );
let name = Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb, let name = Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb,
Expand::x(1, Tui::bold(true, scene.name.read().unwrap().clone())) Expand::x(1, Tui::bold(true, scene.name.clone()))
); );
let clips = Map::new(||Arranger::tracks_with_widths_static(tracks), move|(index, track, x1, x2), _| let clips = Map::new(||Arranger::tracks_with_widths_static(tracks), move|(index, track, x1, x2), _|
Push::x((x2 - x1) as u16, Self::cell_clip(scene, index, track, (x2 - x1) as u16, height)) Push::x((x2 - x1) as u16, Self::cell_clip(scene, index, track, (x2 - x1) as u16, height))
@ -264,20 +261,6 @@ impl Arranger {
fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> { fn toolbar_view (&self) -> impl Content<TuiOut> + use<'_> {
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock)))) Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.clock))))
} }
fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
ArrangerStatus::from(self)
//let edit_clip = MidiEditClip(&self.editor);
////let selectors = When(false, Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player)));
//row!([>selectors,<] edit_clip, MidiEditStatus(&self.editor))
}
fn editor_status_view (&self) -> impl Content<TuiOut> + use<'_> {
Bsp::e(
//ClipSelected::play_phrase(&self.player),
//ClipSelected::next_phrase(&self.player),
MidiEditClip(&self.editor),
MidiEditStatus(&self.editor),
)
}
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> { fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
let w = self.size.w(); let w = self.size.w();
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 }; let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };

View file

@ -46,17 +46,20 @@ render!(TuiOut: (self: PlayPause) => Tui::bg(
Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)), Tui::fg(Color::Rgb(0, 255, 0), Bsp::s(" 🭍🭑🬽 ", " 🭞🭜🭘 ",)),
Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",)))))))); Tui::fg(Color::Rgb(255, 128, 0), Bsp::s(" ▗▄▖ ", " ▝▀▘ ",))))))));
pub struct BeatStats { compact: bool, bpm: String, beat: String, time: String, } pub struct BeatStats { compact: bool, bpm: Arc<str>, beat: Arc<str>, time: Arc<str>, }
impl BeatStats { impl BeatStats {
fn new (compact: bool, clock: &Clock) -> Self { fn new (compact: bool, clock: &Clock) -> Self {
let (beat, time) = clock.started.read().unwrap().as_ref().map(|started|{ let bpm = format!("{:.3}", clock.timebase.bpm.get()).into();
let current_usec = clock.global.usec.get() - started.usec.get(); let (beat, time) = if let Some(started) = clock.started.read().unwrap().as_ref() {
let now = clock.global.usec.get() - started.usec.get();
( (
clock.timebase.format_beats_1(clock.timebase.usecs_to_pulse(current_usec)), clock.timebase.format_beats_1(clock.timebase.usecs_to_pulse(now)).into(),
format!("{:.3}s", current_usec/1000000.) format!("{:.3}s", now/1000000.).into()
) )
}).unwrap_or_else(||("-.-.--".to_string(), "-.---s".to_string())); } else {
Self { compact, bpm: format!("{:.3}", clock.timebase.bpm.get()), beat, time } ("-.-.--".to_string().into(), "-.---s".to_string().into())
};
Self { compact, bpm, beat, time }
} }
} }
render!(TuiOut: (self: BeatStats) => Either(self.compact, render!(TuiOut: (self: BeatStats) => Either(self.compact,
@ -71,7 +74,7 @@ render!(TuiOut: (self: BeatStats) => Either(self.compact,
Bsp::e("Time ", Tui::fg(TuiTheme::g(255), &self.time)), Bsp::e("Time ", Tui::fg(TuiTheme::g(255), &self.time)),
))); )));
pub struct OutputStats { compact: bool, sample_rate: String, buffer_size: String, latency: String, } pub struct OutputStats { compact: bool, sample_rate: Arc<str>, buffer_size: Arc<str>, latency: Arc<str>, }
impl OutputStats { impl OutputStats {
fn new (compact: bool, clock: &Clock) -> Self { fn new (compact: bool, clock: &Clock) -> Self {
let rate = clock.timebase.sr.get(); let rate = clock.timebase.sr.get();
@ -82,9 +85,9 @@ impl OutputStats {
format!("{:.1}kHz", rate / 1000.) format!("{:.1}kHz", rate / 1000.)
} else { } else {
format!("{:.0}Hz", rate) format!("{:.0}Hz", rate)
}, }.into(),
buffer_size: format!("{chunk}"), buffer_size: format!("{chunk}").into(),
latency: format!("{:.1}ms", chunk as f64 / rate * 1000.), latency: format!("{:.1}ms", chunk as f64 / rate * 1000.).into(),
} }
} }
} }

View file

@ -6,10 +6,10 @@ use crate::*;
impl_time_unit!(Microsecond); impl_time_unit!(Microsecond);
impl Microsecond { impl Microsecond {
#[inline] pub fn format_msu (&self) -> String { #[inline] pub fn format_msu (&self) -> Arc<str> {
let usecs = self.get() as usize; let usecs = self.get() as usize;
let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000); let (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
let (minutes, seconds) = (seconds / 60, seconds % 60); let (minutes, seconds) = (seconds / 60, seconds % 60);
format!("{minutes}:{seconds:02}:{msecs:03}") format!("{minutes}:{seconds:02}:{msecs:03}").into()
} }
} }

View file

@ -64,7 +64,7 @@ impl Moment {
self.pulse.set(pulse); self.pulse.set(pulse);
self.sample.set(self.timebase.pulses_to_sample(pulse)); self.sample.set(self.timebase.pulses_to_sample(pulse));
} }
#[inline] pub fn format_beat (&self) -> String { #[inline] pub fn format_beat (&self) -> Arc<str> {
self.timebase.format_beats_1(self.pulse.get()) self.timebase.format_beats_1(self.pulse.get()).into()
} }
} }

View file

@ -78,32 +78,32 @@ impl Timebase {
events.map(|(time, event)|(self.quantize(step, time).0, event)).collect() events.map(|(time, event)|(self.quantize(step, time).0, event)).collect()
} }
/// Format a number of pulses into Beat.Bar.Pulse starting from 0 /// Format a number of pulses into Beat.Bar.Pulse starting from 0
#[inline] pub fn format_beats_0 (&self, pulse: f64) -> String { #[inline] pub fn format_beats_0 (&self, pulse: f64) -> Arc<str> {
let pulse = pulse as usize; let pulse = pulse as usize;
let ppq = self.ppq.get() as usize; let ppq = self.ppq.get() as usize;
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
format!("{}.{}.{pulses:02}", beats / 4, beats % 4) format!("{}.{}.{pulses:02}", beats / 4, beats % 4).into()
} }
/// Format a number of pulses into Beat.Bar starting from 0 /// Format a number of pulses into Beat.Bar starting from 0
#[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String { #[inline] pub fn format_beats_0_short (&self, pulse: f64) -> Arc<str> {
let pulse = pulse as usize; let pulse = pulse as usize;
let ppq = self.ppq.get() as usize; let ppq = self.ppq.get() as usize;
let beats = if ppq > 0 { pulse / ppq } else { 0 }; let beats = if ppq > 0 { pulse / ppq } else { 0 };
format!("{}.{}", beats / 4, beats % 4) format!("{}.{}", beats / 4, beats % 4).into()
} }
/// Format a number of pulses into Beat.Bar.Pulse starting from 1 /// Format a number of pulses into Beat.Bar.Pulse starting from 1
#[inline] pub fn format_beats_1 (&self, pulse: f64) -> String { #[inline] pub fn format_beats_1 (&self, pulse: f64) -> Arc<str> {
let pulse = pulse as usize; let pulse = pulse as usize;
let ppq = self.ppq.get() as usize; let ppq = self.ppq.get() as usize;
let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) }; let (beats, pulses) = if ppq > 0 { (pulse / ppq, pulse % ppq) } else { (0, 0) };
format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1) format!("{}.{}.{pulses:02}", beats / 4 + 1, beats % 4 + 1).into()
} }
/// Format a number of pulses into Beat.Bar.Pulse starting from 1 /// Format a number of pulses into Beat.Bar.Pulse starting from 1
#[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String { #[inline] pub fn format_beats_1_short (&self, pulse: f64) -> Arc<str> {
let pulse = pulse as usize; let pulse = pulse as usize;
let ppq = self.ppq.get() as usize; let ppq = self.ppq.get() as usize;
let beats = if ppq > 0 { pulse / ppq } else { 0 }; let beats = if ppq > 0 { pulse / ppq } else { 0 };
format!("{}.{}", beats / 4 + 1, beats % 4 + 1) format!("{}.{}", beats / 4 + 1, beats % 4 + 1).into()
} }
} }

View file

@ -18,7 +18,7 @@ pub enum FileBrowserCommand {
Confirm, Confirm,
Select(usize), Select(usize),
Chdir(PathBuf), Chdir(PathBuf),
Filter(String), Filter(Arc<str>),
} }
render!(TuiOut: (self: FileBrowser) => /*Stack::down(|add|{ render!(TuiOut: (self: FileBrowser) => /*Stack::down(|add|{
let mut i = 0; let mut i = 0;

View file

@ -23,10 +23,10 @@ impl EdnViewData<TuiOut> for &Groovebox {
":input-meter-r" => Box::new(Meter("R/", self.sampler.input_meter[1])), ":input-meter-r" => Box::new(Meter("R/", self.sampler.input_meter[1])),
":transport" => Box::new(TransportView::new(true, &self.player.clock)), ":transport" => Box::new(TransportView::new(true, &self.player.clock)),
":clip-play" => Box::new(ClipSelected::play_phrase(&self.player)), ":clip-play" => Box::new(self.player.play_status()),
":clip-next" => Box::new(ClipSelected::next_phrase(&self.player)), ":clip-next" => Box::new(self.player.next_status()),
":clip-edit" => Box::new(MidiEditClip(&self.editor)), ":clip-edit" => Box::new(self.editor.clip_status()),
":edit-stat" => Box::new(MidiEditStatus(&self.editor)), ":edit-stat" => Box::new(self.editor.edit_status()),
":pool-view" => Box::new(PoolView(self.compact, &self.pool)), ":pool-view" => Box::new(PoolView(self.compact, &self.pool)),
":midi-view" => Box::new(&self.editor), ":midi-view" => Box::new(&self.editor),

View file

@ -30,10 +30,10 @@ impl Groovebox {
} }
fn selector_view (&self) -> impl Content<TuiOut> + use<'_> { fn selector_view (&self) -> impl Content<TuiOut> + use<'_> {
row!( row!(
ClipSelected::play_phrase(&self.player), self.player.play_status(),
ClipSelected::next_phrase(&self.player), self.player.next_status(),
MidiEditClip(&self.editor), self.editor.clip_status(),
MidiEditStatus(&self.editor), self.editor.edit_status(),
) )
} }
fn sample_view (&self) -> impl Content<TuiOut> + use<'_> { fn sample_view (&self) -> impl Content<TuiOut> + use<'_> {
@ -53,7 +53,7 @@ impl Groovebox {
PoolView(self.compact, &self.pool)) PoolView(self.compact, &self.pool))
} }
fn sampler_view (&self) -> impl Content<TuiOut> + use<'_> { fn sampler_view (&self) -> impl Content<TuiOut> + use<'_> {
let sampler_w = if self.compact { 4 } else { 11 }; let sampler_w = if self.compact { 4 } else { 40 };
let sampler_y = if self.compact { 1 } else { 0 }; let sampler_y = if self.compact { 1 } else { 0 };
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y( Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(
SampleList::new(self.compact, &self.sampler, &self.editor)))) SampleList::new(self.compact, &self.sampler, &self.editor))))

View file

@ -242,12 +242,12 @@ impl RegisterPort for Arc<RwLock<JackConnection>> {
/// Event enum for JACK events. /// Event enum for JACK events.
pub enum JackEvent { pub enum JackEvent {
ThreadInit, ThreadInit,
Shutdown(ClientStatus, String), Shutdown(ClientStatus, Arc<str>),
Freewheel(bool), Freewheel(bool),
SampleRate(Frames), SampleRate(Frames),
ClientRegistration(String, bool), ClientRegistration(Arc<str>, bool),
PortRegistration(PortId, bool), PortRegistration(PortId, bool),
PortRename(PortId, String, String), PortRename(PortId, Arc<str>, Arc<str>),
PortsConnected(PortId, PortId, bool), PortsConnected(PortId, PortId, bool),
GraphReorder, GraphReorder,
XRun, XRun,

View file

@ -11,7 +11,7 @@ impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
} }
} }
pub struct Menu<E: Engine, S, C: Command<S>> { pub struct Menu<E: Engine, S, C: Command<S>> {
pub title: String, pub title: Arc<str>,
pub items: Vec<MenuItem<E, S, C>>, pub items: Vec<MenuItem<E, S, C>>,
pub index: Option<usize>, pub index: Option<usize>,
} }

View file

@ -11,9 +11,7 @@ pub(crate) mod midi_note; pub(crate) use midi_note::*;
pub(crate) mod midi_range; pub(crate) use midi_range::*; pub(crate) mod midi_range; pub(crate) use midi_range::*;
pub(crate) mod midi_point; pub(crate) use midi_point::*; pub(crate) mod midi_point; pub(crate) use midi_point::*;
pub(crate) mod midi_view; pub(crate) use midi_view::*; pub(crate) mod midi_view; pub(crate) use midi_view::*;
pub(crate) mod midi_editor; pub(crate) use midi_editor::*; pub(crate) mod midi_editor; pub(crate) use midi_editor::*;
pub(crate) mod midi_status; pub(crate) use midi_status::*;
/// Add "all notes off" to the start of a buffer. /// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) { pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {

View file

@ -17,7 +17,7 @@ pub trait HasMidiClip {
pub struct MidiClip { pub struct MidiClip {
pub uuid: uuid::Uuid, pub uuid: uuid::Uuid,
/// Name of phrase /// Name of phrase
pub name: String, pub name: Arc<str>,
/// Temporal resolution in pulses per quarter note /// Temporal resolution in pulses per quarter note
pub ppq: usize, pub ppq: usize,
/// Length of phrase in pulses /// Length of phrase in pulses
@ -49,7 +49,7 @@ impl MidiClip {
) -> Self { ) -> Self {
Self { Self {
uuid: uuid::Uuid::new_v4(), uuid: uuid::Uuid::new_v4(),
name: name.as_ref().to_string(), name: name.as_ref().into(),
ppq: PPQ, ppq: PPQ,
length, length,
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]), notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),

View file

@ -122,6 +122,39 @@ impl MidiEditor {
self.mode.redraw(); self.mode.redraw();
} }
} }
pub fn clip_status (&self) -> impl Content<TuiOut> + '_ {
let (color, name, length, looped) = if let Some(phrase) = self.phrase().as_ref().map(|p|p.read().unwrap()) {
(phrase.color, phrase.name.clone(), phrase.length, phrase.looped)
} else {
(ItemPalette::from(TuiTheme::g(64)), String::new().into(), 0, false)
};
row!(
FieldV(color, "Edit", format!("{name} ({length})")),
FieldV(color, "Loop", looped.to_string())
)
}
pub fn edit_status (&self) -> impl Content<TuiOut> + '_ {
let (color, name, length, looped) = if let Some(phrase) = self.phrase().as_ref().map(|p|p.read().unwrap()) {
(phrase.color, phrase.name.clone(), phrase.length, phrase.looped)
} else {
(ItemPalette::from(TuiTheme::g(64)), String::new().into(), 0, false)
};
let time_point = self.time_point();
let time_start = self.time_start();
let time_end = self.time_end();
let time_axis = self.time_axis().get();
let time_zoom = self.time_zoom().get();
let time_lock = if self.time_lock().get() { "[lock]" } else { " " };
let time_field = FieldV(color, "Time", format!("{length}/{time_zoom}+{time_point} {time_lock}"));
let note_point = format!("{:>3}", self.note_point());
let note_name = format!("{:4}", Note::pitch_to_name(self.note_point()));
let note_len = format!("{:>4}", self.note_len());;;;
let note_field = FieldV(color, "Note", format!("{note_name} {note_point} {note_len}"));
Bsp::e(time_field, note_field,)
}
} }
impl std::fmt::Debug for MidiEditor { impl std::fmt::Debug for MidiEditor {

View file

@ -52,7 +52,9 @@ impl MidiPlayer {
midi_from: &[impl AsRef<str>], midi_from: &[impl AsRef<str>],
midi_to: &[impl AsRef<str>], midi_to: &[impl AsRef<str>],
) -> Usually<Self> { ) -> Usually<Self> {
let name = name.as_ref(); let name = name.as_ref();
let midi_in = jack.midi_in(&format!("M/{name}"), midi_from)?;
let midi_out = jack.midi_out(&format!("{name}/M"), midi_to)?;
Ok(Self { Ok(Self {
clock: Clock::from(jack), clock: Clock::from(jack),
play_phrase: None, play_phrase: None,
@ -62,19 +64,20 @@ impl MidiPlayer {
overdub: false, overdub: false,
notes_in: RwLock::new([false;128]).into(), notes_in: RwLock::new([false;128]).into(),
midi_ins: vec![ midi_ins: vec![midi_in],
jack.midi_in(&format!("M/{name}"), midi_from)?, midi_outs: vec![midi_out],
],
midi_outs: vec![
jack.midi_out(&format!("{name}/M"), midi_to)?,
],
notes_out: RwLock::new([false;128]).into(), notes_out: RwLock::new([false;128]).into(),
reset: true, reset: true,
note_buf: vec![0;8], note_buf: vec![0;8],
}) })
} }
pub fn play_status (&self) -> impl Content<TuiOut> {
ClipSelected::play_phrase(self)
}
pub fn next_status (&self) -> impl Content<TuiOut> {
ClipSelected::next_phrase(self)
}
} }
impl std::fmt::Debug for MidiPlayer { impl std::fmt::Debug for MidiPlayer {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {

View file

@ -21,7 +21,7 @@ pub enum PhrasePoolCommand {
Swap(usize, usize), Swap(usize, usize),
Import(usize, PathBuf), Import(usize, PathBuf),
Export(usize, PathBuf), Export(usize, PathBuf),
SetName(usize, String), SetName(usize, Arc<str>),
SetLength(usize, usize), SetLength(usize, usize),
SetColor(usize, ItemColor), SetColor(usize, ItemColor),
} }

View file

@ -1,36 +0,0 @@
use crate::*;
pub struct MidiEditClip<'a>(pub &'a MidiEditor);
render!(TuiOut: (self: MidiEditClip<'a>) => {
let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) {
(phrase.color, phrase.name.clone(), phrase.length, phrase.looped)
} else {
(ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false)
};
row!(
FieldV(color, "Edit", format!("{name} ({length})")),
FieldV(color, "Loop", looped.to_string())
)
});
pub struct MidiEditStatus<'a>(pub &'a MidiEditor);
render!(TuiOut: (self: MidiEditStatus<'a>) => {
let (color, name, length, looped) = if let Some(phrase) = self.0.phrase().as_ref().map(|p|p.read().unwrap()) {
(phrase.color, phrase.name.clone(), phrase.length, phrase.looped)
} else {
(ItemPalette::from(TuiTheme::g(64)), String::new(), 0, false)
};
let time_point = self.0.time_point();
let time_start = self.0.time_start();
let time_end = self.0.time_end();
let time_axis = self.0.time_axis().get();
let time_zoom = self.0.time_zoom().get();
let time_lock = if self.0.time_lock().get() { "[lock]" } else { " " };
let time_field = FieldV(color, "Time", format!("{length}/{time_zoom}+{time_point} {time_lock}"));
let note_point = format!("{:>3}", self.0.note_point());
let note_name = format!("{:4}", Note::pitch_to_name(self.0.note_point()));
let note_len = format!("{:>4}", self.0.note_len());;;;
let note_field = FieldV(color, "Note", format!("{note_name} {note_point} {note_len}"));
Bsp::e(time_field, note_field,)
});

View file

@ -4,7 +4,7 @@ use crate::*;
pub struct Mixer { pub struct Mixer {
/// JACK client handle (needs to not be dropped for standalone mode to work). /// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackConnection>>, pub jack: Arc<RwLock<JackConnection>>,
pub name: String, pub name: Arc<str>,
pub tracks: Vec<MixerTrack>, pub tracks: Vec<MixerTrack>,
pub selected_track: usize, pub selected_track: usize,
pub selected_column: usize, pub selected_column: usize,
@ -13,7 +13,7 @@ pub struct Mixer {
/// A mixer track. /// A mixer track.
#[derive(Debug)] #[derive(Debug)]
pub struct MixerTrack { pub struct MixerTrack {
pub name: String, pub name: Arc<str>,
/// Inputs of 1st device /// Inputs of 1st device
pub audio_ins: Vec<Port<AudioIn>>, pub audio_ins: Vec<Port<AudioIn>>,
/// Outputs of last device /// Outputs of last device
@ -47,7 +47,7 @@ impl Mixer {
impl MixerTrack { impl MixerTrack {
pub fn new (name: &str) -> Usually<Self> { pub fn new (name: &str) -> Usually<Self> {
Ok(Self { Ok(Self {
name: name.to_string(), name: name.to_string().into(),
audio_ins: vec![], audio_ins: vec![],
audio_outs: vec![], audio_outs: vec![],
devices: vec![], devices: vec![],
@ -267,7 +267,7 @@ const SYM_LV2: &str = "lv2";
from_edn!("mixer/track" => |jack: &Arc<RwLock<JackConnection>>, args| -> MixerTrack { from_edn!("mixer/track" => |jack: &Arc<RwLock<JackConnection>>, args| -> MixerTrack {
let mut _gain = 0.0f64; let mut _gain = 0.0f64;
let mut track = MixerTrack { let mut track = MixerTrack {
name: String::new(), name: "".into(),
audio_ins: vec![], audio_ins: vec![],
audio_outs: vec![], audio_outs: vec![],
devices: vec![], devices: vec![],

View file

@ -8,8 +8,8 @@ pub use self::lv2::LV2Plugin;
pub struct Plugin { pub struct Plugin {
/// JACK client handle (needs to not be dropped for standalone mode to work). /// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackConnection>>, pub jack: Arc<RwLock<JackConnection>>,
pub name: String, pub name: Arc<str>,
pub path: Option<String>, pub path: Option<Arc<str>>,
pub plugin: Option<PluginKind>, pub plugin: Option<PluginKind>,
pub selected: usize, pub selected: usize,
pub mapping: bool, pub mapping: bool,
@ -47,7 +47,7 @@ impl Plugin {
Ok(Self { Ok(Self {
jack: jack.clone(), jack: jack.clone(),
name: name.into(), name: name.into(),
path: Some(String::from(path)), path: Some(String::from(path).into()),
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)), plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
selected: 0, selected: 0,
mapping: false, mapping: false,

View file

@ -24,7 +24,7 @@ pub struct PoolModel {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum PoolMode { pub enum PoolMode {
/// Renaming a pattern /// Renaming a pattern
Rename(usize, String), Rename(usize, Arc<str>),
/// Editing the length of a pattern /// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus), Length(usize, usize, PhraseLengthFocus),
/// Load phrase from disk /// Load phrase from disk
@ -147,10 +147,10 @@ fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand>
return None return None
}, },
kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Phrase(PhrasePoolCommand::Add(count, MidiClip::new( kpat!(Char('a')) | kpat!(Shift-Char('A')) => Cmd::Phrase(PhrasePoolCommand::Add(count, MidiClip::new(
String::from("Clip"), true, 4 * PPQ, None, Some(ItemPalette::random()) "Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
))), ))),
kpat!(Char('i')) => Cmd::Phrase(PhrasePoolCommand::Add(index + 1, MidiClip::new( kpat!(Char('i')) => Cmd::Phrase(PhrasePoolCommand::Add(index + 1, MidiClip::new(
String::from("Clip"), true, 4 * PPQ, None, Some(ItemPalette::random()) "Clip", true, 4 * PPQ, None, Some(ItemPalette::random())
))), ))),
kpat!(Char('d')) | kpat!(Shift-Char('D')) => { kpat!(Char('d')) | kpat!(Shift-Char('D')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate(); let mut phrase = state.phrases()[index].read().unwrap().duplicate();

View file

@ -30,14 +30,14 @@ impl PhraseLength {
pub fn ticks (&self) -> usize { pub fn ticks (&self) -> usize {
self.pulses % self.ppq self.pulses % self.ppq
} }
pub fn bars_string (&self) -> String { pub fn bars_string (&self) -> Arc<str> {
format!("{}", self.bars()) format!("{}", self.bars()).into()
} }
pub fn beats_string (&self) -> String { pub fn beats_string (&self) -> Arc<str> {
format!("{}", self.beats()) format!("{}", self.beats()).into()
} }
pub fn ticks_string (&self) -> String { pub fn ticks_string (&self) -> Arc<str> {
format!("{:>02}", self.ticks()) format!("{:>02}", self.ticks()).into()
} }
} }

View file

@ -6,7 +6,7 @@ pub enum PhraseRenameCommand {
Begin, Begin,
Cancel, Cancel,
Confirm, Confirm,
Set(String), Set(Arc<str>),
} }
impl Command<PoolModel> for PhraseRenameCommand { impl Command<PoolModel> for PhraseRenameCommand {
@ -16,7 +16,7 @@ impl Command<PoolModel> for PhraseRenameCommand {
Some(PoolMode::Rename(phrase, ref mut old_name)) => match self { Some(PoolMode::Rename(phrase, ref mut old_name)) => match self {
Set(s) => { Set(s) => {
state.phrases()[phrase].write().unwrap().name = s; state.phrases()[phrase].write().unwrap().name = s;
return Ok(Some(Self::Set(old_name.clone()))) return Ok(Some(Self::Set(old_name.clone().into())))
}, },
Confirm => { Confirm => {
let old_name = old_name.clone(); let old_name = old_name.clone();
@ -24,7 +24,7 @@ impl Command<PoolModel> for PhraseRenameCommand {
return Ok(Some(Self::Set(old_name))) return Ok(Some(Self::Set(old_name)))
}, },
Cancel => { Cancel => {
state.phrases()[phrase].write().unwrap().name = old_name.clone(); state.phrases()[phrase].write().unwrap().name = old_name.clone().into();
}, },
_ => unreachable!() _ => unreachable!()
}, },
@ -40,14 +40,14 @@ impl InputToCommand<Event, PoolModel> for PhraseRenameCommand {
if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() { if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() {
Some(match input { Some(match input {
kpat!(Char(c)) => { kpat!(Char(c)) => {
let mut new_name = old_name.clone(); let mut new_name = old_name.clone().to_string();
new_name.push(*c); new_name.push(*c);
Self::Set(new_name) Self::Set(new_name.into())
}, },
kpat!(Backspace) => { kpat!(Backspace) => {
let mut new_name = old_name.clone(); let mut new_name = old_name.clone().to_string();
new_name.pop(); new_name.pop();
Self::Set(new_name) Self::Set(new_name.into())
}, },
kpat!(Enter) => Self::Confirm, kpat!(Enter) => Self::Confirm,
kpat!(Esc) => Self::Cancel, kpat!(Esc) => Self::Cancel,

View file

@ -2,9 +2,9 @@ use crate::*;
pub struct ClipSelected { pub struct ClipSelected {
pub(crate) title: &'static str, pub(crate) title: &'static str,
pub(crate) name: String, pub(crate) name: Arc<str>,
pub(crate) color: ItemPalette, pub(crate) color: ItemPalette,
pub(crate) time: String, pub(crate) time: Arc<str>,
} }
render!(TuiOut: (self: ClipSelected) => render!(TuiOut: (self: ClipSelected) =>
@ -16,9 +16,9 @@ impl ClipSelected {
pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self { pub fn play_phrase <T: HasPlayPhrase + HasClock> (state: &T) -> Self {
let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() { let (name, color) = if let Some((_, Some(phrase))) = state.play_phrase() {
let MidiClip { ref name, color, .. } = *phrase.read().unwrap(); let MidiClip { ref name, color, .. } = *phrase.read().unwrap();
(name.clone(), color) (name.clone().into(), color)
} else { } else {
("".to_string(), TuiTheme::g(64).into()) ("".to_string().into(), TuiTheme::g(64).into())
}; };
Self { Self {
title: "Now", title: "Now",
@ -28,14 +28,14 @@ impl ClipSelected {
.map(|(times, time)|format!("{:>3}x {:>}", .map(|(times, time)|format!("{:>3}x {:>}",
times+1.0, times+1.0,
state.clock().timebase.format_beats_1(time))) state.clock().timebase.format_beats_1(time)))
.unwrap_or_else(||String::from(" ")) .unwrap_or_else(||String::from(" ")).into()
} }
} }
/// Shows next phrase with beats remaining until switchover /// Shows next phrase with beats remaining until switchover
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self { pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
let mut time = String::from("--.-.--"); let mut time: Arc<str> = String::from("--.-.--").into();
let mut name = String::from(""); let mut name: Arc<str> = String::from("").into();
let mut color = ItemPalette::from(TuiTheme::g(64)); let mut color = ItemPalette::from(TuiTheme::g(64));
if let Some((t, Some(phrase))) = state.next_phrase() { if let Some((t, Some(phrase))) = state.next_phrase() {
let phrase = phrase.read().unwrap(); let phrase = phrase.read().unwrap();
@ -50,7 +50,7 @@ impl ClipSelected {
} else { } else {
String::new() String::new()
} }
} }.into()
} else if let Some((t, Some(phrase))) = state.play_phrase() { } else if let Some((t, Some(phrase))) = state.play_phrase() {
let phrase = phrase.read().unwrap(); let phrase = phrase.read().unwrap();
if phrase.looped { if phrase.looped {
@ -59,10 +59,12 @@ impl ClipSelected {
let target = t.pulse.get() + phrase.length as f64; let target = t.pulse.get() + phrase.length as f64;
let current = state.clock().playhead.pulse.get(); let current = state.clock().playhead.pulse.get();
if target > current { if target > current {
time = format!("-{:>}", state.clock().timebase.format_beats_0(target - current)) time = format!("-{:>}", state.clock().timebase.format_beats_0(
target - current
)).into()
} }
} else { } else {
name = "Stop".to_string(); name = "Stop".to_string().into();
} }
}; };
Self { title: "Next", time, name, color, } Self { title: "Next", time, name, color, }

View file

@ -5,7 +5,7 @@ render!(TuiOut: (self: PoolView<'a>) => {
let Self(compact, model) = self; let Self(compact, model) = self;
let PoolModel { phrases, mode, .. } = self.1; let PoolModel { phrases, mode, .. } = self.1;
let color = self.1.phrase().read().unwrap().color; let color = self.1.phrase().read().unwrap().color;
Outer( Bsp::b(Repeat(" "), Tui::bg(color.darkest.rgb, Outer(
Style::default().fg(color.dark.rgb).bg(color.darkest.rgb) Style::default().fg(color.dark.rgb).bg(color.darkest.rgb)
).enclose(Map::new(||model.phrases().iter(), |clip, i|{ ).enclose(Map::new(||model.phrases().iter(), |clip, i|{
let item_height = 1; let item_height = 1;
@ -20,5 +20,5 @@ render!(TuiOut: (self: PoolView<'a>) => {
Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "")))), Align::w(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "")))),
Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "")))), Align::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "")))),
))) )))
})) }))))
}); });

View file

@ -4,7 +4,7 @@ use super::*;
/// A sound sample. /// A sound sample.
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct Sample { pub struct Sample {
pub name: String, pub name: Arc<str>,
pub start: usize, pub start: usize,
pub end: usize, pub end: usize,
pub channels: Vec<Vec<f32>>, pub channels: Vec<Vec<f32>>,
@ -24,8 +24,8 @@ pub struct Sample {
} }
impl Sample { impl Sample {
pub fn new (name: &str, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self { pub fn new (name: impl AsRef<str>, start: usize, end: usize, channels: Vec<Vec<f32>>) -> Self {
Self { name: name.to_string(), start, end, channels, rate: None, gain: 1.0 } Self { name: name.as_ref().into(), start, end, channels, rate: None, gain: 1.0 }
} }
pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice { pub fn play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
Voice { Voice {

View file

@ -18,9 +18,7 @@ render!(TuiOut: (self: SampleList<'a>) => {
let note_pt = editor.note_point(); let note_pt = editor.note_point();
let note_hi = editor.note_hi(); let note_hi = editor.note_hi();
Outer(Style::default().fg(TuiTheme::g(96))).enclose(Map::new(move||(note_lo..=note_hi).rev(), move|note, i| { Outer(Style::default().fg(TuiTheme::g(96))).enclose(Map::new(move||(note_lo..=note_hi).rev(), move|note, i| {
let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a)))); let offset = |a|Push::y(i as u16, Align::n(Fixed::y(1, Fill::x(a))));
let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset }; let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset };
let mut fg = TuiTheme::g(160); let mut fg = TuiTheme::g(160);
if sampler.mapped[note].is_some() { if sampler.mapped[note].is_some() {
@ -33,13 +31,20 @@ render!(TuiOut: (self: SampleList<'a>) => {
fg = Color::Rgb(224,64,32) fg = Color::Rgb(224,64,32)
} }
} }
let label = if *compact {
offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", if *compact {
String::default() String::default()
} else if let Some(sample) = &sampler.mapped[note] { } else if let Some(sample) = &sampler.mapped[note] {
sample.read().unwrap().name.clone() let sample = sample.read().unwrap();
format!("{:8} {:3} {:6}-{:6}/{:6}",
sample.name,
sample.gain,
sample.start,
sample.end,
sample.channels[0].len()
)
} else { } else {
String::from("(none)") String::from("(none)")
}))) };
offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", label)))
})) }))
}); });

View file

@ -57,16 +57,21 @@ impl Sequencer {
Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock)))) Fill::x(Fixed::y(2, Align::x(TransportView::new(true, &self.player.clock))))
} }
fn status_view (&self) -> impl Content<TuiOut> + use<'_> { fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
let edit_clip = MidiEditClip(&self.editor); row!(
let selectors = When(self.selectors, Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player))); When(self.selectors, Bsp::e(
row!(selectors, edit_clip, MidiEditStatus(&self.editor)) self.player.play_status(),
self.player.next_status(),
)),
self.editor.clip_status(),
self.editor.edit_status(),
)
} }
fn selector_view (&self) -> impl Content<TuiOut> + use<'_> { fn selector_view (&self) -> impl Content<TuiOut> + use<'_> {
row!( row!(
ClipSelected::play_phrase(&self.player), self.player.play_status(),
ClipSelected::next_phrase(&self.player), self.player.next_status(),
MidiEditClip(&self.editor), self.editor.clip_status(),
MidiEditStatus(&self.editor), self.editor.edit_status(),
) )
} }
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> { fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
@ -180,8 +185,8 @@ command!(|self: SequencerCommand, state: Sequencer|match self {
#[derive(Clone)] #[derive(Clone)]
pub struct SequencerStatus { pub struct SequencerStatus {
pub(crate) width: usize, pub(crate) width: usize,
pub(crate) cpu: Option<String>, pub(crate) cpu: Option<Arc<str>>,
pub(crate) size: String, pub(crate) size: Arc<str>,
pub(crate) playing: bool, pub(crate) playing: bool,
} }
from!(|state:&Sequencer|SequencerStatus = { from!(|state:&Sequencer|SequencerStatus = {
@ -192,8 +197,8 @@ from!(|state:&Sequencer|SequencerStatus = {
Self { Self {
width, width,
playing: state.clock.is_rolling(), playing: state.clock.is_rolling(),
cpu: state.perf.percentage().map(|cpu|format!("{cpu:.01}%")), cpu: state.perf.percentage().map(|cpu|format!("{cpu:.01}%").into()),
size: format!("{}x{}│", width, state.size.h()), size: format!("{}x{}│", width, state.size.h()).into(),
} }
}); });
render!(TuiOut: (self: SequencerStatus) => Fixed::y(2, lay!( render!(TuiOut: (self: SequencerStatus) => Fixed::y(2, lay!(

View file

@ -18,6 +18,15 @@ impl Content<TuiOut> for String {
} }
} }
impl Content<TuiOut> for Arc<str> {
fn layout (&self, to: [u16;4]) -> [u16;4] {
to.center_xy([self.chars().count() as u16, 1])
}
fn render (&self, to: &mut TuiOut) {
to.blit(self, to.area.x(), to.area.y(), None)
}
}
pub struct Repeat<'a>(pub &'a str); pub struct Repeat<'a>(pub &'a str);
impl Content<TuiOut> for Repeat<'_> { impl Content<TuiOut> for Repeat<'_> {