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

@ -94,6 +94,11 @@ from_jack!(|jack| Arranger {
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) => {
//let pool_w = if self.pool.visible { self.splits[1] } else { 0 };
//let color = self.color;
@ -108,58 +113,54 @@ from_jack!(|jack| Arranger {
//Self::render_mode(self))))), Fill::y(&"fixme: self.editor"))))));
//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
#[derive(Clone)]
pub struct ArrangerStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) playing: bool,
}
from!(|state:&Arranger|ArrangerStatus = {
let samples = state.clock.chunk.load(Relaxed);
let rate = state.clock.timebase.sr.get();
let buffer = samples as f64 / rate;
let width = state.size.w();
Self {
width,
playing: state.clock.is_rolling(),
cpu: state.perf.percentage().map(|cpu|format!("{cpu:.01}%")),
size: format!("{}x{}│", width, state.size.h()),
}
});
render!(TuiOut: (self: ArrangerStatus) => Fixed::y(2, lay!(
Self::help(),
Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
)));
impl ArrangerStatus {
fn help () -> impl Content<TuiOut> {
let single = |binding, command|row!(" ", col!(
Tui::fg(TuiTheme::yellow(), binding),
command
));
let double = |(b1, c1), (b2, c2)|col!(
row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
);
Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
single("SPACE", "play/pause"),
single(" Ctrl", " scroll"),
single(" ▲▼▶◀", " cell"),
double(("p", "put"), ("g", "get")),
double(("q", "enqueue"), ("e", "edit")),
single(" wsad", " note"),
double(("a", "append"), ("s", "set"),),
double((",.", "length"), ("<>", "triplet"),),
double(("[]", "phrase"), ("{}", "order"),),
))
}
fn stats (&self) -> impl Content<TuiOut> + use<'_> {
row!(&self.cpu, &self.size)
}
}
///// Status bar for arranger app
//#[derive(Clone)]
//pub struct ArrangerStatus {
//pub(crate) width: usize,
//pub(crate) cpu: Option<Arc<str>>,
//pub(crate) size: Arc<str>,
//pub(crate) playing: bool,
//}
//from!(|state:&Arranger|ArrangerStatus = {
//let samples = state.clock.chunk.load(Relaxed);
//let rate = state.clock.timebase.sr.get();
//let buffer = samples as f64 / rate;
//let width = state.size.w();
//Self {
//width,
//playing: state.clock.is_rolling(),
//cpu: state.perf.percentage().map(|cpu|format!("│{cpu:.01}%").into()),
//size: format!("{}x{}│", width, state.size.h()).into(),
//}
//});
//render!(TuiOut: (self: ArrangerStatus) => Fixed::y(2, lay!(
//Self::help(),
//Fill::xy(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
//)));
//impl ArrangerStatus {
//fn help () -> impl Content<TuiOut> {
//let single = |binding, command|row!(" ", col!(
//Tui::fg(TuiTheme::yellow(), binding),
//command
//));
//let double = |(b1, c1), (b2, c2)|col!(
//row!(" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,),
//row!(" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,),
//);
//Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!(
//single("SPACE", "play/pause"),
//single(" Ctrl", " scroll"),
//single(" ▲▼▶◀", " cell"),
//double(("p", "put"), ("g", "get")),
//double(("q", "enqueue"), ("e", "edit")),
//single(" wsad", " note"),
//double(("a", "append"), ("s", "set"),),
//double((",.", "length"), ("<>", "triplet"),),
//double(("[]", "phrase"), ("{}", "order"),),
//))
//}
//fn stats (&self) -> impl Content<TuiOut> + use<'_> {
//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>)
-> Usually<&mut ArrangerScene>
{
let name = name.map_or_else(||self.scene_default_name(), |x|x.to_string());
let scene = ArrangerScene {
name: Arc::new(name.into()),
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),
};
@ -16,8 +15,8 @@ impl Arranger {
pub fn scene_del (&mut self, index: usize) {
todo!("delete scene");
}
fn scene_default_name (&self) -> String {
format!("Sc{:3>}", self.scenes.len() + 1)
fn scene_default_name (&self) -> Arc<str> {
format!("Sc{:3>}", self.scenes.len() + 1).into()
}
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().and_then(|s|self.scenes.get(s))
@ -28,14 +27,14 @@ impl Arranger {
}
#[derive(Default, Debug, Clone)] pub struct ArrangerScene {
/// Name of scene
pub(crate) name: Arc<RwLock<String>>,
pub(crate) name: Arc<str>,
/// Clips in scene, one per track
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
/// Identifying color of scene
pub(crate) color: ItemPalette,
}
impl ArrangerScene {
pub fn name (&self) -> &Arc<RwLock<String>> {
pub fn name (&self) -> &Arc<str> {
&self.name
}
pub fn clips (&self) -> &Vec<Option<Arc<RwLock<MidiClip>>>> {
@ -45,7 +44,7 @@ impl ArrangerScene {
self.color
}
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
pub fn pulses (&self) -> usize {

View file

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

View file

@ -1,17 +1,17 @@
use crate::*;
impl Arranger {
pub fn track_next_name (&self) -> String {
format!("Tr{}", self.tracks.len() + 1)
pub fn track_next_name (&self) -> Arc<str> {
format!("Tr{:02}", self.tracks.len() + 1).into()
}
pub fn track_add (&mut self, name: Option<&str>, color: Option<ItemPalette>)
-> 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 {
width: name.len() + 2,
name: Arc::new(name.into()),
color: color.unwrap_or_else(ItemPalette::random),
player: MidiPlayer::from(&self.clock),
name,
};
self.tracks.push(track);
let index = self.tracks.len() - 1;
@ -26,7 +26,7 @@ impl Arranger {
}
#[derive(Debug)] pub struct ArrangerTrack {
/// Name of track
pub name: Arc<RwLock<String>>,
pub name: Arc<str>,
/// Preferred width of track column
pub width: usize,
/// Identifying color of track
@ -38,7 +38,7 @@ has_clock!(|self:ArrangerTrack|self.player.clock());
has_player!(|self:ArrangerTrack|self.player);
impl ArrangerTrack {
/// Name of track
pub fn name (&self) -> &Arc<RwLock<String>> {
pub fn name (&self) -> &Arc<str> {
&self.name
}
/// Preferred width of track column
@ -54,7 +54,7 @@ impl ArrangerTrack {
self.color
}
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) {
*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;
render!(TuiOut: (self: Arranger) => {
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 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 editor = Fixed::y(20, enclosed(&self.editor));
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||[
(0, 2, self.output_row_header(), self.output_row_cells()),
(2, 2, self.elapsed_row_header(), self.elapsed_row_cells()),
(4, 2, self.next_row_header(), self.next_row_cells()),
(2, 3, self.elapsed_row_header(), self.elapsed_row_cells()),
(4, 3, self.next_row_header(), self.next_row_cells()),
(6, 3, self.track_row_header(), self.track_row_cells()),
(9, 20, self.scene_row_headers(), self.scene_row_cells()),
(29, 2, self.input_row_header(), self.input_row_cells()),
(8, 20, self.scene_row_headers(), self.scene_row_cells()),
(25, 2, self.input_row_header(), self.input_row_cells()),
].into_iter(),
move|(y, h, header, cells), index|map_south(y, h, Fill::x(Align::w(Bsp::e(
Fixed::xy(scenes_w, h, header),
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 {
@ -63,7 +61,7 @@ impl Arranger {
(move||Fixed::y(2, Map::new(||self.tracks_with_widths(), move|(_, track, x1, x2), i| {
let w = (x2 - x1) as u16;
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)))
})).boxed()).into()
}
@ -73,19 +71,17 @@ impl Arranger {
}
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| {
let color = track.color();
//let color = track.color();
let color: ItemPalette = track.color().dark.into();
let timebase = self.clock().timebase();
let elapsed = {
let mut result = String::new();
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb,
if let Some((_, Some(phrase))) = track.player.play_phrase().as_ref() {
let length = phrase.read().unwrap().length;
let elapsed = track.player.pulses_since_start().unwrap() as usize;
result = format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
}
result
};
let value = Tui::fg_bg(color.lightest.rgb, color.base.rgb, elapsed);
format!("+{:>}", timebase.format_beats_1_short((elapsed % length) as f64))
} else {
String::new()
});
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))
})).boxed()).into()
@ -109,12 +105,14 @@ impl Arranger {
(||Tui::bold(true, Tui::fg(TuiTheme::g(128), "Track")).boxed()).into()
}
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 name = format!(" {}", &track.name);
Tui::bg(color.base.rgb, map_east(x1 as u16, (x2 - x1) as u16,
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()))))))
})).boxed()).into()
Self::phat_cell(color, color.darkest.rgb.into(),
Tui::bold(true, name))))) })).boxed()).into()
}
fn input_row_header <'a> (&'a self) -> BoxThunk<'a, TuiOut> {
@ -138,7 +136,7 @@ impl Arranger {
move|(_, scene, y1, y2), i| {
let h = (y2 - y1) as u16;
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);
*last_color.write().unwrap() = color;
map_south(y1 as u16, 2, Fill::x(cell))
@ -147,7 +145,7 @@ impl Arranger {
}).into()
}
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)
@ -175,8 +173,7 @@ impl Arranger {
}
/// name and width of track
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, name))
Tui::bold(true, Tui::fg(track.color.lightest.rgb, track.name().clone()))
}
/// beats until switchover
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| {
let h = (y2 - y1) as u16;
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)
})
}
@ -235,7 +232,7 @@ impl Arranger {
scene.color.base.rgb, if playing { "" } else { " " }
);
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), _|
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<'_> {
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<'_> {
let w = self.size.w();
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(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 {
fn new (compact: bool, clock: &Clock) -> Self {
let (beat, time) = clock.started.read().unwrap().as_ref().map(|started|{
let current_usec = clock.global.usec.get() - started.usec.get();
let bpm = format!("{:.3}", clock.timebase.bpm.get()).into();
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)),
format!("{:.3}s", current_usec/1000000.)
clock.timebase.format_beats_1(clock.timebase.usecs_to_pulse(now)).into(),
format!("{:.3}s", now/1000000.).into()
)
}).unwrap_or_else(||("-.-.--".to_string(), "-.---s".to_string()));
Self { compact, bpm: format!("{:.3}", clock.timebase.bpm.get()), beat, time }
} else {
("-.-.--".to_string().into(), "-.---s".to_string().into())
};
Self { compact, bpm, beat, time }
}
}
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)),
)));
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 {
fn new (compact: bool, clock: &Clock) -> Self {
let rate = clock.timebase.sr.get();
@ -82,9 +85,9 @@ impl OutputStats {
format!("{:.1}kHz", rate / 1000.)
} else {
format!("{:.0}Hz", rate)
},
buffer_size: format!("{chunk}"),
latency: format!("{:.1}ms", chunk as f64 / rate * 1000.),
}.into(),
buffer_size: format!("{chunk}").into(),
latency: format!("{:.1}ms", chunk as f64 / rate * 1000.).into(),
}
}
}

View file

@ -6,10 +6,10 @@ use crate::*;
impl_time_unit!(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 (seconds, msecs) = (usecs / 1000000, usecs / 1000 % 1000);
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.sample.set(self.timebase.pulses_to_sample(pulse));
}
#[inline] pub fn format_beat (&self) -> String {
self.timebase.format_beats_1(self.pulse.get())
#[inline] pub fn format_beat (&self) -> Arc<str> {
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()
}
/// 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 ppq = self.ppq.get() as usize;
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
#[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 ppq = self.ppq.get() as usize;
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
#[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 ppq = self.ppq.get() as usize;
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
#[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 ppq = self.ppq.get() as usize;
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,
Select(usize),
Chdir(PathBuf),
Filter(String),
Filter(Arc<str>),
}
render!(TuiOut: (self: FileBrowser) => /*Stack::down(|add|{
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])),
":transport" => Box::new(TransportView::new(true, &self.player.clock)),
":clip-play" => Box::new(ClipSelected::play_phrase(&self.player)),
":clip-next" => Box::new(ClipSelected::next_phrase(&self.player)),
":clip-edit" => Box::new(MidiEditClip(&self.editor)),
":edit-stat" => Box::new(MidiEditStatus(&self.editor)),
":clip-play" => Box::new(self.player.play_status()),
":clip-next" => Box::new(self.player.next_status()),
":clip-edit" => Box::new(self.editor.clip_status()),
":edit-stat" => Box::new(self.editor.edit_status()),
":pool-view" => Box::new(PoolView(self.compact, &self.pool)),
":midi-view" => Box::new(&self.editor),

View file

@ -30,10 +30,10 @@ impl Groovebox {
}
fn selector_view (&self) -> impl Content<TuiOut> + use<'_> {
row!(
ClipSelected::play_phrase(&self.player),
ClipSelected::next_phrase(&self.player),
MidiEditClip(&self.editor),
MidiEditStatus(&self.editor),
self.player.play_status(),
self.player.next_status(),
self.editor.clip_status(),
self.editor.edit_status(),
)
}
fn sample_view (&self) -> impl Content<TuiOut> + use<'_> {
@ -53,7 +53,7 @@ impl Groovebox {
PoolView(self.compact, &self.pool))
}
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 };
Fixed::x(sampler_w, Push::y(sampler_y, Fill::y(
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.
pub enum JackEvent {
ThreadInit,
Shutdown(ClientStatus, String),
Shutdown(ClientStatus, Arc<str>),
Freewheel(bool),
SampleRate(Frames),
ClientRegistration(String, bool),
ClientRegistration(Arc<str>, bool),
PortRegistration(PortId, bool),
PortRename(PortId, String, String),
PortRename(PortId, Arc<str>, Arc<str>),
PortsConnected(PortId, PortId, bool),
GraphReorder,
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 title: String,
pub title: Arc<str>,
pub items: Vec<MenuItem<E, S, C>>,
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_point; pub(crate) use midi_point::*;
pub(crate) mod midi_view; pub(crate) use midi_view::*;
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.
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {

View file

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

View file

@ -122,6 +122,39 @@ impl MidiEditor {
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 {

View file

@ -52,7 +52,9 @@ impl MidiPlayer {
midi_from: &[impl AsRef<str>],
midi_to: &[impl AsRef<str>],
) -> 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 {
clock: Clock::from(jack),
play_phrase: None,
@ -62,19 +64,20 @@ impl MidiPlayer {
overdub: false,
notes_in: RwLock::new([false;128]).into(),
midi_ins: vec![
jack.midi_in(&format!("M/{name}"), midi_from)?,
],
midi_outs: vec![
jack.midi_out(&format!("{name}/M"), midi_to)?,
],
midi_ins: vec![midi_in],
midi_outs: vec![midi_out],
notes_out: RwLock::new([false;128]).into(),
reset: true,
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 {
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),
Import(usize, PathBuf),
Export(usize, PathBuf),
SetName(usize, String),
SetName(usize, Arc<str>),
SetLength(usize, usize),
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 {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackConnection>>,
pub name: String,
pub name: Arc<str>,
pub tracks: Vec<MixerTrack>,
pub selected_track: usize,
pub selected_column: usize,
@ -13,7 +13,7 @@ pub struct Mixer {
/// A mixer track.
#[derive(Debug)]
pub struct MixerTrack {
pub name: String,
pub name: Arc<str>,
/// Inputs of 1st device
pub audio_ins: Vec<Port<AudioIn>>,
/// Outputs of last device
@ -47,7 +47,7 @@ impl Mixer {
impl MixerTrack {
pub fn new (name: &str) -> Usually<Self> {
Ok(Self {
name: name.to_string(),
name: name.to_string().into(),
audio_ins: vec![],
audio_outs: vec![],
devices: vec![],
@ -267,7 +267,7 @@ const SYM_LV2: &str = "lv2";
from_edn!("mixer/track" => |jack: &Arc<RwLock<JackConnection>>, args| -> MixerTrack {
let mut _gain = 0.0f64;
let mut track = MixerTrack {
name: String::new(),
name: "".into(),
audio_ins: vec![],
audio_outs: vec![],
devices: vec![],

View file

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

View file

@ -24,7 +24,7 @@ pub struct PoolModel {
#[derive(Debug, Clone)]
pub enum PoolMode {
/// Renaming a pattern
Rename(usize, String),
Rename(usize, Arc<str>),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
/// Load phrase from disk
@ -147,10 +147,10 @@ fn to_phrases_command (state: &PoolModel, input: &Event) -> Option<PoolCommand>
return None
},
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(
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')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate();

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@ render!(TuiOut: (self: PoolView<'a>) => {
let Self(compact, model) = self;
let PoolModel { phrases, mode, .. } = self.1;
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)
).enclose(Map::new(||model.phrases().iter(), |clip, i|{
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::e(When(selected, Tui::bold(true, Tui::fg(TuiTheme::g(255), "")))),
)))
}))
}))))
});

View file

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

View file

@ -18,9 +18,7 @@ render!(TuiOut: (self: SampleList<'a>) => {
let note_pt = editor.note_point();
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| {
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 fg = TuiTheme::g(160);
if sampler.mapped[note].is_some() {
@ -33,13 +31,20 @@ render!(TuiOut: (self: SampleList<'a>) => {
fg = Color::Rgb(224,64,32)
}
}
offset(Tui::fg_bg(fg, bg, format!("{note:3} {}", if *compact {
let label = if *compact {
String::default()
} 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 {
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))))
}
fn status_view (&self) -> impl Content<TuiOut> + use<'_> {
let edit_clip = MidiEditClip(&self.editor);
let selectors = When(self.selectors, Bsp::e(ClipSelected::play_phrase(&self.player), ClipSelected::next_phrase(&self.player)));
row!(selectors, edit_clip, MidiEditStatus(&self.editor))
row!(
When(self.selectors, Bsp::e(
self.player.play_status(),
self.player.next_status(),
)),
self.editor.clip_status(),
self.editor.edit_status(),
)
}
fn selector_view (&self) -> impl Content<TuiOut> + use<'_> {
row!(
ClipSelected::play_phrase(&self.player),
ClipSelected::next_phrase(&self.player),
MidiEditClip(&self.editor),
MidiEditStatus(&self.editor),
self.player.play_status(),
self.player.next_status(),
self.editor.clip_status(),
self.editor.edit_status(),
)
}
fn pool_view (&self) -> impl Content<TuiOut> + use<'_> {
@ -180,8 +185,8 @@ command!(|self: SequencerCommand, state: Sequencer|match self {
#[derive(Clone)]
pub struct SequencerStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) cpu: Option<Arc<str>>,
pub(crate) size: Arc<str>,
pub(crate) playing: bool,
}
from!(|state:&Sequencer|SequencerStatus = {
@ -192,8 +197,8 @@ from!(|state:&Sequencer|SequencerStatus = {
Self {
width,
playing: state.clock.is_rolling(),
cpu: state.perf.percentage().map(|cpu|format!("{cpu:.01}%")),
size: format!("{}x{}│", width, state.size.h()),
cpu: state.perf.percentage().map(|cpu|format!("{cpu:.01}%").into()),
size: format!("{}x{}│", width, state.size.h()).into(),
}
});
render!(TuiOut: (self: SequencerStatus) => Fixed::y(2, lay!(