flatten workspace into 1 crate

This commit is contained in:
🪞👃🪞 2024-12-29 00:10:30 +01:00
parent 7c4e1e2166
commit d926422c67
147 changed files with 66 additions and 126 deletions

65
src/README.md Normal file
View file

@ -0,0 +1,65 @@
# `tek`
This crate implements several musical utilities.
## `tek_sequencer`
A single-track, multi-pattern MIDI sequencer with properly tempo-synced pattern switch.
---
## `tek_arranger`
A multi-track, multi-pattern MIDI sequencer translating the familiar clip launching workflow
into the TUI medium.
---
## `tek_groovebox`
TODO: A single-track, multi-pattern MIDI sequencer,
attached to a sampler or plugin (see `tek_plugin`).
---
## `tek_timer`
TODO: This crate implements time sync and JACK transport control.
* Warning: If transport is set rolling by qjackctl, this program can't pause it
* Todo: bpm: shift +/- 0.001
* Todo: quant/sync: shift = next/prev value of same type (normal, triplet, dotted)
* Or: use shift to switch between inc/dec top/bottom value?
* Todo: focus play button
* Todo: focus time position
* Todo: edit numeric values
* Todo: jump to time/bbt markers
* Todo: count xruns
---
## `tek_mixer`
TODO:
// - Meters: propagate clipping:
// - If one stage clips, all stages after it are marked red
// - If one track clips, all tracks that feed from it are marked red?
---
## `tek_track`
TODO:
---
## `tek_sampler`
TODO: This crate implements a sampler device which plays audio files
in response to MIDI notes.
---
## `tek_plugin`
TODO:

1
src/api.rs Normal file
View file

@ -0,0 +1 @@
mod sampler; pub(crate) use sampler::*;

4
src/architecture.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

162
src/arranger.rs Normal file
View file

@ -0,0 +1,162 @@
use crate::*;
mod arranger_command; pub(crate) use self::arranger_command::*;
mod arranger_scene; pub(crate) use self::arranger_scene::*;
mod arranger_select; pub(crate) use self::arranger_select::*;
mod arranger_track; pub(crate) use self::arranger_track::*;
mod arranger_mode; pub(crate) use self::arranger_mode::*;
mod arranger_v; #[allow(unused)] pub(crate) use self::arranger_v::*;
mod arranger_h;
/// Root view for standalone `tek_arranger`
pub struct ArrangerTui {
jack: Arc<RwLock<JackClient>>,
pub clock: ClockModel,
pub phrases: PoolModel,
pub tracks: Vec<ArrangerTrack>,
pub scenes: Vec<ArrangerScene>,
pub splits: [u16;2],
pub selected: ArrangerSelection,
pub mode: ArrangerMode,
pub color: ItemPalette,
pub size: Measure<Tui>,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub editor: MidiEditorModel,
pub perf: PerfModel,
}
impl ArrangerTui {
pub fn selected (&self) -> ArrangerSelection {
self.selected
}
pub fn selected_mut (&mut self) -> &mut ArrangerSelection {
&mut self.selected
}
pub fn activate (&mut self) -> Usually<()> {
if let ArrangerSelection::Scene(s) = self.selected {
for (t, track) in self.tracks.iter_mut().enumerate() {
let phrase = self.scenes[s].clips[t].clone();
if track.player.play_phrase.is_some() || phrase.is_some() {
track.player.enqueue_next(phrase.as_ref());
}
}
if self.clock().is_stopped() {
self.clock().play_from(Some(0))?;
}
} else if let ArrangerSelection::Clip(t, s) = self.selected {
let phrase = self.scenes[s].clips[t].clone();
self.tracks[t].player.enqueue_next(phrase.as_ref());
};
Ok(())
}
pub fn selected_phrase (&self) -> Option<Arc<RwLock<MidiClip>>> {
self.selected_scene()?.clips.get(self.selected.track()?)?.clone()
}
pub fn toggle_loop (&mut self) {
if let Some(phrase) = self.selected_phrase() {
phrase.write().unwrap().toggle_loop()
}
}
pub fn randomize_color (&mut self) {
match self.selected {
ArrangerSelection::Mix => { self.color = ItemPalette::random() },
ArrangerSelection::Track(t) => { self.tracks[t].color = ItemPalette::random() },
ArrangerSelection::Scene(s) => { self.scenes[s].color = ItemPalette::random() },
ArrangerSelection::Clip(t, s) => if let Some(phrase) = &self.scenes[s].clips[t] {
phrase.write().unwrap().color = ItemPalette::random();
}
}
}
}
from_jack!(|jack| ArrangerTui {
let clock = ClockModel::from(jack);
let phrase = Arc::new(RwLock::new(MidiClip::new(
"New", true, 4 * clock.timebase.ppq.get() as usize,
None, Some(ItemColor::random().into())
)));
Self {
clock,
phrases: (&phrase).into(),
editor: (&phrase).into(),
selected: ArrangerSelection::Clip(0, 0),
scenes: vec![],
tracks: vec![],
color: TuiTheme::bg().into(),
mode: ArrangerMode::V(1),
size: Measure::new(),
splits: [12, 20],
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
jack: jack.clone(),
}
});
impl ArrangerTui {
fn render_mode (state: &Self) -> impl Render<Tui> + use<'_> {
match state.mode {
ArrangerMode::H => todo!("horizontal arranger"),
ArrangerMode::V(factor) => Self::render_mode_v(state, factor),
}
}
}
render!(<Tui>|self: ArrangerTui|{
let play = Fixed::wh(5, 2, PlayPause(self.clock.is_rolling()));
let transport = TransportView::from((self, Some(ItemPalette::from(TuiTheme::g(96))), true));
let with_transport = |x|col!([row!(![&play, &transport]), &x]);
let pool_size = if self.phrases.visible { self.splits[1] } else { 0 };
let with_pool = |x|Split::left(false, pool_size, PoolView(&self.phrases), x);
let status = ArrangerStatus::from(self);
let with_editbar = |x|Tui::split_n(false, 1, MidiEditStatus(&self.editor), x);
let with_status = |x|Tui::split_n(false, 2, status, x);
let with_size = |x|lay!([&self.size, x]);
let arranger = ||lay!(|add|{
let color = self.color;
add(&Fill::wh(Tui::bg(color.darkest.rgb, ())))?;
add(&Fill::wh(Outer(Style::default().fg(color.dark.rgb).bg(color.darkest.rgb))))?;
add(&Self::render_mode(self))
});
with_size(with_status(with_editbar(with_pool(with_transport(col!([
Fill::w(Fixed::h(20, arranger())),
Fill::wh(&self.editor),
]))))))
});
audio!(|self: ArrangerTui, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if Control::Quit == ClockAudio(self).process(client, scope) {
return Control::Quit
}
// Update MIDI sequencers
let tracks = &mut self.tracks;
let note_buf = &mut self.note_buf;
let midi_buf = &mut self.midi_buf;
if Control::Quit == TracksAudio(tracks, note_buf, midi_buf).process(client, scope) {
return Control::Quit
}
// FIXME: one of these per playing track
//self.now.set(0.);
//if let ArrangerSelection::Clip(t, s) = self.selected {
//let phrase = self.scenes.get(s).map(|scene|scene.clips.get(t));
//if let Some(Some(Some(phrase))) = phrase {
//if let Some(track) = self.tracks().get(t) {
//if let Some((ref started_at, Some(ref playing))) = track.player.play_phrase {
//let phrase = phrase.read().unwrap();
//if *playing.read().unwrap() == *phrase {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % phrase.length as f64;
//self.now.set(now);
//}
//}
//}
//}
//}
// End profiling cycle
self.perf.update(t0, scope);
return Control::Continue
});
has_clock!(|self: ArrangerTui|&self.clock);
has_phrases!(|self: ArrangerTui|self.phrases.phrases);
has_editor!(|self: ArrangerTui|self.editor);
handle!(<Tui>|self: ArrangerTui, input|ArrangerCommand::execute_with_state(self, input));

View file

@ -0,0 +1,339 @@
use crate::*;
use ClockCommand::{Play, Pause};
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
#[derive(Clone, Debug)] pub enum ArrangerCommand {
History(isize),
Color(ItemPalette),
Clock(ClockCommand),
Scene(ArrangerSceneCommand),
Track(ArrangerTrackCommand),
Clip(ArrangerClipCommand),
Select(ArrangerSelection),
Zoom(usize),
Phrases(PoolCommand),
Editor(PhraseCommand),
StopAll,
Clear,
}
#[derive(Clone, Debug)]
pub enum ArrangerTrackCommand {
Add,
Delete(usize),
Stop(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
}
#[derive(Clone, Debug)]
pub enum ArrangerSceneCommand {
Enqueue(usize),
Add,
Delete(usize),
Swap(usize, usize),
SetSize(usize),
SetZoom(usize),
SetColor(usize, ItemPalette),
}
#[derive(Clone, Debug)]
pub enum ArrangerClipCommand {
Get(usize, usize),
Put(usize, usize, Option<Arc<RwLock<MidiClip>>>),
Enqueue(usize, usize),
Edit(Option<Arc<RwLock<MidiClip>>>),
SetLoop(usize, usize, bool),
SetColor(usize, usize, ItemPalette),
}
input_to_command!(ArrangerCommand: <Tui>|state: ArrangerTui, input|match input.event() {
key_pat!(Char('u')) => Self::History(-1),
key_pat!(Char('U')) => Self::History(1),
// TODO: k: toggle on-screen keyboard
key_pat!(Ctrl-Char('k')) => { todo!("keyboard") },
// Transport: Play/pause
key_pat!(Char(' ')) =>
Self::Clock(if state.clock().is_stopped() { Play(None) } else { Pause(None) }),
// Transport: Play from start or rewind to start
key_pat!(Shift-Char(' ')) =>
Self::Clock(if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }),
key_pat!(Char('e')) =>
Self::Editor(PhraseCommand::Show(Some(state.phrases.phrase().clone()))),
key_pat!(Ctrl-Left) =>
Self::Scene(ArrangerSceneCommand::Add),
key_pat!(Ctrl-Char('t')) =>
Self::Track(ArrangerTrackCommand::Add),
// Tab: Toggle visibility of phrase pool column
key_pat!(Tab) =>
Self::Phrases(PoolCommand::Show(!state.phrases.visible)),
_ => {
use ArrangerCommand as Cmd;
use ArrangerSelection as Selected;
use ArrangerSceneCommand as Scene;
use ArrangerTrackCommand as Track;
use ArrangerClipCommand as Clip;
let t_len = state.tracks.len();
let s_len = state.scenes.len();
match state.selected() {
Selected::Clip(t, s) => match input.event() {
key_pat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))),
key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))),
key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))),
key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Up) => Some(Cmd::Select(
if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })),
key_pat!(Down) => Some(Cmd::Select(
Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))),
key_pat!(Left) => Some(Cmd::Select(
if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })),
key_pat!(Right) => Some(Cmd::Select(
Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))),
_ => None
},
Selected::Scene(s) => match input.event() {
key_pat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
key_pat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
key_pat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
key_pat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
key_pat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))),
key_pat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))),
key_pat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))),
key_pat!(Up) => Some(
Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })),
key_pat!(Down) => Some(
Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))),
key_pat!(Left) =>
return None,
key_pat!(Right) => Some(
Cmd::Select(Selected::Clip(0, s))),
_ => None
},
Selected::Track(t) => match input.event() {
key_pat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
key_pat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
key_pat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
key_pat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
key_pat!(Delete) => Some(Cmd::Track(Track::Delete(t))),
key_pat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))),
key_pat!(Up) =>
return None,
key_pat!(Down) => Some(
Cmd::Select(Selected::Clip(t, 0))),
key_pat!(Left) => Some(
Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })),
key_pat!(Right) => Some(
Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))),
_ => None
},
Selected::Mix => match input.event() {
key_pat!(Delete) => Some(Cmd::Clear),
key_pat!(Char('0')) => Some(Cmd::StopAll),
key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
key_pat!(Up) =>
return None,
key_pat!(Down) => Some(
Cmd::Select(Selected::Scene(0))),
key_pat!(Left) =>
return None,
key_pat!(Right) => Some(
Cmd::Select(Selected::Track(0))),
_ => None
},
}
}.or_else(||if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) {
Some(Self::Editor(command))
} else if let Some(command) = PoolCommand::input_to_command(&state.phrases, input) {
Some(Self::Phrases(command))
} else {
None
})?
});
fn to_arrangement_command (state: &ArrangerTui, input: &TuiInput) -> Option<ArrangerCommand> {
use ArrangerCommand as Cmd;
use ArrangerSelection as Selected;
use ArrangerSceneCommand as Scene;
use ArrangerTrackCommand as Track;
use ArrangerClipCommand as Clip;
let t_len = state.tracks.len();
let s_len = state.scenes.len();
match state.selected() {
Selected::Clip(t, s) => match input.event() {
key_pat!(Char('g')) => Some(Cmd::Phrases(PoolCommand::Select(0))),
key_pat!(Char('q')) => Some(Cmd::Clip(Clip::Enqueue(t, s))),
key_pat!(Char(',')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('.')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('<')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('>')) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Char('p')) => Some(Cmd::Clip(Clip::Put(t, s, Some(state.phrases.phrase().clone())))),
key_pat!(Char('l')) => Some(Cmd::Clip(ArrangerClipCommand::SetLoop(t, s, false))),
key_pat!(Delete) => Some(Cmd::Clip(Clip::Put(t, s, None))),
key_pat!(Up) => Some(Cmd::Select(
if s > 0 { Selected::Clip(t, s - 1) } else { Selected::Track(t) })),
key_pat!(Down) => Some(Cmd::Select(
Selected::Clip(t, (s + 1).min(s_len.saturating_sub(1))))),
key_pat!(Left) => Some(Cmd::Select(
if t > 0 { Selected::Clip(t - 1, s) } else { Selected::Scene(s) })),
key_pat!(Right) => Some(Cmd::Select(
Selected::Clip((t + 1).min(t_len.saturating_sub(1)), s))),
_ => None
},
Selected::Scene(s) => match input.event() {
key_pat!(Char(',')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
key_pat!(Char('.')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
key_pat!(Char('<')) => Some(Cmd::Scene(Scene::Swap(s, s - 1))),
key_pat!(Char('>')) => Some(Cmd::Scene(Scene::Swap(s, s + 1))),
key_pat!(Char('q')) => Some(Cmd::Scene(Scene::Enqueue(s))),
key_pat!(Delete) => Some(Cmd::Scene(Scene::Delete(s))),
key_pat!(Char('c')) => Some(Cmd::Scene(Scene::SetColor(s, ItemPalette::random()))),
key_pat!(Up) => Some(
Cmd::Select(if s > 0 { Selected::Scene(s - 1) } else { Selected::Mix })),
key_pat!(Down) => Some(
Cmd::Select(Selected::Scene((s + 1).min(s_len.saturating_sub(1))))),
key_pat!(Left) =>
None,
key_pat!(Right) => Some(
Cmd::Select(Selected::Clip(0, s))),
_ => None
},
Selected::Track(t) => match input.event() {
key_pat!(Char(',')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
key_pat!(Char('.')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
key_pat!(Char('<')) => Some(Cmd::Track(Track::Swap(t, t - 1))),
key_pat!(Char('>')) => Some(Cmd::Track(Track::Swap(t, t + 1))),
key_pat!(Delete) => Some(Cmd::Track(Track::Delete(t))),
key_pat!(Char('c')) => Some(Cmd::Track(Track::SetColor(t, ItemPalette::random()))),
key_pat!(Up) =>
None,
key_pat!(Down) => Some(
Cmd::Select(Selected::Clip(t, 0))),
key_pat!(Left) => Some(
Cmd::Select(if t > 0 { Selected::Track(t - 1) } else { Selected::Mix })),
key_pat!(Right) => Some(
Cmd::Select(Selected::Track((t + 1).min(t_len.saturating_sub(1))))),
_ => None
},
Selected::Mix => match input.event() {
key_pat!(Delete) => Some(Cmd::Clear),
key_pat!(Char('0')) => Some(Cmd::StopAll),
key_pat!(Char('c')) => Some(Cmd::Color(ItemPalette::random())),
key_pat!(Up) =>
None,
key_pat!(Down) => Some(
Cmd::Select(Selected::Scene(0))),
key_pat!(Left) =>
None,
key_pat!(Right) => Some(
Cmd::Select(Selected::Track(0))),
_ => None
},
}
}
command!(|self: ArrangerCommand, state: ArrangerTui|match self {
Self::Scene(cmd) => cmd.execute(state)?.map(Self::Scene),
Self::Track(cmd) => cmd.execute(state)?.map(Self::Track),
Self::Clip(cmd) => cmd.execute(state)?.map(Self::Clip),
Self::Editor(cmd) => cmd.execute(&mut state.editor)?.map(Self::Editor),
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
Self::Zoom(_) => { todo!(); },
Self::Select(selected) => {
*state.selected_mut() = selected;
None
},
Self::Color(palette) => {
let old = state.color;
state.color = palette;
Some(Self::Color(old))
},
Self::Phrases(cmd) => {
let mut default = |cmd: PoolCommand|{
cmd.execute(&mut state.phrases).map(|x|x.map(Self::Phrases))
};
match cmd {
// autoselect: automatically load selected phrase in editor
PoolCommand::Select(_) => {
let undo = default(cmd)?;
state.editor.set_phrase(Some(state.phrases.phrase()));
undo
},
// reload phrase in editor to update color
PoolCommand::Phrase(PhrasePoolCommand::SetColor(index, _)) => {
let undo = default(cmd)?;
state.editor.set_phrase(Some(state.phrases.phrase()));
undo
},
_ => default(cmd)?
}
},
Self::History(_) => { todo!() },
Self::StopAll => {
for track in 0..state.tracks.len() {
state.tracks[track].player.enqueue_next(None);
}
None
},
Self::Clear => { todo!() },
});
command!(|self: ArrangerTrackCommand, state: ArrangerTui|match self {
Self::SetColor(index, color) => {
let old = state.tracks[index].color;
state.tracks[index].color = color;
Some(Self::SetColor(index, old))
},
Self::Stop(track) => {
state.tracks[track].player.enqueue_next(None);
None
},
_ => None
});
command!(|self: ArrangerSceneCommand, state: ArrangerTui|match self {
Self::Delete(index) => {
state.scene_del(index);
None
},
Self::SetColor(index, color) => {
let old = state.scenes[index].color;
state.scenes[index].color = color;
Some(Self::SetColor(index, old))
},
Self::Enqueue(scene) => {
for track in 0..state.tracks.len() {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
}
None
},
_ => None
});
command!(|self: ArrangerClipCommand, state: ArrangerTui|match self {
Self::Get(track, scene) => { todo!() },
Self::Put(track, scene, phrase) => {
let old = state.scenes[scene].clips[track].clone();
state.scenes[scene].clips[track] = phrase;
Some(Self::Put(track, scene, old))
},
Self::Enqueue(track, scene) => {
state.tracks[track].player.enqueue_next(state.scenes[scene].clips[track].as_ref());
None
},
_ => None
});

View file

@ -0,0 +1 @@
// TODO

View file

@ -0,0 +1,26 @@
use crate::*;
/// Display mode of arranger
#[derive(Clone, PartialEq)]
pub enum ArrangerMode {
/// Tracks are columns
V(usize),
/// Tracks are rows
H,
}
render!(<Tui>|self: ArrangerMode|{});
/// Arranger display mode can be cycled
impl ArrangerMode {
/// Cycle arranger display mode
pub fn next (&mut self) {
*self = match self {
Self::H => Self::V(1),
Self::V(1) => Self::V(2),
Self::V(2) => Self::V(2),
Self::V(0) => Self::H,
Self::V(_) => Self::V(0),
}
}
}
fn any_size <E: Engine> (_: E::Size) -> Perhaps<E::Size>{
Ok(Some([0.into(),0.into()].into()))
}

View file

@ -0,0 +1,91 @@
use crate::*;
impl ArrangerTui {
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()),
clips: vec![None;self.tracks.len()],
color: color.unwrap_or_else(ItemPalette::random),
};
self.scenes.push(scene);
let index = self.scenes.len() - 1;
Ok(&mut self.scenes[index])
}
pub fn scene_del (&mut self, index: usize) {
todo!("delete scene");
}
fn scene_default_name (&self) -> String {
format!("S{:3>}", self.scenes.len() + 1)
}
pub fn selected_scene (&self) -> Option<&ArrangerScene> {
self.selected.scene().and_then(|s|self.scenes.get(s))
}
pub fn selected_scene_mut (&mut self) -> Option<&mut ArrangerScene> {
self.selected.scene().and_then(|s|self.scenes.get_mut(s))
}
}
#[derive(Default, Debug, Clone)] pub struct ArrangerScene {
/// Name of scene
pub(crate) name: Arc<RwLock<String>>,
/// Clips in scene, one per track
pub(crate) clips: Vec<Option<Arc<RwLock<MidiClip>>>>,
/// Identifying color of scene
pub(crate) color: ItemPalette,
}
impl ArrangerScene {
pub fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
pub fn clips (&self) -> &Vec<Option<Arc<RwLock<MidiClip>>>> {
&self.clips
}
pub fn color (&self) -> ItemPalette {
self.color
}
pub fn ppqs (scenes: &[Self], factor: usize) -> Vec<(usize, usize)> {
let mut total = 0;
if factor == 0 {
scenes.iter().map(|scene|{
let pulses = scene.pulses().max(PPQ);
total += pulses;
(pulses, total - pulses)
}).collect()
} else {
(0..=scenes.len()).map(|i|{
(factor*PPQ, factor*PPQ*i)
}).collect()
}
}
pub fn longest_name (scenes: &[Self]) -> usize {
scenes.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
}
/// Returns the pulse length of the longest phrase in the scene
pub fn pulses (&self) -> usize {
self.clips().iter().fold(0, |a, p|{
a.max(p.as_ref().map(|q|q.read().unwrap().length).unwrap_or(0))
})
}
/// Returns true if all phrases in the scene are
/// currently playing on the given collection of tracks.
pub fn is_playing (&self, tracks: &[ArrangerTrack]) -> bool {
self.clips().iter().any(|clip|clip.is_some()) && self.clips().iter().enumerate()
.all(|(track_index, clip)|match clip {
Some(clip) => tracks
.get(track_index)
.map(|track|{
if let Some((_, Some(phrase))) = track.player().play_phrase() {
*phrase.read().unwrap() == *clip.read().unwrap()
} else {
false
}
})
.unwrap_or(false),
None => true
})
}
pub fn clip (&self, index: usize) -> Option<&Arc<RwLock<MidiClip>>> {
match self.clips().get(index) { Some(Some(clip)) => Some(clip), _ => None }
}
}

View file

@ -0,0 +1,60 @@
use crate::*;
#[derive(PartialEq, Clone, Copy, Debug)]
/// Represents the current user selection in the arranger
pub enum ArrangerSelection {
/// The whole mix is selected
Mix,
/// A track is selected.
Track(usize),
/// A scene is selected.
Scene(usize),
/// A clip (track × scene) is selected.
Clip(usize, usize),
}
/// Focus identification methods
impl ArrangerSelection {
pub fn is_mix (&self) -> bool { matches!(self, Self::Mix) }
pub fn is_track (&self) -> bool { matches!(self, Self::Track(_)) }
pub fn is_scene (&self) -> bool { matches!(self, Self::Scene(_)) }
pub fn is_clip (&self) -> bool { matches!(self, Self::Clip(_, _)) }
pub fn description (
&self,
tracks: &[ArrangerTrack],
scenes: &[ArrangerScene],
) -> String {
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::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),
None => format!("T{t} S{s}: Empty")
},
_ => format!("T{t} S{s}: Empty"),
}
})
}
pub fn track (&self) -> Option<usize> {
use ArrangerSelection::*;
match self {
Clip(t, _) => Some(*t),
Track(t) => Some(*t),
_ => None
}
}
pub fn scene (&self) -> Option<usize> {
use ArrangerSelection::*;
match self {
Clip(_, s) => Some(*s),
Scene(s) => Some(*s),
_ => None
}
}
}

View file

@ -0,0 +1,111 @@
use crate::*;
impl ArrangerTui {
pub fn track_next_name (&self) -> String {
format!("T{}", self.tracks.len() + 1)
}
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 track = ArrangerTrack {
width: name.len() + 2,
name: Arc::new(name.into()),
color: color.unwrap_or_else(ItemPalette::random),
player: MidiPlayer::from(&self.clock),
};
self.tracks.push(track);
let index = self.tracks.len() - 1;
Ok(&mut self.tracks[index])
}
pub fn track_del (&mut self, index: usize) {
self.tracks.remove(index);
for scene in self.scenes.iter_mut() {
scene.clips.remove(index);
}
}
}
#[derive(Debug)] pub struct ArrangerTrack {
/// Name of track
pub name: Arc<RwLock<String>>,
/// Preferred width of track column
pub width: usize,
/// Identifying color of track
pub color: ItemPalette,
/// MIDI player state
pub player: MidiPlayer,
}
has_clock!(|self:ArrangerTrack|self.player.clock());
has_player!(|self:ArrangerTrack|self.player);
impl ArrangerTrack {
pub fn widths (tracks: &[Self]) -> Vec<(usize, usize)> {
let mut widths = vec![];
let mut total = 0;
for track in tracks.iter() {
let width = track.width;
widths.push((width, total));
total += width;
}
widths.push((0, total));
widths
}
pub fn with_widths (tracks: &[ArrangerTrack])
-> impl Iterator<Item = (usize, &ArrangerTrack, usize, usize)>
{
let mut x = 0;
tracks.iter().enumerate().map(move |(index, track)|{
let data = (index, track, x, x + track.width);
x += track.width;
data
})
}
/// Name of track
pub fn name (&self) -> &Arc<RwLock<String>> {
&self.name
}
/// Preferred width of track column
fn width (&self) -> usize {
self.width
}
/// Preferred width of track column
fn width_mut (&mut self) -> &mut usize {
&mut self.width
}
/// Identifying color of track
pub fn color (&self) -> ItemPalette {
self.color
}
fn longest_name (tracks: &[Self]) -> usize {
tracks.iter().map(|s|s.name().read().unwrap().len()).fold(0, usize::max)
}
pub const MIN_WIDTH: usize = 6;
fn width_inc (&mut self) {
*self.width_mut() += 1;
}
fn width_dec (&mut self) {
if self.width() > Self::MIN_WIDTH {
*self.width_mut() -= 1;
}
}
}
/// Hosts the JACK callback for a collection of tracks
pub struct TracksAudio<'a>(
// Track collection
pub &'a mut [ArrangerTrack],
/// Note buffer
pub &'a mut Vec<u8>,
/// Note chunk buffer
pub &'a mut Vec<Vec<Vec<u8>>>,
);
impl Audio for TracksAudio<'_> {
#[inline] fn process (&mut self, client: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
let note_buffer = &mut self.1;
let output_buffer = &mut self.2;
for track in model.iter_mut() {
if PlayerAudio(track.player_mut(), note_buffer, output_buffer).process(client, scope) == Control::Quit {
return Control::Quit
}
}
Control::Continue
}
}

View file

@ -0,0 +1,23 @@
use crate::*;
mod v_clips; pub(crate) use self::v_clips::*;
mod v_cursor; pub(crate) use self::v_cursor::*;
mod v_head; pub(crate) use self::v_head::*;
mod v_io; pub(crate) use self::v_io::*;
mod v_sep; pub(crate) use self::v_sep::*;
const HEADER_H: u16 = 5;
const SCENES_W_OFFSET: u16 = 3;
impl ArrangerTui {
pub fn render_mode_v (state: &ArrangerTui, factor: usize) -> impl Render<Tui> + use<'_> {
lay!([
ArrangerVColSep::from(state),
ArrangerVRowSep::from((state, factor)),
col!([
ArrangerVHead::from(state),
ArrangerVIns::from(state),
ArrangerVClips::from((state, factor)),
ArrangerVOuts::from(state),
]),
ArrangerVCursor::from((state, factor)),
])
}
}

View file

@ -0,0 +1,62 @@
use crate::*;
use super::*;
pub struct ArrangerVClips<'a> {
size: &'a Measure<Tui>,
scenes: &'a Vec<ArrangerScene>,
tracks: &'a Vec<ArrangerTrack>,
rows: Vec<(usize, usize)>,
}
from!(<'a>|args:(&'a ArrangerTui, usize)|ArrangerVClips<'a> = Self {
size: &args.0.size,
scenes: &args.0.scenes,
tracks: &args.0.tracks,
rows: ArrangerScene::ppqs(&args.0.scenes, args.1),
});
render!(<Tui>|self: ArrangerVClips<'a>|Fill::wh(
col!((scene, pulses) in self.scenes.iter().zip(self.rows.iter().map(|row|row.0)) => {
Self::format_scene(self.tracks, scene, pulses)
})
));
impl<'a> ArrangerVClips<'a> {
fn format_scene (
tracks: &'a [ArrangerTrack], scene: &'a ArrangerScene, pulses: usize
) -> impl Render<Tui> + use<'a> {
let height = 1.max((pulses / PPQ) as u16);
let playing = scene.is_playing(tracks);
Fixed::h(height, row!([
Tui::bg(scene.color.base.rgb,
if playing { "" } else { " " }),
Tui::fg_bg(scene.color.lightest.rgb, scene.color.base.rgb,
Tui::grow_x(1, Tui::bold(true, scene.name.read().unwrap().as_str()))),
row!((index, track, x1, x2) in ArrangerTrack::with_widths(tracks) => {
Self::format_clip(scene, index, track, (x2 - x1) as u16, height)
})])
)
}
fn format_clip (
scene: &'a ArrangerScene, index: usize, track: &'a ArrangerTrack, w: u16, h: u16
) -> impl Render<Tui> + use<'a> {
Fixed::wh(w, h, Layers::new(move |add|{
if let Some(Some(phrase)) = scene.clips.get(index) {
let mut bg = TuiTheme::border_bg();
let name = &(phrase as &Arc<RwLock<MidiClip>>).read().unwrap().name.to_string();
let max_w = name.len().min((w as usize).saturating_sub(2));
let color = phrase.read().unwrap().color;
bg = color.dark.rgb;
if let Some((_, Some(ref playing))) = track.player.play_phrase() {
if *playing.read().unwrap() == *phrase.read().unwrap() {
bg = color.light.rgb
}
};
add(&Tui::bg(bg,
Tui::push_x(1, Fixed::w(w, &name.as_str()[0..max_w])))
)?;
}
Ok(())
}))
}
}

View file

@ -0,0 +1,81 @@
use crate::*;
use super::*;
pub struct ArrangerVCursor {
cols: Vec<(usize, usize)>,
rows: Vec<(usize, usize)>,
color: ItemPalette,
reticle: Reticle,
selected: ArrangerSelection,
scenes_w: u16,
}
from!(|args:(&ArrangerTui, usize)|ArrangerVCursor = Self {
cols: ArrangerTrack::widths(&args.0.tracks),
rows: ArrangerScene::ppqs(&args.0.scenes, args.1),
selected: args.0.selected(),
scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&args.0.scenes) as u16,
color: args.0.color,
reticle: Reticle(Style {
fg: Some(args.0.color.lighter.rgb),
bg: None,
underline_color: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::DIM
}),
});
render!(<Tui>|self: ArrangerVCursor|render(move|to: &mut TuiOutput|{
let area = to.area();
let focused = true;
let selected = self.selected;
let get_track_area = |t: usize| [
self.scenes_w + area.x() + self.cols[t].1 as u16, area.y(),
self.cols[t].0 as u16, area.h(),
];
let get_scene_area = |s: usize| [
area.x(), HEADER_H + area.y() + (self.rows[s].1 / PPQ) as u16,
area.w(), (self.rows[s].0 / PPQ) as u16
];
let get_clip_area = |t: usize, s: usize| [
(self.scenes_w + area.x() + self.cols[t].1 as u16).saturating_sub(1),
HEADER_H + area.y() + (self.rows[s].1/PPQ) as u16,
self.cols[t].0 as u16 + 2,
(self.rows[s].0 / PPQ) as u16
];
let mut track_area: Option<[u16;4]> = None;
let mut scene_area: Option<[u16;4]> = None;
let mut clip_area: Option<[u16;4]> = None;
let area = match selected {
ArrangerSelection::Mix => area,
ArrangerSelection::Track(t) => {
track_area = Some(get_track_area(t));
area
},
ArrangerSelection::Scene(s) => {
scene_area = Some(get_scene_area(s));
area
},
ArrangerSelection::Clip(t, s) => {
track_area = Some(get_track_area(t));
scene_area = Some(get_scene_area(s));
clip_area = Some(get_clip_area(t, s));
area
},
};
let bg = self.color.lighter.rgb;//Color::Rgb(0, 255, 0);
if let Some([x, y, width, height]) = track_area {
to.fill_fg([x, y, 1, height], bg);
to.fill_fg([x + width, y, 1, height], bg);
}
if let Some([_, y, _, height]) = scene_area {
to.fill_ul([area.x(), y - 1, area.w(), 1], bg);
to.fill_ul([area.x(), y + height - 1, area.w(), 1], bg);
}
Ok(if focused {
to.render_in(if let Some(clip_area) = clip_area { clip_area }
else if let Some(track_area) = track_area { track_area.clip_h(HEADER_H) }
else if let Some(scene_area) = scene_area { scene_area.clip_w(self.scenes_w) }
else { area.clip_w(self.scenes_w).clip_h(HEADER_H) }, &self.reticle)?
})
}));

View file

@ -0,0 +1,83 @@
use crate::*;
use super::*;
pub struct ArrangerVHead<'a> {
scenes_w: u16,
timebase: &'a Arc<Timebase>,
current: &'a Arc<Moment>,
tracks: &'a [ArrangerTrack],
}
from!(<'a>|state: &'a ArrangerTui|ArrangerVHead<'a> = Self { // A
tracks: &state.tracks,
timebase: state.clock().timebase(),
current: &state.clock().playhead,
scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&state.scenes) as u16,
});
render!(<Tui>|self: ArrangerVHead<'a>|Tui::push_x(self.scenes_w, row!(
(_, track, x1, x2) in ArrangerTrack::with_widths(self.tracks) => {
let (w, h) = (ArrangerTrack::MIN_WIDTH.max(x2 - x1), HEADER_H);
let color = track.color();
fn row <T: Render<Tui>> (color: ItemPalette, field: &T) -> impl Render<Tui> + use<'_, T> {
row!([
Tui::fg(color.light.rgb, ""),
Tui::fg(color.lightest.rgb, field)
])
}
Tui::bg(color.base.rgb, Tui::min_xy(w as u16, h, Fixed::wh(w as u16, 5, col!([
row(color, &Self::format_name(track, w)),
row(color, &Self::format_input(track)?),
row(color, &Self::format_output(track)?),
row(color, &Self::format_elapsed(track, self.timebase)),
row(color, &Self::format_until_next(track, self.current)),
]))))
}
)));
impl ArrangerVHead<'_> {
/// name and width of track
fn format_name (track: &ArrangerTrack, _w: usize) -> impl Render<Tui> {
let name = track.name().read().unwrap().clone();
Tui::bold(true, Tui::fg(track.color.lightest.rgb, name))
}
/// input port
fn format_input (track: &ArrangerTrack) -> Usually<impl Render<Tui>> {
Ok(format!(">{}", track.player.midi_ins().first().map(|port|port.short_name())
.transpose()?.unwrap_or("?".into())))
}
/// output port
fn format_output (track: &ArrangerTrack) -> Usually<impl Render<Tui>> {
Ok(format!("<{}", track.player.midi_outs().first().map(|port|port.short_name())
.transpose()?.unwrap_or("?".into())))
}
/// beats elapsed
fn format_elapsed (track: &ArrangerTrack, timebase: &Arc<Timebase>) -> impl Render<Tui> {
let mut result = String::new();
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();
let elapsed = timebase.format_beats_1_short(
(elapsed as usize % length) as f64
);
result = format!("+{elapsed:>}")
}
result
}
/// beats until switchover
fn format_until_next (track: &ArrangerTrack, current: &Arc<Moment>)
-> Option<impl Render<Tui>>
{
let timebase = &current.timebase;
let mut result = String::new();
if let Some((t, _)) = track.player.next_phrase().as_ref() {
let target = t.pulse.get();
let current = current.pulse.get();
if target > current {
let remaining = target - current;
result = format!("-{:>}", timebase.format_beats_0_short(remaining))
}
}
Some(result)
}
}

View file

@ -0,0 +1,26 @@
use crate::*;
use super::*;
pub struct ArrangerVIns<'a> {
size: &'a Measure<Tui>,
tracks: &'a Vec<ArrangerTrack>,
}
from!(<'a>|args: &'a ArrangerTui|ArrangerVIns<'a> = Self {
size: &args.size,
tracks: &args.tracks,
});
render!(<Tui>|self: ArrangerVIns<'a>|());
pub struct ArrangerVOuts<'a> {
size: &'a Measure<Tui>,
tracks: &'a Vec<ArrangerTrack>,
}
from!(<'a>|args: &'a ArrangerTui|ArrangerVOuts<'a> = Self {
size: &args.size,
tracks: &args.tracks,
});
render!(<Tui>|self: ArrangerVOuts<'a>|());

View file

@ -0,0 +1,48 @@
use crate::*;
use super::*;
pub struct ArrangerVColSep {
fg: Color,
cols: Vec<(usize, usize)>,
scenes_w: u16
}
from!(|state:&ArrangerTui|ArrangerVColSep = Self {
fg: TuiTheme::separator_fg(false),
cols: ArrangerTrack::widths(&state.tracks),
scenes_w: SCENES_W_OFFSET + ArrangerScene::longest_name(&state.scenes) as u16,
});
render!(<Tui>|self: ArrangerVColSep|render(move|to: &mut TuiOutput|{
let style = Some(Style::default().fg(self.fg));
Ok(for x in self.cols.iter().map(|col|col.1) {
let x = self.scenes_w + to.area().x() + x as u16;
for y in to.area().y()..to.area().y2() {
to.blit(&"", x, y, style);
}
})
}));
pub struct ArrangerVRowSep {
fg: Color,
rows: Vec<(usize, usize)>,
}
from!(|args:(&ArrangerTui, usize)|ArrangerVRowSep = Self {
fg: TuiTheme::separator_fg(false),
rows: ArrangerScene::ppqs(&args.0.scenes, args.1),
});
render!(<Tui>|self: ArrangerVRowSep|render(move|to: &mut TuiOutput|{
Ok(for y in self.rows.iter().map(|row|row.1) {
let y = to.area().y() + (y / PPQ) as u16 + 1;
if y >= to.buffer.area.height { break }
for x in to.area().x()..to.area().x2().saturating_sub(2) {
if x < to.buffer.area.x && y < to.buffer.area.y {
let cell = to.buffer.get_mut(x, y);
cell.modifier = Modifier::UNDERLINED;
cell.underline_color = self.fg;
}
}
})
}));

1
src/audio.rs Normal file
View file

@ -0,0 +1 @@
use crate::*;

69
src/core.rs Normal file
View file

@ -0,0 +1,69 @@
pub(crate) use std::error::Error;
pub(crate) mod color;
pub(crate) use color::*;
pub use color::*;
pub(crate) mod command; pub(crate) use command::*;
pub(crate) mod engine; pub(crate) use engine::*;
pub(crate) mod focus; pub(crate) use focus::*;
pub(crate) mod input; pub(crate) use input::*;
pub(crate) mod output; pub(crate) use output::*;
pub(crate) use std::sync::atomic::{Ordering, AtomicBool, AtomicUsize};
pub(crate) use Ordering::Relaxed;
pub use self::{
engine::Engine,
input::Handle,
output::Render
};
/// Standard result type.
pub type Usually<T> = Result<T, Box<dyn Error>>;
/// Standard optional result type.
pub type Perhaps<T> = Result<Option<T>, Box<dyn Error>>;
/// Define test modules.
#[macro_export] macro_rules! testmod {
($($name:ident)*) => { $(#[cfg(test)] mod $name;)* };
}
/// Prototypal case of implementor macro.
/// Saves 4loc per data pats.
#[macro_export] macro_rules! from {
($(<$($lt:lifetime),+>)?|$state:ident:$Source:ty|$Target:ty=$cb:expr) => {
impl $(<$($lt),+>)? From<$Source> for $Target {
fn from ($state:$Source) -> Self { $cb }
}
};
}
pub trait Gettable<T> {
/// Returns current value
fn get (&self) -> T;
}
pub trait Mutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&mut self, value: T) -> T;
}
pub trait InteriorMutable<T>: Gettable<T> {
/// Sets new value, returns old
fn set (&self, value: T) -> T;
}
impl Gettable<bool> for AtomicBool {
fn get (&self) -> bool { self.load(Relaxed) }
}
impl InteriorMutable<bool> for AtomicBool {
fn set (&self, value: bool) -> bool { self.swap(value, Relaxed) }
}
impl Gettable<usize> for AtomicUsize {
fn get (&self) -> usize { self.load(Relaxed) }
}
impl InteriorMutable<usize> for AtomicUsize {
fn set (&self, value: usize) -> usize { self.swap(value, Relaxed) }
}

105
src/core/color.rs Normal file
View file

@ -0,0 +1,105 @@
use crate::*;
use rand::{thread_rng, distributions::uniform::UniformSampler};
pub use ratatui::prelude::Color;
pub trait HasColor {
fn color (&self) -> ItemColor;
}
#[macro_export] macro_rules! has_color {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasColor for $Struct $(<$($L),*$($T),*>)? {
fn color (&$self) -> ItemColor { $cb }
}
}
}
/// A color in OKHSL and RGB representations.
#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub struct ItemColor {
pub okhsl: Okhsl<f32>,
pub rgb: Color,
}
/// A color in OKHSL and RGB with lighter and darker variants.
#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub struct ItemPalette {
pub base: ItemColor,
pub light: ItemColor,
pub lighter: ItemColor,
pub lightest: ItemColor,
pub dark: ItemColor,
pub darker: ItemColor,
pub darkest: ItemColor,
}
from!(|okhsl: Okhsl<f32>|ItemColor = Self { okhsl, rgb: okhsl_to_rgb(okhsl) });
from!(|rgb: Color|ItemColor = Self { rgb, okhsl: rgb_to_okhsl(rgb) });
// A single color within item theme parameters, in OKHSL and RGB representations.
impl ItemColor {
pub fn random () -> Self {
let mut rng = thread_rng();
let lo = Okhsl::new(-180.0, 0.01, 0.25);
let hi = Okhsl::new( 180.0, 0.9, 0.5);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_dark () -> Self {
let mut rng = thread_rng();
let lo = Okhsl::new(-180.0, 0.025, 0.075);
let hi = Okhsl::new( 180.0, 0.5, 0.150);
UniformOkhsl::new(lo, hi).sample(&mut rng).into()
}
pub fn random_near (color: Self, distance: f32) -> Self {
color.mix(Self::random(), distance)
}
pub fn mix (&self, other: Self, distance: f32) -> Self {
if distance > 1.0 { panic!("color mixing takes distance between 0.0 and 1.0"); }
self.okhsl.mix(other.okhsl, distance).into()
}
}
from!(|base: Color|ItemPalette = Self::from(ItemColor::from(base)));
from!(|base: ItemColor|ItemPalette = {
let mut light = base.okhsl;
light.lightness = (light.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
let mut lighter = light;
lighter.lightness = (lighter.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
let mut lightest = lighter;
lightest.lightness = (lightest.lightness * 1.3).min(Okhsl::<f32>::max_lightness());
let mut dark = base.okhsl;
dark.lightness = (dark.lightness * 0.75).max(Okhsl::<f32>::min_lightness());
dark.saturation = (dark.saturation * 0.75).max(Okhsl::<f32>::min_saturation());
let mut darker = dark;
darker.lightness = (darker.lightness * 0.66).max(Okhsl::<f32>::min_lightness());
darker.saturation = (darker.saturation * 0.66).max(Okhsl::<f32>::min_saturation());
let mut darkest = darker;
darkest.lightness = (darkest.lightness * 0.50).max(Okhsl::<f32>::min_lightness());
darkest.saturation = (darkest.saturation * 0.50).max(Okhsl::<f32>::min_saturation());
Self {
base,
light: light.into(),
lighter: lighter.into(),
lightest: lightest.into(),
dark: dark.into(),
darker: darker.into(),
darkest: darkest.into(),
}
});
impl ItemPalette {
pub fn random () -> Self {
ItemColor::random().into()
}
pub fn random_near (color: Self, distance: f32) -> Self {
color.base.mix(ItemColor::random(), distance).into()
}
}
pub fn okhsl_to_rgb (color: Okhsl<f32>) -> Color {
let Srgb { red, green, blue, .. }: Srgb<f32> = Srgb::from_color_unclamped(color);
Color::Rgb((red * 255.0) as u8, (green * 255.0) as u8, (blue * 255.0) as u8,)
}
pub fn rgb_to_okhsl (color: Color) -> Okhsl<f32> {
if let Color::Rgb(r, g, b) = color {
Okhsl::from_color(Srgb::new(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0))
} else {
unreachable!("only Color::Rgb is supported")
}
}

116
src/core/command.rs Normal file
View file

@ -0,0 +1,116 @@
use crate::*;
#[macro_export] macro_rules! command {
(|$self:ident:$Command:ty,$state:ident:$State:ty|$handler:expr) => {
impl Command<$State> for $Command {
fn execute ($self, $state: &mut $State) -> Perhaps<Self> {
Ok($handler)
}
}
}
}
#[macro_export] macro_rules! input_to_command {
($Command:ty: <$Engine:ty>|$state:ident:$State:ty,$input:ident|$handler:expr) => {
impl InputToCommand<$Engine, $State> for $Command {
fn input_to_command ($state: &$State, $input: &<$Engine as Engine>::Input) -> Option<Self> {
Some($handler)
}
}
}
}
#[derive(Clone)]
pub enum NextPrev {
Next,
Prev,
}
pub trait Execute<T> {
fn command (&mut self, command: T) -> Perhaps<T>;
}
pub trait Command<S>: Send + Sync + Sized {
fn execute (self, state: &mut S) -> Perhaps<Self>;
}
pub fn delegate <B, C: Command<S>, S> (
cmd: C,
wrap: impl Fn(C)->B,
state: &mut S,
) -> Perhaps<B> {
Ok(cmd.execute(state)?.map(wrap))
}
pub trait InputToCommand<E: Engine, S>: Command<S> + Sized {
fn input_to_command (state: &S, input: &E::Input) -> Option<Self>;
fn execute_with_state (state: &mut S, input: &E::Input) -> Perhaps<bool> {
Ok(if let Some(command) = Self::input_to_command(state, input) {
let _undo = command.execute(state)?;
Some(true)
} else {
None
})
}
}
pub struct MenuBar<E: Engine, S, C: Command<S>> {
pub menus: Vec<Menu<E, S, C>>,
pub index: usize,
}
impl<E: Engine, S, C: Command<S>> MenuBar<E, S, C> {
pub fn new () -> Self { Self { menus: vec![], index: 0 } }
pub fn add (mut self, menu: Menu<E, S, C>) -> Self {
self.menus.push(menu);
self
}
}
pub struct Menu<E: Engine, S, C: Command<S>> {
pub title: String,
pub items: Vec<MenuItem<E, S, C>>,
pub index: Option<usize>,
}
impl<E: Engine, S, C: Command<S>> Menu<E, S, C> {
pub fn new (title: impl AsRef<str>) -> Self {
Self {
title: title.as_ref().to_string(),
items: vec![],
index: None,
}
}
pub fn add (mut self, item: MenuItem<E, S, C>) -> Self {
self.items.push(item);
self
}
pub fn sep (mut self) -> Self {
self.items.push(MenuItem::sep());
self
}
pub fn cmd (mut self, hotkey: &'static str, text: &'static str, command: C) -> Self {
self.items.push(MenuItem::cmd(hotkey, text, command));
self
}
pub fn off (mut self, hotkey: &'static str, text: &'static str) -> Self {
self.items.push(MenuItem::off(hotkey, text));
self
}
}
pub enum MenuItem<E: Engine, S, C: Command<S>> {
/// Unused.
__(PhantomData<E>, PhantomData<S>),
/// A separator. Skip it.
Separator,
/// A menu item with command, description and hotkey.
Command(&'static str, &'static str, C),
/// A menu item that can't be activated but has description and hotkey
Disabled(&'static str, &'static str)
}
impl<E: Engine, S, C: Command<S>> MenuItem<E, S, C> {
pub fn sep () -> Self {
Self::Separator
}
pub fn cmd (hotkey: &'static str, text: &'static str, command: C) -> Self {
Self::Command(hotkey, text, command)
}
pub fn off (hotkey: &'static str, text: &'static str) -> Self {
Self::Disabled(hotkey, text)
}
}

49
src/core/engine.rs Normal file
View file

@ -0,0 +1,49 @@
use crate::*;
/// Platform backend.
pub trait Engine: Send + Sync + Sized {
/// Input event type
type Input: Input<Self>;
/// Result of handling input
type Handled;
/// Render target
type Output: Output<Self>;
/// Unit of length
type Unit: Coordinate;
/// Rectangle without offset
type Size: Size<Self::Unit> + From<[Self::Unit;2]> + Debug + Copy;
/// Rectangle with offset
type Area: Area<Self::Unit> + From<[Self::Unit;4]> + Debug + Copy;
/// Prepare before run
fn setup (&mut self) -> Usually<()> { Ok(()) }
/// True if done
fn exited (&self) -> bool;
/// Clean up after run
fn teardown (&mut self) -> Usually<()> { Ok(()) }
}
/// A UI component that can render itself as a [Render], and [Handle] input.
pub trait Component<E: Engine>: Render<E> + Handle<E> {}
/// Everything that implements [Render] and [Handle] is a [Component].
impl<E: Engine, C: Render<E> + Handle<E>> Component<E> for C {}
/// A component that can exit.
pub trait Exit: Send {
fn exited (&self) -> bool;
fn exit (&mut self);
fn boxed (self) -> Box<dyn Exit> where Self: Sized + 'static {
Box::new(self)
}
}
/// Marker trait for [Component]s that can [Exit].
pub trait ExitableComponent<E>: Exit + Component<E> where E: Engine {
/// Perform type erasure for collecting heterogeneous components.
fn boxed (self) -> Box<dyn ExitableComponent<E>> where Self: Sized + 'static {
Box::new(self)
}
}
/// All [Components]s that implement [Exit] implement [ExitableComponent].
impl<E: Engine, C: Component<E> + Exit> ExitableComponent<E> for C {}

308
src/core/focus.rs Normal file
View file

@ -0,0 +1,308 @@
use crate::*;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum FocusState<T: Copy + Debug + PartialEq> {
Focused(T),
Entered(T),
}
impl<T: Copy + Debug + PartialEq> FocusState<T> {
pub fn inner (&self) -> T {
match self {
Self::Focused(inner) => *inner,
Self::Entered(inner) => *inner,
}
}
pub fn set_inner (&mut self, inner: T) {
*self = match self {
Self::Focused(_) => Self::Focused(inner),
Self::Entered(_) => Self::Entered(inner),
}
}
pub fn is_focused (&self) -> bool { matches!(self, Self::Focused(_)) }
pub fn is_entered (&self) -> bool { matches!(self, Self::Entered(_)) }
pub fn focus (&mut self) { *self = Self::Focused(self.inner()) }
pub fn enter (&mut self) { *self = Self::Entered(self.inner()) }
}
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum FocusCommand<T: Send + Sync> {
Up,
Down,
Left,
Right,
Next,
Prev,
Enter,
Exit,
Set(T)
}
impl<F: HasFocus + HasEnter + FocusGrid + FocusOrder> Command<F> for FocusCommand<F::Item> {
fn execute (self, state: &mut F) -> Perhaps<FocusCommand<F::Item>> {
use FocusCommand::*;
match self {
Next => { state.focus_next(); },
Prev => { state.focus_prev(); },
Up => { state.focus_up(); },
Down => { state.focus_down(); },
Left => { state.focus_left(); },
Right => { state.focus_right(); },
Enter => { state.focus_enter(); },
Exit => { state.focus_exit(); },
Set(to) => { state.set_focused(to); },
}
Ok(None)
}
}
/// Trait for things that have focusable subparts.
pub trait HasFocus {
type Item: Copy + PartialEq + Debug + Send + Sync;
/// Get the currently focused item.
fn focused (&self) -> Self::Item;
/// Get the currently focused item.
fn set_focused (&mut self, to: Self::Item);
/// Loop forward until a specific item is focused.
fn focus_to (&mut self, to: Self::Item) {
self.set_focused(to);
self.focus_updated();
}
/// Run this on focus update
fn focus_updated (&mut self) {}
}
/// Trait for things that have enterable subparts.
pub trait HasEnter: HasFocus {
/// Get the currently focused item.
fn entered (&self) -> bool;
/// Get the currently focused item.
fn set_entered (&mut self, entered: bool);
/// Enter into the currently focused component
fn focus_enter (&mut self) {
self.set_entered(true);
self.focus_updated();
}
/// Exit the currently entered component
fn focus_exit (&mut self) {
self.set_entered(false);
self.focus_updated();
}
}
/// Trait for things that implement directional navigation between focusable elements.
pub trait FocusGrid: HasFocus {
fn focus_layout (&self) -> &[&[Self::Item]];
fn focus_cursor (&self) -> (usize, usize);
fn focus_cursor_mut (&mut self) -> &mut (usize, usize);
fn focus_current (&self) -> Self::Item {
let (x, y) = self.focus_cursor();
self.focus_layout()[y][x]
}
fn focus_update (&mut self) {
self.focus_to(self.focus_current());
self.focus_updated()
}
fn focus_up (&mut self) {
let original_focused = self.focused();
let (_, original_y) = self.focus_cursor();
loop {
let (x, y) = self.focus_cursor();
let next_y = if y == 0 {
self.focus_layout().len().saturating_sub(1)
} else {
y - 1
};
if next_y == original_y {
break
}
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
x
} else {
((x as f32 / self.focus_layout()[original_y].len() as f32)
* self.focus_layout()[next_y].len() as f32) as usize
};
*self.focus_cursor_mut() = (next_x, next_y);
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
fn focus_down (&mut self) {
let original_focused = self.focused();
let (_, original_y) = self.focus_cursor();
loop {
let (x, y) = self.focus_cursor();
let next_y = if y >= self.focus_layout().len().saturating_sub(1) {
0
} else {
y + 1
};
if next_y == original_y {
break
}
let next_x = if self.focus_layout()[y].len() == self.focus_layout()[next_y].len() {
x
} else {
((x as f32 / self.focus_layout()[original_y].len() as f32)
* self.focus_layout()[next_y].len() as f32) as usize
};
*self.focus_cursor_mut() = (next_x, next_y);
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
fn focus_left (&mut self) {
let original_focused = self.focused();
let (original_x, y) = self.focus_cursor();
loop {
let x = self.focus_cursor().0;
let next_x = if x == 0 {
self.focus_layout()[y].len().saturating_sub(1)
} else {
x - 1
};
if next_x == original_x {
break
}
*self.focus_cursor_mut() = (next_x, y);
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
fn focus_right (&mut self) {
let original_focused = self.focused();
let (original_x, y) = self.focus_cursor();
loop {
let x = self.focus_cursor().0;
let next_x = if x >= self.focus_layout()[y].len().saturating_sub(1) {
0
} else {
x + 1
};
if next_x == original_x {
break
}
self.focus_cursor_mut().0 = next_x;
if self.focus_current() != original_focused {
break
}
}
self.focus_update();
}
}
/// Trait for things that implement next/prev navigation between focusable elements.
pub trait FocusOrder {
/// Focus the next item.
fn focus_next (&mut self);
/// Focus the previous item.
fn focus_prev (&mut self);
}
/// Next/prev navigation for directional focusables works in the given way.
impl<T: FocusGrid + HasEnter> FocusOrder for T {
/// Focus the next item.
fn focus_next (&mut self) {
let current = self.focused();
let (x, y) = self.focus_cursor();
if x < self.focus_layout()[y].len().saturating_sub(1) {
self.focus_right();
} else {
self.focus_down();
self.focus_cursor_mut().0 = 0;
}
if self.focused() == current { // FIXME: prevent infinite loop
self.focus_next()
}
self.focus_exit();
self.focus_update();
}
/// Focus the previous item.
fn focus_prev (&mut self) {
let current = self.focused();
let (x, _) = self.focus_cursor();
if x > 0 {
self.focus_left();
} else {
self.focus_up();
let (_, y) = self.focus_cursor();
let next_x = self.focus_layout()[y].len().saturating_sub(1);
self.focus_cursor_mut().0 = next_x;
}
if self.focused() == current { // FIXME: prevent infinite loop
self.focus_prev()
}
self.focus_exit();
self.focus_update();
}
}
pub trait FocusWrap<T> {
fn wrap <W: Render<Tui>> (self, focus: T, content: &'_ W) -> impl Render<Tui> + '_;
}
pub fn to_focus_command <T: Send + Sync> (input: &TuiInput) -> Option<FocusCommand<T>> {
use KeyCode::{Tab, BackTab, Up, Down, Left, Right, Enter, Esc};
Some(match input.event() {
key_pat!(Tab) => FocusCommand::Next,
key_pat!(Shift-Tab) => FocusCommand::Prev,
key_pat!(BackTab) => FocusCommand::Prev,
key_pat!(Shift-BackTab) => FocusCommand::Prev,
key_pat!(Up) => FocusCommand::Up,
key_pat!(Down) => FocusCommand::Down,
key_pat!(Left) => FocusCommand::Left,
key_pat!(Right) => FocusCommand::Right,
key_pat!(Enter) => FocusCommand::Enter,
key_pat!(Esc) => FocusCommand::Exit,
_ => return None
})
}
#[macro_export] macro_rules! impl_focus {
($Struct:ident $Focus:ident $Grid:expr $(=> [$self:ident : $update_focus:expr])?) => {
impl HasFocus for $Struct {
type Item = $Focus;
/// Get the currently focused item.
fn focused (&self) -> Self::Item {
self.focus.inner()
}
/// Get the currently focused item.
fn set_focused (&mut self, to: Self::Item) {
self.focus.set_inner(to)
}
$(fn focus_updated (&mut $self) { $update_focus })?
}
impl HasEnter for $Struct {
/// Get the currently focused item.
fn entered (&self) -> bool {
self.focus.is_entered()
}
/// Get the currently focused item.
fn set_entered (&mut self, entered: bool) {
if entered {
self.focus.to_entered()
} else {
self.focus.to_focused()
}
}
}
impl FocusGrid for $Struct {
fn focus_cursor (&self) -> (usize, usize) {
self.cursor
}
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
&mut self.cursor
}
fn focus_layout (&self) -> &[&[$Focus]] {
use $Focus::*;
&$Grid
}
}
}
}

68
src/core/input.rs Normal file
View file

@ -0,0 +1,68 @@
use crate::*;
/// Current input state
pub trait Input<E: Engine> {
/// Type of input event
type Event;
/// Currently handled event
fn event (&self) -> &Self::Event;
/// Whether component should exit
fn is_done (&self) -> bool;
/// Mark component as done
fn done (&self);
}
/// Handle input
pub trait Handle<E: Engine>: Send + Sync {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled>;
}
#[macro_export] macro_rules! handle {
(<$E:ty>|$self:ident:$Struct:ty,$input:ident|$handler:expr) => {
impl Handle<$E> for $Struct {
fn handle (&mut $self, $input: &<$E as Engine>::Input) -> Perhaps<<$E as Engine>::Handled> {
$handler
}
}
}
}
impl<E: Engine, H: Handle<E>> Handle<E> for &mut H {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
(*self).handle(context)
}
}
impl<E: Engine, H: Handle<E>> Handle<E> for Option<H> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
if let Some(ref mut handle) = self {
handle.handle(context)
} else {
Ok(None)
}
}
}
impl<H, E: Engine> Handle<E> for Mutex<H> where H: Handle<E> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
self.get_mut().unwrap().handle(context)
}
}
impl<H, E: Engine> Handle<E> for Arc<Mutex<H>> where H: Handle<E> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
self.lock().unwrap().handle(context)
}
}
impl<H, E: Engine> Handle<E> for RwLock<H> where H: Handle<E> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
self.write().unwrap().handle(context)
}
}
impl<H, E: Engine> Handle<E> for Arc<RwLock<H>> where H: Handle<E> {
fn handle (&mut self, context: &E::Input) -> Perhaps<E::Handled> {
self.write().unwrap().handle(context)
}
}

160
src/core/output.rs Normal file
View file

@ -0,0 +1,160 @@
use crate::*;
#[macro_export] macro_rules! render {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*E$(,$T:ident$(:$U:path)?)*$(,)?>)?|$cb:expr) => {
impl<E: Engine, $($($L),*$($T $(: $U)?),*)?> Render<E> for $Struct $(<$($L,)* E, $($T),*>)? {
fn min_size (&$self, to: <E as Engine>::Size) -> Perhaps<<E as Engine>::Size> {
$cb.min_size(to)
}
fn render (&$self, to: &mut <E as Engine>::Output) -> Usually<()> {
$cb.render(to)
}
}
};
(<$E:ty>|$self:ident:$Struct:ident$(<
$($($L:lifetime),+)?
$($($T:ident$(:$U:path)?),+)?
>)?|$cb:expr) => {
impl $(<
$($($L),+)?
$($($T$(:$U)?),+)?
>)? Render<$E> for $Struct $(<
$($($L),+)?
$($($T),+)?
>)? {
fn min_size (&$self, to: <$E as Engine>::Size) -> Perhaps<<$E as Engine>::Size> {
$cb.min_size(to)
}
fn render (&$self, to: &mut <$E as Engine>::Output) -> Usually<()> {
$cb.render(to)
}
}
}
}
/// Rendering target
pub trait Output<E: Engine> {
/// Current output area
fn area (&self) -> E::Area;
/// Mutable pointer to area
fn area_mut (&mut self) -> &mut E::Area;
/// Render widget in area
fn render_in (&mut self, area: E::Area, widget: &dyn Render<E>) -> Usually<()>;
}
/// Cast to dynamic pointer
pub fn widget <E: Engine, T: Render<E>> (w: &T) -> &dyn Render<E> {
w as &dyn Render<E>
}
/// A renderable component
pub trait Render<E: Engine>: Send + Sync {
/// Minimum size to use
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(Some(to))
}
/// Draw to output render target
fn render (&self, to: &mut E::Output) -> Usually<()>;
}
impl<E: Engine, R: Render<E>> Render<E> for &R {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
(*self).min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
(*self).render(to)
}
}
impl<E: Engine> Render<E> for &dyn Render<E> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
(*self).min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
(*self).render(to)
}
}
//impl<E: Engine> Render<E> for &mut dyn Render<E> {
//fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
//(*self).min_size(to)
//}
//fn render (&self, to: &mut E::Output) -> Usually<()> {
//(*self).render(to)
//}
//}
impl<E: Engine> Render<E> for Box<dyn Render<E> + '_> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
(**self).min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
(**self).render(to)
}
}
impl<E: Engine, W: Render<E>> Render<E> for Arc<W> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
self.as_ref().min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
self.as_ref().render(to)
}
}
impl<E: Engine, W: Render<E>> Render<E> for Mutex<W> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
self.lock().unwrap().min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
self.lock().unwrap().render(to)
}
}
impl<E: Engine, W: Render<E>> Render<E> for RwLock<W> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
self.read().unwrap().min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
self.read().unwrap().render(to)
}
}
impl<E: Engine, W: Render<E>> Render<E> for Option<W> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(self.as_ref().map(|widget|widget.min_size(to)).transpose()?.flatten())
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
self.as_ref().map(|widget|widget.render(to)).unwrap_or(Ok(()))
}
}
/// A custom [Render] defined by passing layout and render closures in place.
pub struct Widget<
E: Engine,
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
>(L, R, PhantomData<E>);
impl<
E: Engine,
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
> Widget<E, L, R> {
pub fn new (layout: L, render: R) -> Self {
Self(layout, render, Default::default())
}
}
impl<
E: Engine,
L: Send + Sync + Fn(E::Size)->Perhaps<E::Size>,
R: Send + Sync + Fn(&mut E::Output)->Usually<()>
> Render<E> for Widget<E, L, R> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
self.0(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
self.1(to)
}
}

49
src/core/test.rs Normal file
View file

@ -0,0 +1,49 @@
#[cfg(test)] mod test_focus {
use super::focus::*;
#[test] fn test_focus () {
struct FocusTest {
focused: char,
cursor: (usize, usize)
}
impl HasFocus for FocusTest {
type Item = char;
fn focused (&self) -> Self::Item {
self.focused
}
fn set_focused (&mut self, to: Self::Item) {
self.focused = to
}
}
impl FocusGrid for FocusTest {
fn focus_cursor (&self) -> (usize, usize) {
self.cursor
}
fn focus_cursor_mut (&mut self) -> &mut (usize, usize) {
&mut self.cursor
}
fn focus_layout (&self) -> &[&[Self::Item]] {
&[
&['a', 'a', 'a', 'b', 'b', 'd'],
&['a', 'a', 'a', 'b', 'b', 'd'],
&['a', 'a', 'a', 'c', 'c', 'd'],
&['a', 'a', 'a', 'c', 'c', 'd'],
&['e', 'e', 'e', 'e', 'e', 'e'],
]
}
}
let mut tester = FocusTest { focused: 'a', cursor: (0, 0) };
tester.focus_right();
assert_eq!(tester.cursor.0, 3);
assert_eq!(tester.focused, 'b');
tester.focus_down();
assert_eq!(tester.cursor.1, 2);
assert_eq!(tester.focused, 'c');
}
}

201
src/edn.rs Normal file
View file

@ -0,0 +1,201 @@
use crate::*;
use std::sync::{Arc, RwLock};
use std::collections::BTreeMap;
pub use clojure_reader::edn::Edn;
//pub use clojure_reader::{edn::{read, Edn}, error::Error as EdnError};
/// EDN parsing helper.
#[macro_export] macro_rules! edn {
($edn:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
match $edn { $($pat => $expr),* }
};
($edn:ident in $args:ident { $($pat:pat => $expr:expr),* $(,)? }) => {
for $edn in $args {
edn!($edn { $($pat => $expr),* })
}
};
}
pub trait FromEdn<C>: Sized {
const ID: &'static str;
fn from_edn (context: C, expr: &[Edn<'_>]) -> Usually<Self>;
}
/// Implements the [FromEdn] trait.
#[macro_export] macro_rules! from_edn {
($id:expr => |$context:tt:$Context:ty, $args:ident| -> $T:ty $body:block) => {
impl FromEdn<$Context> for $T {
const ID: &'static str = $id;
fn from_edn <'e> ($context: $Context, $args: &[Edn<'e>]) -> Usually<Self> {
$body
}
}
}
}
from_edn!("sampler" => |jack: &Arc<RwLock<JackClient>>, args| -> crate::Sampler {
let mut name = String::new();
let mut dir = String::new();
let mut samples = BTreeMap::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":dir")) {
dir = String::from(*n);
}
},
Edn::List(args) => match args.first() {
Some(Edn::Symbol("sample")) => {
let (midi, sample) = MidiSample::from_edn((jack, &dir), &args[1..])?;
if let Some(midi) = midi {
samples.insert(midi, sample);
} else {
panic!("sample without midi binding: {}", sample.read().unwrap().name);
}
},
_ => panic!("unexpected in sampler {name}: {args:?}")
},
_ => panic!("unexpected in sampler {name}: {edn:?}")
});
Self::new(jack, &name)
});
type MidiSample = (Option<u7>, Arc<RwLock<crate::Sample>>);
from_edn!("sample" => |(_jack, dir): (&Arc<RwLock<JackClient>>, &str), args| -> MidiSample {
let mut name = String::new();
let mut file = String::new();
let mut midi = None;
let mut start = 0usize;
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(f)) = map.get(&Edn::Key(":file")) {
file = String::from(*f);
}
if let Some(Edn::Int(i)) = map.get(&Edn::Key(":start")) {
start = *i as usize;
}
if let Some(Edn::Int(m)) = map.get(&Edn::Key(":midi")) {
midi = Some(u7::from(*m as u8));
}
},
_ => panic!("unexpected in sample {name}"),
});
let (end, data) = Sample::read_data(&format!("{dir}/{file}"))?;
Ok((midi, Arc::new(RwLock::new(crate::Sample {
name,
start,
end,
channels: data,
rate: None,
gain: 1.0
}))))
});
from_edn!("plugin/lv2" => |jack: &Arc<RwLock<JackClient>>, args| -> Plugin {
let mut name = String::new();
let mut path = String::new();
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(":name")) {
name = String::from(*n);
}
if let Some(Edn::Str(p)) = map.get(&Edn::Key(":path")) {
path = String::from(*p);
}
},
_ => panic!("unexpected in lv2 '{name}'"),
});
Plugin::new_lv2(jack, &name, &path)
});
const SYM_NAME: &str = ":name";
const SYM_GAIN: &str = ":gain";
const SYM_SAMPLER: &str = "sampler";
const SYM_LV2: &str = "lv2";
from_edn!("mixer/track" => |jack: &Arc<RwLock<JackClient>>, args| -> MixerTrack {
let mut _gain = 0.0f64;
let mut track = MixerTrack {
name: String::new(),
audio_ins: vec![],
audio_outs: vec![],
devices: vec![],
};
edn!(edn in args {
Edn::Map(map) => {
if let Some(Edn::Str(n)) = map.get(&Edn::Key(SYM_NAME)) {
track.name = n.to_string();
}
if let Some(Edn::Double(g)) = map.get(&Edn::Key(SYM_GAIN)) {
_gain = f64::from(*g);
}
},
Edn::List(args) => match args.first() {
// Add a sampler device to the track
Some(Edn::Symbol(SYM_SAMPLER)) => {
track.devices.push(
Box::new(Sampler::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
);
panic!(
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"sampler\"",
&track.name,
args.first().unwrap()
)
},
// Add a LV2 plugin to the track.
Some(Edn::Symbol(SYM_LV2)) => {
track.devices.push(
Box::new(Plugin::from_edn(jack, &args[1..])?) as Box<dyn MixerTrackDevice>
);
panic!(
"unsupported in track {}: {:?}; tek_mixer not compiled with feature \"plugin\"",
&track.name,
args.first().unwrap()
)
},
None =>
panic!("empty list track {}", &track.name),
_ =>
panic!("unexpected in track {}: {:?}", &track.name, args.first().unwrap())
},
_ => {}
});
Ok(track)
});
//impl ArrangerScene {
////TODO
////pub fn from_edn <'a, 'e> (args: &[Edn<'e>]) -> Usually<Self> {
////let mut name = None;
////let mut clips = vec![];
////edn!(edn in args {
////Edn::Map(map) => {
////let key = map.get(&Edn::Key(":name"));
////if let Some(Edn::Str(n)) = key {
////name = Some(*n);
////} else {
////panic!("unexpected key in scene '{name:?}': {key:?}")
////}
////},
////Edn::Symbol("_") => {
////clips.push(None);
////},
////Edn::Int(i) => {
////clips.push(Some(*i as usize));
////},
////_ => panic!("unexpected in scene '{name:?}': {edn:?}")
////});
////Ok(ArrangerScene {
////name: Arc::new(name.unwrap_or("").to_string().into()),
////color: ItemColor::random(),
////clips,
////})
////}
//}

268
src/groovebox.rs Normal file
View file

@ -0,0 +1,268 @@
use crate::*;
use super::*;
use KeyCode::{Char, Delete, Tab, Up, Down, Left, Right};
use ClockCommand::{Play, Pause};
use GrooveboxCommand::*;
use PhraseCommand::*;
use PhrasePoolCommand::*;
pub struct GrooveboxTui {
_jack: Arc<RwLock<JackClient>>,
pub player: MidiPlayer,
pub pool: PoolModel,
pub editor: MidiEditorModel,
pub sampler: crate::sampler::Sampler,
pub size: Measure<Tui>,
pub status: bool,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub perf: PerfModel,
}
from_jack!(|jack|GrooveboxTui {
let mut player = MidiPlayer::new(jack, "sequencer")?;
let phrase = Arc::new(RwLock::new(MidiClip::new(
"New", true, 4 * player.clock.timebase.ppq.get() as usize,
None, Some(ItemColor::random().into())
)));
player.play_phrase = Some((Moment::zero(&player.clock.timebase), Some(phrase.clone())));
let pool = crate::pool::PoolModel::from(&phrase);
let editor = crate::midi::MidiEditorModel::from(&phrase);
let sampler = crate::sampler::Sampler::new(jack, "sampler")?;
Self {
_jack: jack.clone(),
player,
pool,
editor,
sampler,
size: Measure::new(),
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
status: true,
}
});
audio!(|self: GrooveboxTui, client, scope|{
let t0 = self.perf.get_t0();
if Control::Quit == ClockAudio(&mut self.player).process(client, scope) {
return Control::Quit
}
if Control::Quit == PlayerAudio(
&mut self.player, &mut self.note_buf, &mut self.midi_buf
).process(client, scope) {
return Control::Quit
}
if Control::Quit == SamplerAudio(&mut self.sampler).process(client, scope) {
return Control::Quit
}
for RawMidi { time, bytes } in self.player.midi_ins[0].iter(scope) {
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
match message {
MidiMessage::NoteOn { ref key, .. } => {
self.editor.set_note_point(key.as_int() as usize);
},
MidiMessage::Controller { controller, value } => {
if let Some(sample) = &self.sampler.mapped[self.editor.note_point()] {
let mut sample = sample.write().unwrap();
let percentage = value.as_int() as f64 / 127.;
match controller.as_int() {
20 => {
sample.start = (percentage * sample.end as f64) as usize;
},
21 => {
let length = sample.channels[0].len();
sample.end = sample.start + (percentage * (length as f64 - sample.start as f64)) as usize;
sample.end = sample.end.min(length);
},
24 => {
sample.gain = percentage as f32 * 2.0;
},
_ => {}
}
}
}
_ => {}
}
}
}
self.perf.update(t0, scope);
Control::Continue
});
has_clock!(|self:GrooveboxTui|&self.player.clock);
render!(<Tui>|self:GrooveboxTui|{
let w = self.size.w();
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
let pool_w = if self.pool.visible { phrase_w } else { 0 };
let sampler_w = 11;
let note_pt = self.editor.note_point();
Fill::wh(lay!([
&self.size,
Fill::wh(Align::s(Fixed::h(2, GrooveboxStatus::from(self)))),
Tui::shrink_y(2, col!([
Fixed::h(2, row!([
Fixed::wh(5, 2, PlayPause(self.clock().is_rolling())),
Fixed::h(2, TransportView::from((self, self.player.play_phrase().as_ref().map(|(_,p)|
p.as_ref().map(|p|p.read().unwrap().color)
).flatten().clone(), true))),
])),
Tui::push_x(sampler_w, Fixed::h(1, row!([
PhraseSelector::play_phrase(&self.player),
PhraseSelector::next_phrase(&self.player),
]))),
row!([
Tui::split_n(false, 9,
col!([
row!(|add|{
if let Some(sample) = &self.sampler.mapped[note_pt] {
add(&format!("Sample {}", sample.read().unwrap().end))?;
}
add(&MidiEditStatus(&self.editor))?;
Ok(())
}),
lay!([
Outer(Style::default().fg(TuiTheme::g(128))),
Fill::w(Fixed::h(8, if let Some((_, sample)) = &self.sampler.recording {
SampleViewer(Some(sample.clone()))
} else if let Some(sample) = &self.sampler.mapped[note_pt] {
SampleViewer(Some(sample.clone()))
} else {
SampleViewer(None)
})),
]),
]),
Tui::split_w(false, pool_w,
Tui::pull_y(1, Fill::h(Align::e(PoolView(&self.pool)))),
Tui::split_e(false, sampler_w, Fill::wh(col!([
Meters(self.sampler.input_meter.as_ref()),
GrooveboxSamples(self),
])), Fill::h(&self.editor))
)
),
]),
]))
]))
});
struct GrooveboxSamples<'a>(&'a GrooveboxTui);
render!(<Tui>|self: GrooveboxSamples<'a>|{
let note_lo = self.0.editor.note_lo().load(Relaxed);
let note_pt = self.0.editor.note_point();
let note_hi = self.0.editor.note_hi();
Fill::wh(col!(note in (note_lo..=note_hi).rev() => {
let mut bg = if note == note_pt { TuiTheme::g(64) } else { Color::Reset };
let mut fg = TuiTheme::g(160);
if let Some((index, _)) = self.0.sampler.recording {
if note == index {
bg = Color::Rgb(64,16,0);
fg = Color::Rgb(224,64,32)
}
} else if self.0.sampler.mapped[note].is_some() {
fg = TuiTheme::g(224);
}
Tui::bg(bg, if let Some(sample) = &self.0.sampler.mapped[note] {
Tui::fg(fg, format!("{note:3} ?????? "))
} else {
Tui::fg(fg, format!("{note:3} (none) "))
})
}))
});
pub enum GrooveboxCommand {
History(isize),
Clock(ClockCommand),
Pool(PoolCommand),
Editor(PhraseCommand),
Enqueue(Option<Arc<RwLock<MidiClip>>>),
Sampler(SamplerCommand),
}
handle!(<Tui>|self: GrooveboxTui, input|GrooveboxCommand::execute_with_state(self, input));
input_to_command!(GrooveboxCommand: <Tui>|state: GrooveboxTui, input|match input.event() {
// TODO: k: toggle on-screen keyboard
key_pat!(Ctrl-Char('k')) => {
todo!("keyboard")
},
// Transport: Play from start or rewind to start
key_pat!(Char(' ')) => Clock(
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
),
// Tab: Toggle visibility of phrase pool column
key_pat!(Tab) => Pool(PoolCommand::Show(!state.pool.visible)),
// q: Enqueue currently edited phrase
key_pat!(Char('q')) => Enqueue(Some(state.pool.phrase().clone())),
// 0: Enqueue phrase 0 (stop all)
key_pat!(Char('0')) => Enqueue(Some(state.pool.phrases()[0].clone())),
key_pat!(Shift-Char('R')) => Sampler(if state.sampler.recording.is_some() {
SamplerCommand::RecordFinish
} else {
SamplerCommand::RecordBegin(u7::from(state.editor.note_point() as u8))
}),
// e: Toggle between editing currently playing or other phrase
key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
let selected = state.pool.phrase().clone();
Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
selected
} else {
playing.clone()
})))
} else {
return None
},
// For the rest, use the default keybindings of the components.
// The ones defined above supersede them.
_ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) {
Editor(command)
} else if let Some(command) = PoolCommand::input_to_command(&state.pool, input) {
Pool(command)
} else {
return None
}
});
command!(|self:GrooveboxCommand,state:GrooveboxTui|match self {
Self::Pool(cmd) => {
let mut default = |cmd: PoolCommand|cmd
.execute(&mut state.pool)
.map(|x|x.map(Pool));
match cmd {
// autoselect: automatically load selected phrase in editor
PoolCommand::Select(_) => {
let undo = default(cmd)?;
state.editor.set_phrase(Some(state.pool.phrase()));
undo
},
// update color in all places simultaneously
PoolCommand::Phrase(SetColor(index, _)) => {
let undo = default(cmd)?;
state.editor.set_phrase(Some(state.pool.phrase()));
undo
},
_ => default(cmd)?
}
},
Self::Editor(cmd) => {
let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor));
match cmd {
_ => default()?
}
},
Self::Clock(cmd) => cmd.execute(state)?.map(Clock),
Self::Sampler(cmd) => cmd.execute(&mut state.sampler)?.map(Sampler),
Self::Enqueue(phrase) => {
state.player.enqueue_next(phrase.as_ref());
None
},
Self::History(delta) => {
todo!("undo/redo")
},
});

377
src/jack.rs Normal file
View file

@ -0,0 +1,377 @@
use crate::*;
pub use ::jack as libjack;
pub use ::jack::{
contrib::ClosureProcessHandler, NotificationHandler,
Client, AsyncClient, ClientOptions, ClientStatus,
ProcessScope, Control, CycleTimes, Frames,
Port, PortId, PortSpec, Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
Transport, TransportState, MidiIter, MidiWriter, RawMidi,
};
pub mod activate;
pub(crate) use self::activate::*;
pub use self::activate::JackActivate;
pub mod client;
pub(crate) use self::client::*;
pub use self::client::JackClient;
pub mod jack_event;
pub(crate) use self::jack_event::*;
pub mod ports;
pub(crate) use self::ports::*;
pub use self::ports::RegisterPort;
/// Implement [TryFrom<&Arc<RwLock<JackClient>>>]: create app state from wrapped JACK handle.
#[macro_export] macro_rules! from_jack {
(|$jack:ident|$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)? $cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? TryFrom<&Arc<RwLock<JackClient>>> for $Struct $(<$($L),*$($T),*>)? {
type Error = Box<dyn std::error::Error>;
fn try_from ($jack: &Arc<RwLock<JackClient>>) -> Usually<Self> {
Ok($cb)
}
}
};
}
/// Implement [Audio]: provide JACK callbacks.
#[macro_export] macro_rules! audio {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
#[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb }
}
}
}
/// Trait for thing that has a JACK process callback.
pub trait Audio: Send + Sync {
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
Control::Continue
}
fn callback (
state: &Arc<RwLock<Self>>, client: &Client, scope: &ProcessScope
) -> Control where Self: Sized {
if let Ok(mut state) = state.write() {
state.process(client, scope)
} else {
Control::Quit
}
}
}
/// Trait for things that wrap a JACK client.
pub trait AudioEngine {
fn transport (&self) -> Transport {
self.client().transport()
}
fn port_by_name (&self, name: &str) -> Option<Port<Unowned>> {
self.client().port_by_name(name)
}
fn register_port <PS: PortSpec> (&self, name: &str, spec: PS) -> Usually<Port<PS>> {
Ok(self.client().register_port(name, spec)?)
}
fn client (&self) -> &Client;
fn activate (
self,
process: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static
) -> Usually<Arc<RwLock<Self>>> where Self: Send + Sync + 'static;
fn thread_init (&self, _: &Client) {}
unsafe fn shutdown (&mut self, _status: ClientStatus, _reason: &str) {}
fn freewheel (&mut self, _: &Client, _enabled: bool) {}
fn client_registration (&mut self, _: &Client, _name: &str, _reg: bool) {}
fn port_registration (&mut self, _: &Client, _id: PortId, _reg: bool) {}
fn ports_connected (&mut self, _: &Client, _a: PortId, _b: PortId, _are: bool) {}
fn sample_rate (&mut self, _: &Client, _frames: Frames) -> Control {
Control::Continue
}
fn port_rename (&mut self, _: &Client, _id: PortId, _old: &str, _new: &str) -> Control {
Control::Continue
}
fn graph_reorder (&mut self, _: &Client) -> Control {
Control::Continue
}
fn xrun (&mut self, _: &Client) -> Control {
Control::Continue
}
}
////////////////////////////////////////////////////////////////////////////////////
///// A [AudioComponent] bound to a JACK client and a set of ports.
//pub struct JackDevice<E: Engine> {
///// The active JACK client of this device.
//pub client: DynamicAsyncClient,
///// The device state, encapsulated for sharing between threads.
//pub state: Arc<RwLock<Box<dyn AudioComponent<E>>>>,
///// Unowned copies of the device's JACK ports, for connecting to the device.
///// The "real" readable/writable `Port`s are owned by the `state`.
//pub ports: UnownedJackPorts,
//}
//impl<E: Engine> std::fmt::Debug for JackDevice<E> {
//fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
//f.debug_struct("JackDevice")
//.field("ports", &self.ports)
//.finish()
//}
//}
//impl<E: Engine> Render for JackDevice<E> {
//type Engine = E;
//fn min_size(&self, to: E::Size) -> Perhaps<E::Size> {
//self.state.read().unwrap().layout(to)
//}
//fn render(&self, to: &mut E::Output) -> Usually<()> {
//self.state.read().unwrap().render(to)
//}
//}
//impl<E: Engine> Handle<E> for JackDevice<E> {
//fn handle(&mut self, from: &E::Input) -> Perhaps<E::Handled> {
//self.state.write().unwrap().handle(from)
//}
//}
//impl<E: Engine> Ports for JackDevice<E> {
//fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
//Ok(self.ports.audio_ins.values().collect())
//}
//fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
//Ok(self.ports.audio_outs.values().collect())
//}
//fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
//Ok(self.ports.midi_ins.values().collect())
//}
//fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
//Ok(self.ports.midi_outs.values().collect())
//}
//}
//impl<E: Engine> JackDevice<E> {
///// Returns a locked mutex of the state's contents.
//pub fn state(&self) -> LockResult<RwLockReadGuard<Box<dyn AudioComponent<E>>>> {
//self.state.read()
//}
///// Returns a locked mutex of the state's contents.
//pub fn state_mut(&self) -> LockResult<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
//self.state.write()
//}
//pub fn connect_midi_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
//Ok(self
//.client
//.as_client()
//.connect_ports(port, self.midi_ins()?[index])?)
//}
//pub fn connect_midi_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
//Ok(self
//.client
//.as_client()
//.connect_ports(self.midi_outs()?[index], port)?)
//}
//pub fn connect_audio_in(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
//Ok(self
//.client
//.as_client()
//.connect_ports(port, self.audio_ins()?[index])?)
//}
//pub fn connect_audio_out(&self, index: usize, port: &Port<Unowned>) -> Usually<()> {
//Ok(self
//.client
//.as_client()
//.connect_ports(self.audio_outs()?[index], port)?)
//}
//}
///// Collection of JACK ports as [AudioIn]/[AudioOut]/[MidiIn]/[MidiOut].
//#[derive(Default, Debug)]
//pub struct JackPorts {
//pub audio_ins: BTreeMap<String, Port<AudioIn>>,
//pub midi_ins: BTreeMap<String, Port<MidiIn>>,
//pub audio_outs: BTreeMap<String, Port<AudioOut>>,
//pub midi_outs: BTreeMap<String, Port<MidiOut>>,
//}
///// Collection of JACK ports as [Unowned].
//#[derive(Default, Debug)]
//pub struct UnownedJackPorts {
//pub audio_ins: BTreeMap<String, Port<Unowned>>,
//pub midi_ins: BTreeMap<String, Port<Unowned>>,
//pub audio_outs: BTreeMap<String, Port<Unowned>>,
//pub midi_outs: BTreeMap<String, Port<Unowned>>,
//}
//impl JackPorts {
//pub fn clone_unowned(&self) -> UnownedJackPorts {
//let mut unowned = UnownedJackPorts::default();
//for (name, port) in self.midi_ins.iter() {
//unowned.midi_ins.insert(name.clone(), port.clone_unowned());
//}
//for (name, port) in self.midi_outs.iter() {
//unowned.midi_outs.insert(name.clone(), port.clone_unowned());
//}
//for (name, port) in self.audio_ins.iter() {
//unowned.audio_ins.insert(name.clone(), port.clone_unowned());
//}
//for (name, port) in self.audio_outs.iter() {
//unowned
//.audio_outs
//.insert(name.clone(), port.clone_unowned());
//}
//unowned
//}
//}
///// Implement the `Ports` trait.
//#[macro_export]
//macro_rules! ports {
//($T:ty $({ $(audio: {
//$(ins: |$ai_arg:ident|$ai_impl:expr,)?
//$(outs: |$ao_arg:ident|$ao_impl:expr,)?
//})? $(midi: {
//$(ins: |$mi_arg:ident|$mi_impl:expr,)?
//$(outs: |$mo_arg:ident|$mo_impl:expr,)?
//})?})?) => {
//impl Ports for $T {$(
//$(
//$(fn audio_ins <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
//let cb = |$ai_arg:&'a Self|$ai_impl;
//cb(self)
//})?
//)?
//$(
//$(fn audio_outs <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
//let cb = (|$ao_arg:&'a Self|$ao_impl);
//cb(self)
//})?
//)?
//)? $(
//$(
//$(fn midi_ins <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
//let cb = (|$mi_arg:&'a Self|$mi_impl);
//cb(self)
//})?
//)?
//$(
//$(fn midi_outs <'a> (&'a self) -> Usually<Vec<&'a Port<Unowned>>> {
//let cb = (|$mo_arg:&'a Self|$mo_impl);
//cb(self)
//})?
//)?
//)?}
//};
//}
///// `JackDevice` factory. Creates JACK `Client`s, performs port registration
///// and activation, and encapsulates a `AudioComponent` into a `JackDevice`.
//pub struct Jack {
//pub client: Client,
//pub midi_ins: Vec<String>,
//pub audio_ins: Vec<String>,
//pub midi_outs: Vec<String>,
//pub audio_outs: Vec<String>,
//}
//impl Jack {
//pub fn new(name: &str) -> Usually<Self> {
//Ok(Self {
//midi_ins: vec![],
//audio_ins: vec![],
//midi_outs: vec![],
//audio_outs: vec![],
//client: Client::new(name, ClientOptions::NO_START_SERVER)?.0,
//})
//}
//pub fn run<'a: 'static, D, E>(
//self,
//state: impl FnOnce(JackPorts) -> Box<D>,
//) -> Usually<JackDevice<E>>
//where
//D: AudioComponent<E> + Sized + 'static,
//E: Engine + 'static,
//{
//let owned_ports = JackPorts {
//audio_ins: register_ports(&self.client, self.audio_ins, AudioIn::default())?,
//audio_outs: register_ports(&self.client, self.audio_outs, AudioOut::default())?,
//midi_ins: register_ports(&self.client, self.midi_ins, MidiIn::default())?,
//midi_outs: register_ports(&self.client, self.midi_outs, MidiOut::default())?,
//};
//let midi_outs = owned_ports
//.midi_outs
//.values()
//.map(|p| Ok(p.name()?))
//.collect::<Usually<Vec<_>>>()?;
//let midi_ins = owned_ports
//.midi_ins
//.values()
//.map(|p| Ok(p.name()?))
//.collect::<Usually<Vec<_>>>()?;
//let audio_outs = owned_ports
//.audio_outs
//.values()
//.map(|p| Ok(p.name()?))
//.collect::<Usually<Vec<_>>>()?;
//let audio_ins = owned_ports
//.audio_ins
//.values()
//.map(|p| Ok(p.name()?))
//.collect::<Usually<Vec<_>>>()?;
//let state = Arc::new(RwLock::new(state(owned_ports) as Box<dyn AudioComponent<E>>));
//let client = self.client.activate_async(
//Notifications(Box::new({
//let _state = state.clone();
//move |_event| {
//// FIXME: this deadlocks
////state.lock().unwrap().handle(&event).unwrap();
//}
//}) as Box<dyn Fn(JackEvent) + Send + Sync>),
//ClosureProcessHandler::new(Box::new({
//let state = state.clone();
//move |c: &Client, s: &ProcessScope| state.write().unwrap().process(c, s)
//}) as BoxedAudioHandler),
//)?;
//Ok(JackDevice {
//ports: UnownedJackPorts {
//audio_ins: query_ports(&client.as_client(), audio_ins),
//audio_outs: query_ports(&client.as_client(), audio_outs),
//midi_ins: query_ports(&client.as_client(), midi_ins),
//midi_outs: query_ports(&client.as_client(), midi_outs),
//},
//client,
//state,
//})
//}
//pub fn audio_in(mut self, name: &str) -> Self {
//self.audio_ins.push(name.to_string());
//self
//}
//pub fn audio_out(mut self, name: &str) -> Self {
//self.audio_outs.push(name.to_string());
//self
//}
//pub fn midi_in(mut self, name: &str) -> Self {
//self.midi_ins.push(name.to_string());
//self
//}
//pub fn midi_out(mut self, name: &str) -> Self {
//self.midi_outs.push(name.to_string());
//self
//}
//}

50
src/jack/activate.rs Normal file
View file

@ -0,0 +1,50 @@
use crate::*;
pub trait JackActivate: Sized {
fn activate_with <T: Audio + 'static> (
self,
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
)
-> Usually<Arc<RwLock<T>>>;
}
impl JackActivate for JackClient {
fn activate_with <T: Audio + 'static> (
self,
init: impl FnOnce(&Arc<RwLock<JackClient>>)->Usually<T>
)
-> Usually<Arc<RwLock<T>>>
{
let client = Arc::new(RwLock::new(self));
let target = Arc::new(RwLock::new(init(&client)?));
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
let events = Notifications(event);
let frame = Box::new({
let target = target.clone();
move|c: &_, s: &_|if let Ok(mut target) = target.write() {
target.process(c, s)
} else {
Control::Quit
}
});
let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler);
let mut buffer = Self::Activating;
std::mem::swap(&mut*client.write().unwrap(), &mut buffer);
*client.write().unwrap() = Self::Active(Client::from(buffer).activate_async(events, frames)?);
Ok(target)
}
}
/// A UI component that may be associated with a JACK client by the `Jack` factory.
pub trait AudioComponent<E: Engine>: Component<E> + Audio {
/// Perform type erasure for collecting heterogeneous devices.
fn boxed(self) -> Box<dyn AudioComponent<E>>
where
Self: Sized + 'static,
{
Box::new(self)
}
}
/// All things that implement the required traits can be treated as `AudioComponent`.
impl<E: Engine, W: Component<E> + Audio> AudioComponent<E> for W {}

50
src/jack/client.rs Normal file
View file

@ -0,0 +1,50 @@
use crate::*;
pub type DynamicAsyncClient = AsyncClient<DynamicNotifications, DynamicAudioHandler>;
pub type DynamicAudioHandler = ClosureProcessHandler<(), BoxedAudioHandler>;
pub type BoxedAudioHandler = Box<dyn FnMut(&Client, &ProcessScope) -> Control + Send>;
/// Wraps [Client] or [DynamicAsyncClient] in place.
#[derive(Debug)]
pub enum JackClient {
/// Before activation.
Inactive(Client),
/// During activation.
Activating,
/// After activation. Must not be dropped for JACK thread to persist.
Active(DynamicAsyncClient),
}
from!(|jack: JackClient|Client = match jack {
JackClient::Inactive(client) => client,
JackClient::Activating => panic!("jack client still activating"),
JackClient::Active(_) => panic!("jack client already activated"),
});
impl JackClient {
pub fn new (name: &str) -> Usually<Self> {
let (client, _) = Client::new(name, ClientOptions::NO_START_SERVER)?;
Ok(Self::Inactive(client))
}
}
impl AudioEngine for JackClient {
fn client(&self) -> &Client {
match self {
Self::Inactive(ref client) => client,
Self::Activating => panic!("jack client has not finished activation"),
Self::Active(ref client) => client.as_client(),
}
}
fn activate(
self,
mut cb: impl FnMut(&Arc<RwLock<Self>>, &Client, &ProcessScope) -> Control + Send + 'static,
) -> Usually<Arc<RwLock<Self>>>
where
Self: Send + Sync + 'static
{
let client = Client::from(self);
let state = Arc::new(RwLock::new(Self::Activating));
let event = Box::new(move|_|{/*TODO*/}) as Box<dyn Fn(JackEvent) + Send + Sync>;
let events = Notifications(event);
let frame = Box::new({let state = state.clone(); move|c: &_, s: &_|cb(&state, c, s)});
let frames = ClosureProcessHandler::new(frame as BoxedAudioHandler);
*state.write().unwrap() = Self::Active(client.activate_async(events, frames)?);
Ok(state)
}
}

69
src/jack/jack_event.rs Normal file
View file

@ -0,0 +1,69 @@
use crate::*;
#[derive(Debug, Clone, PartialEq)]
/// Event enum for JACK events.
pub enum JackEvent {
ThreadInit,
Shutdown(ClientStatus, String),
Freewheel(bool),
SampleRate(Frames),
ClientRegistration(String, bool),
PortRegistration(PortId, bool),
PortRename(PortId, String, String),
PortsConnected(PortId, PortId, bool),
GraphReorder,
XRun,
}
/// Notification handler used by the [Jack] factory
/// when constructing [JackDevice]s.
pub type DynamicNotifications = Notifications<Box<dyn Fn(JackEvent) + Send + Sync>>;
/// Generic notification handler that emits [JackEvent]
pub struct Notifications<T: Fn(JackEvent) + Send>(pub T);
impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
fn thread_init(&self, _: &Client) {
self.0(JackEvent::ThreadInit);
}
unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) {
self.0(JackEvent::Shutdown(status, reason.into()));
}
fn freewheel(&mut self, _: &Client, enabled: bool) {
self.0(JackEvent::Freewheel(enabled));
}
fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control {
self.0(JackEvent::SampleRate(frames));
Control::Quit
}
fn client_registration(&mut self, _: &Client, name: &str, reg: bool) {
self.0(JackEvent::ClientRegistration(name.into(), reg));
}
fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) {
self.0(JackEvent::PortRegistration(id, reg));
}
fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
self.0(JackEvent::PortRename(id, old.into(), new.into()));
Control::Continue
}
fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
self.0(JackEvent::PortsConnected(a, b, are));
}
fn graph_reorder(&mut self, _: &Client) -> Control {
self.0(JackEvent::GraphReorder);
Control::Continue
}
fn xrun(&mut self, _: &Client) -> Control {
self.0(JackEvent::XRun);
Control::Continue
}
}

109
src/jack/ports.rs Normal file
View file

@ -0,0 +1,109 @@
use crate::*;
pub trait RegisterPort {
fn midi_in (&self, name: &str) -> Usually<Port<MidiIn>>;
fn midi_out (&self, name: &str) -> Usually<Port<MidiOut>>;
fn audio_in (&self, name: &str) -> Usually<Port<AudioIn>>;
fn audio_out (&self, name: &str) -> Usually<Port<AudioOut>>;
fn connect_midi_from (&self, my_input: &Port<MidiIn>, ports: &[String]) -> Usually<()>;
fn connect_midi_to (&self, my_output: &Port<MidiOut>, ports: &[String]) -> Usually<()>;
fn connect_audio_from (&self, my_input: &Port<AudioIn>, ports: &[String]) -> Usually<()>;
fn connect_audio_to (&self, my_output: &Port<AudioOut>, ports: &[String]) -> Usually<()>;
}
impl RegisterPort for Arc<RwLock<JackClient>> {
fn midi_in (&self, name: &str) -> Usually<Port<MidiIn>> {
Ok(self.read().unwrap().client().register_port(name, MidiIn::default())?)
}
fn midi_out (&self, name: &str) -> Usually<Port<MidiOut>> {
Ok(self.read().unwrap().client().register_port(name, MidiOut::default())?)
}
fn audio_out (&self, name: &str) -> Usually<Port<AudioOut>> {
Ok(self.read().unwrap().client().register_port(name, AudioOut::default())?)
}
fn audio_in (&self, name: &str) -> Usually<Port<AudioIn>> {
Ok(self.read().unwrap().client().register_port(name, AudioIn::default())?)
}
fn connect_midi_from (&self, input: &Port<MidiIn>, ports: &[String]) -> Usually<()> {
let jack = self.read().unwrap();
for port in ports.iter() {
if let Some(port) = jack.port_by_name(port).as_ref() {
jack.client().connect_ports(port, input)?;
} else {
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
}
}
Ok(())
}
fn connect_midi_to (&self, output: &Port<MidiOut>, ports: &[String]) -> Usually<()> {
let jack = self.read().unwrap();
for port in ports.iter() {
if let Some(port) = jack.port_by_name(port).as_ref() {
jack.client().connect_ports(output, port)?;
} else {
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
}
}
Ok(())
}
fn connect_audio_from (&self, input: &Port<AudioIn>, ports: &[String]) -> Usually<()> {
let jack = self.read().unwrap();
for port in ports.iter() {
if let Some(port) = jack.port_by_name(port).as_ref() {
jack.client().connect_ports(port, input)?;
} else {
panic!("Missing MIDI output: {port}. Use jack_lsp to list all port names.");
}
}
Ok(())
}
fn connect_audio_to (&self, output: &Port<AudioOut>, ports: &[String]) -> Usually<()> {
let jack = self.read().unwrap();
for port in ports.iter() {
if let Some(port) = jack.port_by_name(port).as_ref() {
jack.client().connect_ports(output, port)?;
} else {
panic!("Missing MIDI input: {port}. Use jack_lsp to list all port names.");
}
}
Ok(())
}
}
/// Trait for things that may expose JACK ports.
pub trait Ports {
fn audio_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(vec![])
}
fn audio_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(vec![])
}
fn midi_ins(&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(vec![])
}
fn midi_outs(&self) -> Usually<Vec<&Port<Unowned>>> {
Ok(vec![])
}
}
fn register_ports<T: PortSpec + Copy>(
client: &Client,
names: Vec<String>,
spec: T,
) -> Usually<BTreeMap<String, Port<T>>> {
names
.into_iter()
.try_fold(BTreeMap::new(), |mut ports, name| {
let port = client.register_port(&name, spec)?;
ports.insert(name, port);
Ok(ports)
})
}
fn query_ports(client: &Client, names: Vec<String>) -> BTreeMap<String, Port<Unowned>> {
names.into_iter().fold(BTreeMap::new(), |mut ports, name| {
let port = client.port_by_name(&name).unwrap();
ports.insert(name, port);
ports
})
}

90
src/lib.rs Normal file
View file

@ -0,0 +1,90 @@
#![allow(unused)]
#![allow(clippy::unit_arg)]
pub mod core; pub use self::core::*;
pub mod time; pub(crate) use self::time::*;
pub mod space; pub(crate) use self::space::*;
pub mod tui; pub(crate) use self::tui::*;
pub use tui::*;
pub mod jack; pub(crate) use self::jack::*;
pub use self::jack::*;
pub mod midi; pub(crate) use self::midi::*;
pub mod meter; pub(crate) use self::meter::*;
pub mod piano_h; pub(crate) use self::piano_h::*;
pub mod transport; pub(crate) use self::transport::*;
pub use self::transport::TransportTui;
pub mod sequencer; pub(crate) use self::sequencer::*;
pub use self::sequencer::SequencerTui;
pub mod arranger; pub(crate) use self::arranger::*;
pub use self::arranger::ArrangerTui;
pub mod sampler; pub(crate) use self::sampler::*;
pub use self::sampler::{SamplerTui, Sampler, Sample, Voice};
pub mod mixer; pub(crate) use self::mixer::*;
pub use self::mixer::{Mixer, MixerTrack, MixerTrackDevice};
pub mod plugin; pub(crate) use self::plugin::*;
pub use self::plugin::*;
pub mod groovebox; pub(crate) use self::groovebox::*;
pub use self::groovebox::GrooveboxTui;
pub mod pool; pub(crate) use self::pool::*;
pub use self::pool::PoolModel;
pub mod status; pub(crate) use self::status::*;
pub use ::better_panic; pub(crate) use better_panic::{Settings, Verbosity};
pub use ::atomic_float; pub(crate) use atomic_float::*;
pub(crate) use std::sync::{Arc, Mutex, RwLock};
pub(crate) use std::sync::atomic::Ordering;
pub(crate) use std::sync::atomic::{AtomicBool, AtomicUsize};
pub(crate) use std::collections::BTreeMap;
pub(crate) use std::marker::PhantomData;
pub(crate) use std::thread::{spawn, JoinHandle};
pub(crate) use std::path::PathBuf;
pub(crate) use std::ffi::OsString;
pub(crate) use std::time::Duration;
pub(crate) use std::io::{Stdout, stdout};
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
pub(crate) use std::cmp::{Ord, Eq, PartialEq};
pub(crate) use std::fmt::{Debug, Display, Formatter};
pub use ::crossterm;
pub(crate) use crossterm::{ExecutableCommand};
pub(crate) use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen, enable_raw_mode, disable_raw_mode};
pub(crate) use crossterm::event::{KeyCode, KeyModifiers, KeyEvent, KeyEventKind, KeyEventState};
pub use ::ratatui; pub(crate) use ratatui::{
prelude::{Style, Buffer},
style::{Stylize, Modifier},
backend::{Backend, CrosstermBackend, ClearType}
};
pub use ::midly::{self, num::u7}; pub(crate) use ::midly::{
Smf,
MidiMessage,
TrackEventKind,
live::LiveEvent,
};
pub use ::palette; pub(crate) use ::palette::{
*,
convert::*,
okhsl::*
};
testmod! { test }

8
src/meter.rs Normal file
View file

@ -0,0 +1,8 @@
use crate::*;
pub struct Meters<'a>(pub &'a[f32]);
render!(<Tui>|self: Meters<'a>|col!([
&format!("L/{:>+9.3}", self.0[0]),
&format!("R/{:>+9.3}", self.0[1]),
]));

352
src/midi.rs Normal file
View file

@ -0,0 +1,352 @@
use crate::*;
pub(crate) mod midi_pool; pub(crate) use midi_pool::*;
pub(crate) mod midi_clip; pub(crate) use midi_clip::*;
pub(crate) mod midi_launch; pub(crate) use midi_launch::*;
pub(crate) mod midi_play; pub(crate) use midi_play::*;
pub(crate) mod midi_rec; pub(crate) use midi_rec::*;
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::*;
/// Trait for thing that may receive MIDI.
pub trait HasMidiIns {
fn midi_ins (&self) -> &Vec<Port<MidiIn>>;
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>>;
fn has_midi_ins (&self) -> bool {
!self.midi_ins().is_empty()
}
}
/// Trait for thing that may output MIDI.
pub trait HasMidiOuts {
fn midi_outs (&self) -> &Vec<Port<MidiOut>>;
fn midi_outs_mut (&mut self) -> &mut Vec<Port<MidiOut>>;
fn has_midi_outs (&self) -> bool {
!self.midi_outs().is_empty()
}
/// Buffer for serializing a MIDI event. FIXME rename
fn midi_note (&mut self) -> &mut Vec<u8>;
}
/// Add "all notes off" to the start of a buffer.
pub fn all_notes_off (output: &mut [Vec<Vec<u8>>]) {
let mut buf = vec![];
let msg = MidiMessage::Controller { controller: 123.into(), value: 0.into() };
let evt = LiveEvent::Midi { channel: 0.into(), message: msg };
evt.write(&mut buf).unwrap();
output[0].push(buf);
}
/// Return boxed iterator of MIDI events
pub fn parse_midi_input (input: MidiIter) -> Box<dyn Iterator<Item=(usize, LiveEvent, &[u8])> + '_> {
Box::new(input.map(|RawMidi { time, bytes }|(
time as usize,
LiveEvent::parse(bytes).unwrap(),
bytes
)))
}
/// Update notes_in array
pub fn update_keys (keys: &mut[bool;128], message: &MidiMessage) {
match message {
MidiMessage::NoteOn { key, .. } => { keys[key.as_int() as usize] = true; }
MidiMessage::NoteOff { key, .. } => { keys[key.as_int() as usize] = false; },
_ => {}
}
}
pub fn to_note_name (n: usize) -> &'static str {
if n > 127 {
panic!("to_note_name({n}): must be 0-127");
}
MIDI_NOTE_NAMES[n]
}
pub const MIDI_NOTE_NAMES: [&str; 128] = [
"C0", "C#0", "D0", "D#0", "E0", "F0", "F#0", "G0", "G#0", "A0", "A#0", "B0",
"C1", "C#1", "D1", "D#1", "E1", "F1", "F#1", "G1", "G#1", "A1", "A#1", "B1",
"C2", "C#2", "D2", "D#2", "E2", "F2", "F#2", "G2", "G#2", "A2", "A#2", "B2",
"C3", "C#3", "D3", "D#3", "E3", "F3", "F#3", "G3", "G#3", "A3", "A#3", "B3",
"C4", "C#4", "D4", "D#4", "E4", "F4", "F#4", "G4", "G#4", "A4", "A#4", "B4",
"C5", "C#5", "D5", "D#5", "E5", "F5", "F#5", "G5", "G#5", "A5", "A#5", "B5",
"C6", "C#6", "D6", "D#6", "E6", "F6", "F#6", "G6", "G#6", "A6", "A#6", "B6",
"C7", "C#7", "D7", "D#7", "E7", "F7", "F#7", "G7", "G#7", "A7", "A#7", "B7",
"C8", "C#8", "D8", "D#8", "E8", "F8", "F#8", "G8", "G#8", "A8", "A#8", "B8",
"C9", "C#9", "D9", "D#9", "E9", "F9", "F#9", "G9", "G#9", "A9", "A#9", "B9",
"C10", "C#10", "D10", "D#10", "E10", "F10", "F#10", "G10",
];
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
impl MidiPlayerApi for MidiPlayer {}
pub trait HasPlayer {
fn player (&self) -> &impl MidiPlayerApi;
fn player_mut (&mut self) -> &mut impl MidiPlayerApi;
}
#[macro_export] macro_rules! has_player {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasPlayer for $Struct $(<$($L),*$($T),*>)? {
fn player (&$self) -> &impl MidiPlayerApi { &$cb }
fn player_mut (&mut $self) -> &mut impl MidiPlayerApi { &mut$cb }
}
}
}
/// Contains state for playing a phrase
pub struct MidiPlayer {
/// State of clock and playhead
pub(crate) clock: ClockModel,
/// Start time and phrase being played
pub(crate) play_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Start time and next phrase
pub(crate) next_phrase: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
/// Play input through output.
pub(crate) monitoring: bool,
/// Write input to sequence.
pub(crate) recording: bool,
/// Overdub input to sequence.
pub(crate) overdub: bool,
/// Send all notes off
pub(crate) reset: bool, // TODO?: after Some(nframes)
/// Record from MIDI ports to current sequence.
pub midi_ins: Vec<Port<MidiIn>>,
/// Play from current sequence to MIDI ports
pub midi_outs: Vec<Port<MidiOut>>,
/// Notes currently held at input
pub(crate) notes_in: Arc<RwLock<[bool; 128]>>,
/// Notes currently held at output
pub(crate) notes_out: Arc<RwLock<[bool; 128]>>,
/// MIDI output buffer
pub note_buf: Vec<u8>,
}
impl MidiPlayer {
pub fn new (jack: &Arc<RwLock<JackClient>>, name: &str) -> Usually<Self> {
Ok(Self {
clock: ClockModel::from(jack),
play_phrase: None,
next_phrase: None,
recording: false,
monitoring: false,
overdub: false,
notes_in: RwLock::new([false;128]).into(),
midi_ins: vec![
jack.midi_in(&format!("M/{name}"))?,
],
midi_outs: vec![
jack.midi_out(&format!("{name}/M"))?,
],
notes_out: RwLock::new([false;128]).into(),
reset: true,
note_buf: vec![0;8],
})
}
}
impl std::fmt::Debug for MidiPlayer {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiPlayer")
.field("clock", &self.clock)
.field("play_phrase", &self.play_phrase)
.field("next_phrase", &self.next_phrase)
.finish()
}
}
from!(|clock: &ClockModel| MidiPlayer = Self {
clock: clock.clone(),
midi_ins: vec![],
midi_outs: vec![],
note_buf: vec![0;8],
reset: true,
recording: false,
monitoring: false,
overdub: false,
play_phrase: None,
next_phrase: None,
notes_in: RwLock::new([false;128]).into(),
notes_out: RwLock::new([false;128]).into(),
});
from!(|state: (&ClockModel, &Arc<RwLock<MidiClip>>)|MidiPlayer = {
let (clock, phrase) = state;
let mut model = Self::from(clock);
model.play_phrase = Some((Moment::zero(&clock.timebase), Some(phrase.clone())));
model
});
has_clock!(|self: MidiPlayer|&self.clock);
impl HasMidiIns for MidiPlayer {
fn midi_ins (&self) -> &Vec<Port<MidiIn>> {
&self.midi_ins
}
fn midi_ins_mut (&mut self) -> &mut Vec<Port<MidiIn>> {
&mut self.midi_ins
}
}
impl HasMidiOuts for MidiPlayer {
fn midi_outs (&self) -> &Vec<Port<MidiOut>> {
&self.midi_outs
}
fn midi_outs_mut (&mut self) -> &mut Vec<Port<MidiOut>> {
&mut self.midi_outs
}
fn midi_note (&mut self) -> &mut Vec<u8> {
&mut self.note_buf
}
}
/// Hosts the JACK callback for a single MIDI player
pub struct PlayerAudio<'a, T: MidiPlayerApi>(
/// Player
pub &'a mut T,
/// Note buffer
pub &'a mut Vec<u8>,
/// Note chunk buffer
pub &'a mut Vec<Vec<Vec<u8>>>,
);
/// JACK process callback for a sequencer's phrase player/recorder.
impl<T: MidiPlayerApi> Audio for PlayerAudio<'_, T> {
fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
let model = &mut self.0;
let note_buf = &mut self.1;
let midi_buf = &mut self.2;
// Clear output buffer(s)
model.clear(scope, midi_buf, false);
// Write chunk of phrase to output, handle switchover
if model.play(scope, note_buf, midi_buf) {
model.switchover(scope, note_buf, midi_buf);
}
if model.has_midi_ins() {
if model.recording() || model.monitoring() {
// Record and/or monitor input
model.record(scope, midi_buf)
} else if model.has_midi_outs() && model.monitoring() {
// Monitor input to output
model.monitor(scope, midi_buf)
}
}
// Write to output port(s)
model.write(scope, midi_buf);
Control::Continue
}
}
impl MidiRecordApi for MidiPlayer {
fn recording (&self) -> bool {
self.recording
}
fn recording_mut (&mut self) -> &mut bool {
&mut self.recording
}
fn monitoring (&self) -> bool {
self.monitoring
}
fn monitoring_mut (&mut self) -> &mut bool {
&mut self.monitoring
}
fn overdub (&self) -> bool {
self.overdub
}
fn overdub_mut (&mut self) -> &mut bool {
&mut self.overdub
}
fn notes_in (&self) -> &Arc<RwLock<[bool; 128]>> {
&self.notes_in
}
}
impl MidiPlaybackApi for MidiPlayer {
fn notes_out (&self) -> &Arc<RwLock<[bool; 128]>> {
&self.notes_out
}
}
impl HasPlayPhrase for MidiPlayer {
fn reset (&self) -> bool {
self.reset
}
fn reset_mut (&mut self) -> &mut bool {
&mut self.reset
}
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.play_phrase
}
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.play_phrase
}
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&self.next_phrase
}
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
&mut self.next_phrase
}
}
//#[derive(Debug)]
//pub struct MIDIPlayer {
///// Global timebase
//pub clock: Arc<Clock>,
///// Start time and phrase being played
//pub play_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
///// Start time and next phrase
//pub next_phrase: Option<(Moment, Option<Arc<RwLock<Phrase>>>)>,
///// Play input through output.
//pub monitoring: bool,
///// Write input to sequence.
//pub recording: bool,
///// Overdub input to sequence.
//pub overdub: bool,
///// Send all notes off
//pub reset: bool, // TODO?: after Some(nframes)
///// Record from MIDI ports to current sequence.
//pub midi_inputs: Vec<Port<MidiIn>>,
///// Play from current sequence to MIDI ports
//pub midi_outputs: Vec<Port<MidiOut>>,
///// MIDI output buffer
//pub midi_note: Vec<u8>,
///// MIDI output buffer
//pub midi_chunk: Vec<Vec<Vec<u8>>>,
///// Notes currently held at input
//pub notes_in: Arc<RwLock<[bool; 128]>>,
///// Notes currently held at output
//pub notes_out: Arc<RwLock<[bool; 128]>>,
//}
///// Methods used primarily by the process callback
//impl MIDIPlayer {
//pub fn new (
//jack: &Arc<RwLock<JackClient>>,
//clock: &Arc<Clock>,
//name: &str
//) -> Usually<Self> {
//let jack = jack.read().unwrap();
//Ok(Self {
//clock: clock.clone(),
//phrase: None,
//next_phrase: None,
//notes_in: Arc::new(RwLock::new([false;128])),
//notes_out: Arc::new(RwLock::new([false;128])),
//monitoring: false,
//recording: false,
//overdub: true,
//reset: true,
//midi_note: Vec::with_capacity(8),
//midi_chunk: vec![Vec::with_capacity(16);16384],
//midi_outputs: vec![
//jack.client().register_port(format!("{name}_out0").as_str(), MidiOut::default())?
//],
//midi_inputs: vec![
//jack.client().register_port(format!("{name}_in0").as_str(), MidiIn::default())?
//],
//})
//}
//}

109
src/midi/midi_clip.rs Normal file
View file

@ -0,0 +1,109 @@
use crate::*;
pub trait HasMidiClip {
fn phrase (&self) -> &Arc<RwLock<MidiClip>>;
}
#[macro_export] macro_rules! has_phrase {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasMidiClip for $Struct $(<$($L),*$($T),*>)? {
fn phrase (&$self) -> &Arc<RwLock<MidiClip>> { &$cb }
}
}
}
/// A MIDI sequence.
#[derive(Debug, Clone)]
pub struct MidiClip {
pub uuid: uuid::Uuid,
/// Name of phrase
pub name: String,
/// Temporal resolution in pulses per quarter note
pub ppq: usize,
/// Length of phrase in pulses
pub length: usize,
/// Notes in phrase
pub notes: MidiData,
/// Whether to loop the phrase or play it once
pub looped: bool,
/// Start of loop
pub loop_start: usize,
/// Length of loop
pub loop_length: usize,
/// All notes are displayed with minimum length
pub percussive: bool,
/// Identifying color of phrase
pub color: ItemPalette,
}
/// MIDI message structural
pub type MidiData = Vec<Vec<MidiMessage>>;
impl MidiClip {
pub fn new (
name: impl AsRef<str>,
looped: bool,
length: usize,
notes: Option<MidiData>,
color: Option<ItemPalette>,
) -> Self {
Self {
uuid: uuid::Uuid::new_v4(),
name: name.as_ref().to_string(),
ppq: PPQ,
length,
notes: notes.unwrap_or(vec![Vec::with_capacity(16);length]),
looped,
loop_start: 0,
loop_length: length,
percussive: true,
color: color.unwrap_or_else(ItemPalette::random)
}
}
pub fn set_length (&mut self, length: usize) {
self.length = length;
self.notes = vec![Vec::with_capacity(16);length];
}
pub fn duplicate (&self) -> Self {
let mut clone = self.clone();
clone.uuid = uuid::Uuid::new_v4();
clone
}
pub fn toggle_loop (&mut self) { self.looped = !self.looped; }
pub fn record_event (&mut self, pulse: usize, message: MidiMessage) {
if pulse >= self.length { panic!("extend phrase first") }
self.notes[pulse].push(message);
}
/// Check if a range `start..end` contains MIDI Note On `k`
pub fn contains_note_on (&self, k: u7, start: usize, end: usize) -> bool {
for events in self.notes[start.max(0)..end.min(self.notes.len())].iter() {
for event in events.iter() {
if let MidiMessage::NoteOn {key,..} = event { if *key == k { return true } }
}
}
false
}
}
impl Default for MidiClip {
fn default () -> Self {
Self::new(
"Stop",
false,
1,
Some(vec![vec![MidiMessage::Controller {
controller: 123.into(),
value: 0.into()
}]]),
Some(ItemColor::from(Color::Rgb(32, 32, 32)).into())
)
}
}
impl PartialEq for MidiClip {
fn eq (&self, other: &Self) -> bool {
self.uuid == other.uuid
}
}
impl Eq for MidiClip {}

225
src/midi/midi_editor.rs Normal file
View file

@ -0,0 +1,225 @@
use crate::*;
use KeyCode::{Char, Up, Down, Left, Right, Enter};
use PhraseCommand::*;
pub trait HasEditor {
fn editor (&self) -> &MidiEditorModel;
}
#[macro_export] macro_rules! has_editor {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasEditor for $Struct $(<$($L),*$($T),*>)? {
fn editor (&$self) -> &MidiEditorModel { &$cb }
}
}
}
#[derive(Clone, Debug)]
pub enum PhraseCommand {
// TODO: 1-9 seek markers that by default start every 8th of the phrase
AppendNote,
PutNote,
SetNoteCursor(usize),
SetNoteLength(usize),
SetNoteScroll(usize),
SetTimeCursor(usize),
SetTimeScroll(usize),
SetTimeZoom(usize),
SetTimeLock(bool),
Show(Option<Arc<RwLock<MidiClip>>>),
}
event_map_input_to_command!(Tui: MidiEditorModel: PhraseCommand: MidiEditorModel::KEYS);
impl MidiEditorModel {
const KEYS: KeyMapping<31, Self> = [
(kexp!(Ctrl-Alt-Up), &|s: &Self|SetNoteScroll(s.note_point() + 3)),
(kexp!(Ctrl-Alt-Down), &|s: &Self|SetNoteScroll(s.note_point().saturating_sub(3))),
(kexp!(Ctrl-Alt-Left), &|s: &Self|SetTimeScroll(s.time_point().saturating_sub(s.time_zoom().get()))),
(kexp!(Ctrl-Alt-Right), &|s: &Self|SetTimeScroll((s.time_point() + s.time_zoom().get()) % s.phrase_length())),
(kexp!(Ctrl-Up), &|s: &Self|SetNoteScroll(s.note_lo().get() + 1)),
(kexp!(Ctrl-Down), &|s: &Self|SetNoteScroll(s.note_lo().get().saturating_sub(1))),
(kexp!(Ctrl-Left), &|s: &Self|SetTimeScroll(s.time_start().get().saturating_sub(s.note_len()))),
(kexp!(Ctrl-Right), &|s: &Self|SetTimeScroll(s.time_start().get() + s.note_len())),
(kexp!(Alt-Up), &|s: &Self|SetNoteCursor(s.note_point() + 3)),
(kexp!(Alt-Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(3))),
(kexp!(Alt-Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.time_zoom().get()))),
(kexp!(Alt-Right), &|s: &Self|SetTimeCursor((s.time_point() + s.time_zoom().get()) % s.phrase_length())),
(kexp!(Up), &|s: &Self|SetNoteCursor(s.note_point() + 1)),
(kexp!(Char('w')), &|s: &Self|SetNoteCursor(s.note_point() + 1)),
(kexp!(Down), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))),
(kexp!(Char('s')), &|s: &Self|SetNoteCursor(s.note_point().saturating_sub(1))),
(kexp!(Left), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))),
(kexp!(Char('a')), &|s: &Self|SetTimeCursor(s.time_point().saturating_sub(s.note_len()))),
(kexp!(Right), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())),
(kexp!(Char('d')), &|s: &Self|SetTimeCursor((s.time_point() + s.note_len()) % s.phrase_length())),
(kexp!(Char('z')), &|s: &Self|SetTimeLock(!s.time_lock().get())),
(kexp!(Char('-')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))),
(kexp!(Char('_')), &|s: &Self|SetTimeZoom(Note::next(s.time_zoom().get()))),
(kexp!(Char('=')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))),
(kexp!(Char('+')), &|s: &Self|SetTimeZoom(Note::prev(s.time_zoom().get()))),
(kexp!(Enter), &|s: &Self|PutNote),
(kexp!(Ctrl-Enter), &|s: &Self|AppendNote),
(kexp!(Char(',')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: no 3plet
(kexp!(Char('.')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))),
(kexp!(Char('<')), &|s: &Self|SetNoteLength(Note::prev(s.note_len()))), // TODO: 3plet
(kexp!(Char('>')), &|s: &Self|SetNoteLength(Note::next(s.note_len()))),
//// TODO: key_pat!(Char('/')) => // toggle 3plet
//// TODO: key_pat!(Char('?')) => // toggle dotted
];
fn phrase_length (&self) -> usize {
self.phrase().as_ref().map(|p|p.read().unwrap().length).unwrap_or(1)
}
}
impl Command<MidiEditorModel> for PhraseCommand {
fn execute (self, state: &mut MidiEditorModel) -> Perhaps<Self> {
use PhraseCommand::*;
match self {
Show(phrase) => { state.set_phrase(phrase.as_ref()); },
PutNote => { state.put_note(false); },
AppendNote => { state.put_note(true); },
SetTimeZoom(x) => { state.time_zoom().set(x); state.redraw(); },
SetTimeLock(x) => { state.time_lock().set(x); },
SetTimeScroll(x) => { state.time_start().set(x); },
SetNoteScroll(x) => { state.note_lo().set(x.min(127)); },
SetNoteLength(x) => { state.set_note_len(x); },
SetTimeCursor(x) => { state.set_time_point(x); },
SetNoteCursor(note) => { state.set_note_point(note.min(127)); },
_ => todo!("{:?}", self)
}
Ok(None)
}
}
/// Contains state for viewing and editing a phrase
pub struct MidiEditorModel {
/// Renders the phrase
pub mode: Box<dyn PhraseViewMode>,
pub size: Measure<Tui>
}
impl Default for MidiEditorModel {
fn default () -> Self {
let mut mode = Box::new(PianoHorizontal::new(None));
mode.redraw();
Self { mode, size: Measure::new() }
}
}
has_size!(<Tui>|self:MidiEditorModel|&self.size);
render!(<Tui>|self: MidiEditorModel|{
self.autoscroll();
self.autozoom();
&self.mode
});
//render!(<Tui>|self: MidiEditorModel|lay!(|add|{add(&self.size)?;add(self.mode)}));//bollocks
pub trait PhraseViewMode: Render<Tui> + HasSize<Tui> + MidiRange + MidiPoint + Debug + Send + Sync {
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize);
fn redraw (&mut self);
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>>;
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>>;
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
*self.phrase_mut() = phrase.cloned();
self.redraw();
}
}
impl MidiView<Tui> for MidiEditorModel {}
impl TimeRange for MidiEditorModel {
fn time_len (&self) -> &AtomicUsize { self.mode.time_len() }
fn time_zoom (&self) -> &AtomicUsize { self.mode.time_zoom() }
fn time_lock (&self) -> &AtomicBool { self.mode.time_lock() }
fn time_start (&self) -> &AtomicUsize { self.mode.time_start() }
fn time_axis (&self) -> &AtomicUsize { self.mode.time_axis() }
}
impl NoteRange for MidiEditorModel {
fn note_lo (&self) -> &AtomicUsize { self.mode.note_lo() }
fn note_axis (&self) -> &AtomicUsize { self.mode.note_axis() }
}
impl NotePoint for MidiEditorModel {
fn note_len (&self) -> usize { self.mode.note_len() }
fn set_note_len (&self, x: usize) { self.mode.set_note_len(x) }
fn note_point (&self) -> usize { self.mode.note_point() }
fn set_note_point (&self, x: usize) { self.mode.set_note_point(x) }
}
impl TimePoint for MidiEditorModel {
fn time_point (&self) -> usize { self.mode.time_point() }
fn set_time_point (&self, x: usize) { self.mode.set_time_point(x) }
}
impl PhraseViewMode for MidiEditorModel {
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) {
self.mode.buffer_size(phrase)
}
fn redraw (&mut self) {
self.mode.redraw()
}
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>> {
self.mode.phrase()
}
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
self.mode.phrase_mut()
}
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
self.mode.set_phrase(phrase)
}
}
impl MidiEditorModel {
/// Put note at current position
pub fn put_note (&mut self, advance: bool) {
let mut redraw = false;
if let Some(phrase) = self.phrase() {
let mut phrase = phrase.write().unwrap();
let note_start = self.time_point();
let note_point = self.note_point();
let note_len = self.note_len();
let note_end = note_start + (note_len.saturating_sub(1));
let key: u7 = u7::from(note_point as u8);
let vel: u7 = 100.into();
let length = phrase.length;
let note_end = note_end % length;
let note_on = MidiMessage::NoteOn { key, vel };
if !phrase.notes[note_start].iter().any(|msg|*msg == note_on) {
phrase.notes[note_start].push(note_on);
}
let note_off = MidiMessage::NoteOff { key, vel };
if !phrase.notes[note_end].iter().any(|msg|*msg == note_off) {
phrase.notes[note_end].push(note_off);
}
if advance {
self.set_time_point(note_end);
}
redraw = true;
}
if redraw {
self.mode.redraw();
}
}
}
from!(|phrase: &Arc<RwLock<MidiClip>>|MidiEditorModel = {
let mut model = Self::from(Some(phrase.clone()));
model.redraw();
model
});
from!(|phrase: Option<Arc<RwLock<MidiClip>>>|MidiEditorModel = {
let mut model = Self::default();
*model.phrase_mut() = phrase;
model.redraw();
model
});
impl std::fmt::Debug for MidiEditorModel {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("MidiEditorModel")
.field("mode", &self.mode)
.finish()
}
}

34
src/midi/midi_launch.rs Normal file
View file

@ -0,0 +1,34 @@
use crate::*;
pub trait HasPlayPhrase: HasClock {
fn reset (&self) -> bool;
fn reset_mut (&mut self) -> &mut bool;
fn play_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn play_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn next_phrase (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn next_phrase_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>;
fn pulses_since_start (&self) -> Option<f64> {
if let Some((started, Some(_))) = self.play_phrase().as_ref() {
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
Some(elapsed)
} else {
None
}
}
fn pulses_since_start_looped (&self) -> Option<f64> {
if let Some((started, Some(phrase))) = self.play_phrase().as_ref() {
let elapsed = self.clock().playhead.pulse.get() - started.pulse.get();
let length = phrase.read().unwrap().length.max(1); // prevent div0 on empty phrase
let elapsed = (elapsed as usize % length) as f64;
Some(elapsed)
} else {
None
}
}
fn enqueue_next (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
let start = self.clock().next_launch_pulse() as f64;
let instant = Moment::from_pulse(self.clock().timebase(), start);
*self.next_phrase_mut() = Some((instant, phrase.cloned()));
*self.reset_mut() = true;
}
}

36
src/midi/midi_note.rs Normal file
View file

@ -0,0 +1,36 @@
use crate::*;
pub struct Note;
impl Note {
/// (pulses, name), assuming 96 PPQ
pub const DURATIONS: [(usize, &str);26] = [
(1, "1/384"), (2, "1/192"),
(3, "1/128"), (4, "1/96"),
(6, "1/64"), (8, "1/48"),
(12, "1/32"), (16, "1/24"),
(24, "1/16"), (32, "1/12"),
(48, "1/8"), (64, "1/6"),
(96, "1/4"), (128, "1/3"),
(192, "1/2"), (256, "2/3"),
(384, "1/1"), (512, "4/3"),
(576, "3/2"), (768, "2/1"),
(1152, "3/1"), (1536, "4/1"),
(2304, "6/1"), (3072, "8/1"),
(3456, "9/1"), (6144, "16/1"),
];
/// Returns the next shorter length
pub fn prev (pulses: usize) -> usize {
for i in 1..=16 { let length = Note::DURATIONS[16-i].0; if length < pulses { return length } }
pulses
}
/// Returns the next longer length
pub fn next (pulses: usize) -> usize {
for (length, _) in &Note::DURATIONS { if *length > pulses { return *length } }
pulses
}
pub fn pulses_to_name (pulses: usize) -> &'static str {
for (length, name) in &Note::DURATIONS { if *length == pulses { return name } }
""
}
}

149
src/midi/midi_play.rs Normal file
View file

@ -0,0 +1,149 @@
use crate::*;
pub trait MidiPlaybackApi: HasPlayPhrase + HasClock + HasMidiOuts {
fn notes_out (&self) -> &Arc<RwLock<[bool;128]>>;
/// Clear the section of the output buffer that we will be using,
/// emitting "all notes off" at start of buffer if requested.
fn clear (
&mut self, scope: &ProcessScope, out: &mut [Vec<Vec<u8>>], reset: bool
) {
for frame in &mut out[0..scope.n_frames() as usize] {
frame.clear();
}
if reset {
all_notes_off(out);
}
}
/// Output notes from phrase to MIDI output ports.
fn play (
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
) -> bool {
if !self.clock().is_rolling() {
return false
}
// If a phrase is playing, write a chunk of MIDI events from it to the output buffer.
// If no phrase is playing, prepare for switchover immediately.
self.play_phrase().as_ref().map_or(true, |(started, phrase)|{
self.play_chunk(scope, note_buf, out, started, phrase)
})
}
/// Handle switchover from current to next playing phrase.
fn switchover (
&mut self, scope: &ProcessScope, note_buf: &mut Vec<u8>, out: &mut [Vec<Vec<u8>>]
) {
if !self.clock().is_rolling() {
return
}
let sample0 = scope.last_frame_time() as usize;
//let samples = scope.n_frames() as usize;
if let Some((start_at, phrase)) = &self.next_phrase() {
let start = start_at.sample.get() as usize;
let sample = self.clock().started.read().unwrap()
.as_ref().unwrap().sample.get() as usize;
// If it's time to switch to the next phrase:
if start <= sample0.saturating_sub(sample) {
// Samples elapsed since phrase was supposed to start
let _skipped = sample0 - start;
// Switch over to enqueued phrase
let started = Moment::from_sample(self.clock().timebase(), start as f64);
// Launch enqueued phrase
*self.play_phrase_mut() = Some((started, phrase.clone()));
// Unset enqueuement (TODO: where to implement looping?)
*self.next_phrase_mut() = None;
// Fill in remaining ticks of chunk from next phrase.
self.play(scope, note_buf, out);
}
}
}
fn play_chunk (
&self,
scope: &ProcessScope,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
started: &Moment,
phrase: &Option<Arc<RwLock<MidiClip>>>
) -> bool {
// First sample to populate. Greater than 0 means that the first
// pulse of the phrase falls somewhere in the middle of the chunk.
let sample = (scope.last_frame_time() as usize).saturating_sub(
started.sample.get() as usize +
self.clock().started.read().unwrap().as_ref().unwrap().sample.get() as usize
);
// Iterator that emits sample (index into output buffer at which to write MIDI event)
// paired with pulse (index into phrase from which to take the MIDI event) for each
// sample of the output buffer that corresponds to a MIDI pulse.
let pulses = self.clock().timebase().pulses_between_samples(sample, sample + scope.n_frames() as usize);
// Notes active during current chunk.
let notes = &mut self.notes_out().write().unwrap();
let length = phrase.as_ref().map_or(0, |p|p.read().unwrap().length);
for (sample, pulse) in pulses {
// If a next phrase is enqueued, and we're past the end of the current one,
// break the loop here (FIXME count pulse correctly)
let past_end = if phrase.is_some() { pulse >= length } else { true };
if self.next_phrase().is_some() && past_end {
return true
}
// If there's a currently playing phrase, output notes from it to buffer:
if let Some(ref phrase) = phrase {
Self::play_pulse(phrase, pulse, sample, note_buf, out, notes)
}
}
false
}
fn play_pulse (
phrase: &RwLock<MidiClip>,
pulse: usize,
sample: usize,
note_buf: &mut Vec<u8>,
out: &mut [Vec<Vec<u8>>],
notes: &mut [bool;128]
) {
// Source phrase from which the MIDI events will be taken.
let phrase = phrase.read().unwrap();
// Phrase with zero length is not processed
if phrase.length > 0 {
// Current pulse index in source phrase
let pulse = pulse % phrase.length;
// Output each MIDI event from phrase at appropriate frames of output buffer:
for message in phrase.notes[pulse].iter() {
// Clear output buffer for this MIDI event.
note_buf.clear();
// TODO: support MIDI channels other than CH1.
let channel = 0.into();
// Serialize MIDI event into message buffer.
LiveEvent::Midi { channel, message: *message }
.write(note_buf)
.unwrap();
// Append serialized message to output buffer.
out[sample].push(note_buf.clone());
// Update the list of currently held notes.
update_keys(&mut*notes, message);
}
}
}
/// Write a chunk of MIDI data from the output buffer to all assigned output ports.
fn write (&mut self, scope: &ProcessScope, out: &[Vec<Vec<u8>>]) {
let samples = scope.n_frames() as usize;
for port in self.midi_outs_mut().iter_mut() {
Self::write_port(&mut port.writer(scope), samples, out)
}
}
/// Write a chunk of MIDI data from the output buffer to an output port.
fn write_port (writer: &mut MidiWriter, samples: usize, out: &[Vec<Vec<u8>>]) {
for (time, events) in out.iter().enumerate().take(samples) {
for bytes in events.iter() {
writer.write(&RawMidi { time: time as u32, bytes }).unwrap_or_else(|_|{
panic!("Failed to write MIDI data: {bytes:?}");
});
}
}
}
}

50
src/midi/midi_point.rs Normal file
View file

@ -0,0 +1,50 @@
use crate::*;
#[derive(Debug, Clone)]
pub struct MidiPointModel {
/// Time coordinate of cursor
pub time_point: Arc<AtomicUsize>,
/// Note coordinate of cursor
pub note_point: Arc<AtomicUsize>,
/// Length of note that will be inserted, in pulses
pub note_len: Arc<AtomicUsize>,
}
impl Default for MidiPointModel {
fn default () -> Self {
Self {
time_point: Arc::new(0.into()),
note_point: Arc::new(36.into()),
note_len: Arc::new(24.into()),
}
}
}
pub trait NotePoint {
fn note_len (&self) -> usize;
fn set_note_len (&self, x: usize);
fn note_point (&self) -> usize;
fn set_note_point (&self, x: usize);
fn note_end (&self) -> usize { self.note_point() + self.note_len() }
}
pub trait TimePoint {
fn time_point (&self) -> usize;
fn set_time_point (&self, x: usize);
}
pub trait MidiPoint: NotePoint + TimePoint {}
impl<T: NotePoint + TimePoint> MidiPoint for T {}
impl NotePoint for MidiPointModel {
fn note_len (&self) -> usize { self.note_len.load(Relaxed)}
fn set_note_len (&self, x: usize) { self.note_len.store(x, Relaxed) }
fn note_point (&self) -> usize { self.note_point.load(Relaxed).min(127) }
fn set_note_point (&self, x: usize) { self.note_point.store(x.min(127), Relaxed) }
}
impl TimePoint for MidiPointModel {
fn time_point (&self) -> usize { self.time_point.load(Relaxed) }
fn set_time_point (&self, x: usize) { self.time_point.store(x, Relaxed) }
}

93
src/midi/midi_pool.rs Normal file
View file

@ -0,0 +1,93 @@
use crate::*;
pub trait HasPhrases {
fn phrases (&self) -> &Vec<Arc<RwLock<MidiClip>>>;
fn phrases_mut (&mut self) -> &mut Vec<Arc<RwLock<MidiClip>>>;
}
#[macro_export] macro_rules! has_phrases {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasPhrases for $Struct $(<$($L),*$($T),*>)? {
fn phrases (&$self) -> &Vec<Arc<RwLock<MidiClip>>> { &$cb }
fn phrases_mut (&mut $self) -> &mut Vec<Arc<RwLock<MidiClip>>> { &mut$cb }
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum PhrasePoolCommand {
Add(usize, MidiClip),
Delete(usize),
Swap(usize, usize),
Import(usize, PathBuf),
Export(usize, PathBuf),
SetName(usize, String),
SetLength(usize, usize),
SetColor(usize, ItemColor),
}
impl<T: HasPhrases> Command<T> for PhrasePoolCommand {
fn execute (self, model: &mut T) -> Perhaps<Self> {
use PhrasePoolCommand::*;
Ok(match self {
Add(mut index, phrase) => {
let phrase = Arc::new(RwLock::new(phrase));
let phrases = model.phrases_mut();
if index >= phrases.len() {
index = phrases.len();
phrases.push(phrase)
} else {
phrases.insert(index, phrase);
}
Some(Self::Delete(index))
},
Delete(index) => {
let phrase = model.phrases_mut().remove(index).read().unwrap().clone();
Some(Self::Add(index, phrase))
},
Swap(index, other) => {
model.phrases_mut().swap(index, other);
Some(Self::Swap(index, other))
},
Import(index, path) => {
let bytes = std::fs::read(&path)?;
let smf = Smf::parse(bytes.as_slice())?;
let mut t = 0u32;
let mut events = vec![];
for track in smf.tracks.iter() {
for event in track.iter() {
t += event.delta.as_int();
if let TrackEventKind::Midi { channel, message } = event.kind {
events.push((t, channel.as_int(), message));
}
}
}
let mut phrase = MidiClip::new("imported", true, t as usize + 1, None, None);
for event in events.iter() {
phrase.notes[event.0 as usize].push(event.2);
}
Self::Add(index, phrase).execute(model)?
},
Export(_index, _path) => {
todo!("export phrase to midi file");
},
SetName(index, name) => {
let mut phrase = model.phrases()[index].write().unwrap();
let old_name = phrase.name.clone();
phrase.name = name;
Some(Self::SetName(index, old_name))
},
SetLength(index, length) => {
let mut phrase = model.phrases()[index].write().unwrap();
let old_len = phrase.length;
phrase.length = length;
Some(Self::SetLength(index, old_len))
},
SetColor(index, color) => {
let mut color = ItemPalette::from(color);
std::mem::swap(&mut color, &mut model.phrases()[index].write().unwrap().color);
Some(Self::SetColor(index, color.base))
},
})
}
}

64
src/midi/midi_range.rs Normal file
View file

@ -0,0 +1,64 @@
use crate::*;
#[derive(Debug, Clone)]
pub struct MidiRangeModel {
pub time_len: Arc<AtomicUsize>,
/// Length of visible time axis
pub time_axis: Arc<AtomicUsize>,
/// Earliest time displayed
pub time_start: Arc<AtomicUsize>,
/// Time step
pub time_zoom: Arc<AtomicUsize>,
/// Auto rezoom to fit in time axis
pub time_lock: Arc<AtomicBool>,
/// Length of visible note axis
pub note_axis: Arc<AtomicUsize>,
// Lowest note displayed
pub note_lo: Arc<AtomicUsize>,
}
from!(|data:(usize, bool)|MidiRangeModel = Self {
time_len: Arc::new(0.into()),
note_axis: Arc::new(0.into()),
note_lo: Arc::new(0.into()),
time_axis: Arc::new(0.into()),
time_start: Arc::new(0.into()),
time_zoom: Arc::new(data.0.into()),
time_lock: Arc::new(data.1.into()),
});
pub trait TimeRange {
fn time_len (&self) -> &AtomicUsize;
fn time_zoom (&self) -> &AtomicUsize;
fn time_lock (&self) -> &AtomicBool;
fn time_start (&self) -> &AtomicUsize;
fn time_axis (&self) -> &AtomicUsize;
fn time_end (&self) -> usize {
self.time_start().get() + self.time_axis().get() * self.time_zoom().get()
}
}
pub trait NoteRange {
fn note_lo (&self) -> &AtomicUsize;
fn note_axis (&self) -> &AtomicUsize;
fn note_hi (&self) -> usize {
(self.note_lo().get() + self.note_axis().get().saturating_sub(1)).min(127)
}
}
pub trait MidiRange: TimeRange + NoteRange {}
impl<T: TimeRange + NoteRange> MidiRange for T {}
impl TimeRange for MidiRangeModel {
fn time_len (&self) -> &AtomicUsize { &self.time_len }
fn time_zoom (&self) -> &AtomicUsize { &self.time_zoom }
fn time_lock (&self) -> &AtomicBool { &self.time_lock }
fn time_start (&self) -> &AtomicUsize { &self.time_start }
fn time_axis (&self) -> &AtomicUsize { &self.time_axis }
}
impl NoteRange for MidiRangeModel {
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
fn note_axis (&self) -> &AtomicUsize { &self.note_axis }
}

82
src/midi/midi_rec.rs Normal file
View file

@ -0,0 +1,82 @@
use crate::*;
pub trait MidiRecordApi: HasClock + HasPlayPhrase + HasMidiIns {
fn notes_in (&self) -> &Arc<RwLock<[bool;128]>>;
fn recording (&self) -> bool;
fn recording_mut (&mut self) -> &mut bool;
fn toggle_record (&mut self) {
*self.recording_mut() = !self.recording();
}
fn monitoring (&self) -> bool;
fn monitoring_mut (&mut self) -> &mut bool;
fn toggle_monitor (&mut self) {
*self.monitoring_mut() = !self.monitoring();
}
fn monitor (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
// For highlighting keys and note repeat
let notes_in = self.notes_in().clone();
let monitoring = self.monitoring();
for input in self.midi_ins_mut().iter() {
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
if monitoring {
midi_buf[sample].push(bytes.to_vec());
}
update_keys(&mut notes_in.write().unwrap(), &message);
}
}
}
}
fn record (&mut self, scope: &ProcessScope, midi_buf: &mut Vec<Vec<Vec<u8>>>) {
if self.monitoring() {
self.monitor(scope, midi_buf);
}
if !self.clock().is_rolling() {
return
}
if let Some((started, ref clip)) = self.play_phrase().clone() {
self.record_clip(scope, started, clip, midi_buf);
}
if let Some((start_at, phrase)) = &self.next_phrase() {
self.record_next();
}
}
fn record_clip (
&mut self,
scope: &ProcessScope,
started: Moment,
phrase: &Option<Arc<RwLock<MidiClip>>>,
midi_buf: &mut Vec<Vec<Vec<u8>>>
) {
if let Some(phrase) = phrase {
let sample0 = scope.last_frame_time() as usize;
let start = started.sample.get() as usize;
let recording = self.recording();
let timebase = self.clock().timebase().clone();
let quant = self.clock().quant.get();
let mut phrase = phrase.write().unwrap();
let length = phrase.length;
for input in self.midi_ins_mut().iter() {
for (sample, event, bytes) in parse_midi_input(input.iter(scope)) {
if let LiveEvent::Midi { message, .. } = event {
phrase.record_event({
let sample = (sample0 + sample - start) as f64;
let pulse = timebase.samples_to_pulse(sample);
let quantized = (pulse / quant).round() * quant;
quantized as usize % length
}, message);
}
}
}
}
}
fn record_next (&mut self) {
// TODO switch to next clip and record into it
}
fn overdub (&self) -> bool;
fn overdub_mut (&mut self) -> &mut bool;
fn toggle_overdub (&mut self) {
*self.overdub_mut() = !self.overdub();
}
}

26
src/midi/midi_view.rs Normal file
View file

@ -0,0 +1,26 @@
use crate::*;
pub trait MidiView<E: Engine>: MidiRange + MidiPoint + HasSize<E> {
/// Make sure cursor is within range
fn autoscroll (&self) {
let note_point = self.note_point().min(127);
let note_lo = self.note_lo().get();
let note_hi = self.note_hi();
if note_point < note_lo {
self.note_lo().set(note_point);
} else if note_point > note_hi {
self.note_lo().set((note_lo + note_point).saturating_sub(note_hi));
}
}
/// Make sure range is within display
fn autozoom (&self) {
let time_len = self.time_len().get();
let time_axis = self.time_axis().get();
let time_zoom = self.time_zoom().get();
//while time_len.div_ceil(time_zoom) > time_axis {
//println!("\r{time_len} {time_zoom} {time_axis}");
//time_zoom = Note::next(time_zoom);
//}
//self.time_zoom().set(time_zoom);
}
}

263
src/mixer.rs Normal file
View file

@ -0,0 +1,263 @@
use crate::*;
#[derive(Debug)]
pub struct Mixer {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
pub name: String,
pub tracks: Vec<MixerTrack>,
pub selected_track: usize,
pub selected_column: usize,
}
/// A mixer track.
#[derive(Debug)]
pub struct MixerTrack {
pub name: String,
/// Inputs of 1st device
pub audio_ins: Vec<Port<AudioIn>>,
/// Outputs of last device
pub audio_outs: Vec<Port<AudioOut>>,
/// Device chain
pub devices: Vec<Box<dyn MixerTrackDevice>>,
}
audio!(|self: Mixer, _client, _scope|Control::Continue);
impl Mixer {
pub fn new (jack: &Arc<RwLock<JackClient>>, name: &str) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
name: name.into(),
selected_column: 0,
selected_track: 1,
tracks: vec![],
})
}
pub fn track_add (&mut self, name: &str, channels: usize) -> Usually<&mut Self> {
let track = MixerTrack::new(name)?;
self.tracks.push(track);
Ok(self)
}
pub fn track (&self) -> Option<&MixerTrack> {
self.tracks.get(self.selected_track)
}
}
impl MixerTrack {
pub fn new (name: &str) -> Usually<Self> {
Ok(Self {
name: name.to_string(),
audio_ins: vec![],
audio_outs: vec![],
devices: vec![],
//ports: JackPorts::default(),
//devices: vec![],
//device: 0,
})
}
//fn get_device_mut (&self, i: usize) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
//self.devices.get(i).map(|d|d.state.write().unwrap())
//}
//pub fn device_mut (&self) -> Option<RwLockWriteGuard<Box<dyn AudioComponent<E>>>> {
//self.get_device_mut(self.device)
//}
///// Add a device to the end of the chain.
//pub fn append_device (&mut self, device: JackDevice<E>) -> Usually<&mut JackDevice<E>> {
//self.devices.push(device);
//let index = self.devices.len() - 1;
//Ok(&mut self.devices[index])
//}
//pub fn add_device (&mut self, device: JackDevice<E>) {
//self.devices.push(device);
//}
//pub fn connect_first_device (&self) -> Usually<()> {
//if let (Some(port), Some(device)) = (&self.midi_out, self.devices.get(0)) {
//device.client.as_client().connect_ports(&port, &device.midi_ins()?[0])?;
//}
//Ok(())
//}
//pub fn connect_last_device (&self, app: &Track) -> Usually<()> {
//Ok(match self.devices.get(self.devices.len().saturating_sub(1)) {
//Some(device) => {
//app.audio_out(0).map(|left|device.connect_audio_out(0, &left)).transpose()?;
//app.audio_out(1).map(|right|device.connect_audio_out(1, &right)).transpose()?;
//()
//},
//None => ()
//})
//}
}
pub struct TrackView<'a> {
pub chain: Option<&'a MixerTrack>,
pub direction: Direction,
pub focused: bool,
pub entered: bool,
}
impl<'a> Render<Tui> for TrackView<'a> {
fn min_size (&self, area: [u16;2]) -> Perhaps<[u16;2]> {
todo!()
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
todo!();
//let mut area = to.area();
//if let Some(chain) = self.chain {
//match self.direction {
//Direction::Down => area.width = area.width.min(40),
//Direction::Right => area.width = area.width.min(10),
//_ => { unimplemented!() },
//}
//to.fill_bg(to.area(), Nord::bg_lo(self.focused, self.entered));
//let mut split = Stack::new(self.direction);
//for device in chain.devices.as_slice().iter() {
//split = split.add_ref(device);
//}
//let (area, areas) = split.render_areas(to)?;
//if self.focused && self.entered && areas.len() > 0 {
//Corners(Style::default().green().not_dim()).draw(to.with_rect(areas[0]))?;
//}
//Ok(Some(area))
//} else {
//let [x, y, width, height] = area;
//let label = "No chain selected";
//let x = x + (width - label.len() as u16) / 2;
//let y = y + height / 2;
//to.blit(&label, x, y, Some(Style::default().dim().bold()))?;
//Ok(Some(area))
//}
}
}
//impl Content<Tui> for Mixer<Tui> {
//fn content (&self) -> impl Render<Tui> {
//Stack::right(|add| {
//for channel in self.tracks.iter() {
//add(channel)?;
//}
//Ok(())
//})
//}
//}
//impl Content<Tui> for Track<Tui> {
//fn content (&self) -> impl Render<Tui> {
//TrackView {
//chain: Some(&self),
//direction: tek_core::Direction::Right,
//focused: true,
//entered: true,
////pub channels: u8,
////pub input_ports: Vec<Port<AudioIn>>,
////pub pre_gain_meter: f64,
////pub gain: f64,
////pub insert_ports: Vec<Port<AudioOut>>,
////pub return_ports: Vec<Port<AudioIn>>,
////pub post_gain_meter: f64,
////pub post_insert_meter: f64,
////pub level: f64,
////pub pan: f64,
////pub output_ports: Vec<Port<AudioOut>>,
////pub post_fader_meter: f64,
////pub route: String,
//}
//}
//}
handle!(<Tui>|self:Mixer,engine|{
if let TuiEvent::Input(crossterm::event::Event::Key(event)) = engine.event() {
match event.code {
//KeyCode::Char('c') => {
//if event.modifiers == KeyModifiers::CONTROL {
//self.exit();
//}
//},
KeyCode::Down => {
self.selected_track = (self.selected_track + 1) % self.tracks.len();
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Up => {
if self.selected_track == 0 {
self.selected_track = self.tracks.len() - 1;
} else {
self.selected_track -= 1;
}
println!("{}", self.selected_track);
return Ok(Some(true))
},
KeyCode::Left => {
if self.selected_column == 0 {
self.selected_column = 6
} else {
self.selected_column -= 1;
}
return Ok(Some(true))
},
KeyCode::Right => {
if self.selected_column == 6 {
self.selected_column = 0
} else {
self.selected_column += 1;
}
return Ok(Some(true))
},
_ => {
println!("\n{event:?}");
}
}
}
Ok(None)
});
handle!(<Tui>|self:MixerTrack,from|{
match from.event() {
//, NONE, "chain_cursor_up", "move cursor up", || {
key_pat!(KeyCode::Up) => {
Ok(Some(true))
},
// , NONE, "chain_cursor_down", "move cursor down", || {
key_pat!(KeyCode::Down) => {
Ok(Some(true))
},
// Left, NONE, "chain_cursor_left", "move cursor left", || {
key_pat!(KeyCode::Left) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = track.device.saturating_sub(1);
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_cursor_right", "move cursor right", || {
key_pat!(KeyCode::Right) => {
//if let Some(track) = app.arranger.track_mut() {
//track.device = (track.device + 1).min(track.devices.len().saturating_sub(1));
//return Ok(true)
//}
Ok(Some(true))
},
// , NONE, "chain_mode_switch", "switch the display mode", || {
key_pat!(KeyCode::Char('`')) => {
//app.chain_mode = !app.chain_mode;
Ok(Some(true))
},
_ => Ok(None)
}
});
pub enum MixerTrackCommand {}
//impl MixerTrackDevice for LV2Plugin {}
pub trait MixerTrackDevice: Debug + Send + Sync {
fn boxed (self) -> Box<dyn MixerTrackDevice> where Self: Sized + 'static {
Box::new(self)
}
}
impl MixerTrackDevice for Sampler {}
impl MixerTrackDevice for Plugin {}

244
src/piano_h.rs Normal file
View file

@ -0,0 +1,244 @@
use crate::*;
use super::*;
mod piano_h_cursor;
use self::piano_h_cursor::*;
mod piano_h_keys;
pub(crate) use self::piano_h_keys::*;
pub use self::piano_h_keys::render_keys_v;
mod piano_h_notes; use self::piano_h_notes::*;
mod piano_h_time; use self::piano_h_time::*;
pub(crate) fn note_y_iter (note_lo: usize, note_hi: usize, y0: u16) -> impl Iterator<Item=(usize, u16, usize)> {
(note_lo..=note_hi).rev().enumerate().map(move|(y, n)|(y, y0 + y as u16, n))
}
/// A phrase, rendered as a horizontal piano roll.
pub struct PianoHorizontal {
phrase: Option<Arc<RwLock<MidiClip>>>,
/// Buffer where the whole phrase is rerendered on change
buffer: BigBuffer,
/// Size of actual notes area
size: Measure<Tui>,
/// The display window
range: MidiRangeModel,
/// The note cursor
point: MidiPointModel,
/// The highlight color palette
color: ItemPalette,
/// Width of the keyboard
keys_width: u16,
}
impl PianoHorizontal {
pub fn new (phrase: Option<&Arc<RwLock<MidiClip>>>) -> Self {
let size = Measure::new();
let mut range = MidiRangeModel::from((24, true));
range.time_axis = size.x.clone();
range.note_axis = size.y.clone();
let color = phrase.as_ref()
.map(|p|p.read().unwrap().color)
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
Self {
buffer: Default::default(),
point: MidiPointModel::default(),
phrase: phrase.cloned(),
size,
range,
color,
keys_width: 5
}
}
}
render!(<Tui>|self: PianoHorizontal|{
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(), 0, false)
};
let field = move|x, y|row!([
Tui::fg_bg(color.lighter.rgb, color.darker.rgb, Tui::bold(true, x)),
Tui::fg_bg(color.lighter.rgb, color.dark.rgb, &y),
]);
let keys_width = 5;
let keys = move||PianoHorizontalKeys(self);
let timeline = move||PianoHorizontalTimeline(self);
let notes = move||PianoHorizontalNotes(self);
let cursor = move||PianoHorizontalCursor(self);
let border = Fill::wh(Outer(Style::default().fg(self.color.dark.rgb).bg(self.color.darkest.rgb)));
let with_border = |x|lay!([border, Tui::inset_xy(0, 0, &x)]);
with_border(lay!([
Tui::push_x(0, row!(![
//" ",
field("Edit:", name.to_string()), " ",
field("Length:", length.to_string()), " ",
field("Loop:", looped.to_string())
])),
Tui::inset_xy(0, 1, Fill::wh(Bsp::s(
Fixed::h(1, Bsp::e(
Fixed::w(self.keys_width, ""),
Fill::w(timeline()),
)),
Bsp::e(
Fixed::w(self.keys_width, keys()),
Fill::wh(lay!([
&self.size,
Fill::wh(lay!([
Fill::wh(notes()),
Fill::wh(cursor()),
]))
])),
),
)))
]))
});
impl PianoHorizontal {
/// Draw the piano roll foreground using full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_bg (buf: &mut BigBuffer, phrase: &MidiClip, zoom: usize, note_len: usize) {
for (y, note) in (0..127).rev().enumerate() {
for (x, time) in (0..buf.width).map(|x|(x, x*zoom)) {
let cell = buf.get_mut(x, y).unwrap();
cell.set_bg(phrase.color.darkest.rgb);
cell.set_fg(phrase.color.darker.rgb);
cell.set_char(if time % 384 == 0 {
'│'
} else if time % 96 == 0 {
'╎'
} else if time % note_len == 0 {
'┊'
} else if (127 - note) % 12 == 1 {
'='
} else {
'·'
});
}
}
}
/// Draw the piano roll background using full blocks on note on and half blocks on legato: █▄ █▄ █▄
fn draw_fg (buf: &mut BigBuffer, phrase: &MidiClip, zoom: usize) {
let style = Style::default().fg(phrase.color.base.rgb);//.bg(Color::Rgb(0, 0, 0));
let mut notes_on = [false;128];
for (x, time_start) in (0..phrase.length).step_by(zoom).enumerate() {
for (y, note) in (0..127).rev().enumerate() {
if let Some(cell) = buf.get_mut(x, note) {
if notes_on[note] {
cell.set_char('▂');
cell.set_style(style);
}
}
}
let time_end = time_start + zoom;
for time in time_start..time_end.min(phrase.length) {
for event in phrase.notes[time].iter() {
match event {
MidiMessage::NoteOn { key, .. } => {
let note = key.as_int() as usize;
let cell = buf.get_mut(x, note).unwrap();
cell.set_char('█');
cell.set_style(style);
notes_on[note] = true
},
MidiMessage::NoteOff { key, .. } => {
notes_on[key.as_int() as usize] = false
},
_ => {}
}
}
}
}
}
}
has_size!(<Tui>|self:PianoHorizontal|&self.size);
impl TimeRange for PianoHorizontal {
fn time_len (&self) -> &AtomicUsize { self.range.time_len() }
fn time_zoom (&self) -> &AtomicUsize { self.range.time_zoom() }
fn time_lock (&self) -> &AtomicBool { self.range.time_lock() }
fn time_start (&self) -> &AtomicUsize { self.range.time_start() }
fn time_axis (&self) -> &AtomicUsize { self.range.time_axis() }
}
impl NoteRange for PianoHorizontal {
fn note_lo (&self) -> &AtomicUsize { self.range.note_lo() }
fn note_axis (&self) -> &AtomicUsize { self.range.note_axis() }
}
impl NotePoint for PianoHorizontal {
fn note_len (&self) -> usize { self.point.note_len() }
fn set_note_len (&self, x: usize) { self.point.set_note_len(x) }
fn note_point (&self) -> usize { self.point.note_point() }
fn set_note_point (&self, x: usize) { self.point.set_note_point(x) }
}
impl TimePoint for PianoHorizontal {
fn time_point (&self) -> usize { self.point.time_point() }
fn set_time_point (&self, x: usize) { self.point.set_time_point(x) }
}
impl PhraseViewMode for PianoHorizontal {
fn phrase (&self) -> &Option<Arc<RwLock<MidiClip>>> {
&self.phrase
}
fn phrase_mut (&mut self) -> &mut Option<Arc<RwLock<MidiClip>>> {
&mut self.phrase
}
/// Determine the required space to render the phrase.
fn buffer_size (&self, phrase: &MidiClip) -> (usize, usize) {
(phrase.length / self.range.time_zoom().get(), 128)
}
fn redraw (&mut self) {
let buffer = if let Some(phrase) = self.phrase.as_ref() {
let phrase = phrase.read().unwrap();
let buf_size = self.buffer_size(&phrase);
let mut buffer = BigBuffer::from(buf_size);
let note_len = self.note_len();
let time_zoom = self.time_zoom().get();
self.time_len().set(phrase.length);
PianoHorizontal::draw_bg(&mut buffer, &phrase, time_zoom, note_len);
PianoHorizontal::draw_fg(&mut buffer, &phrase, time_zoom);
buffer
} else {
Default::default()
};
self.buffer = buffer
}
fn set_phrase (&mut self, phrase: Option<&Arc<RwLock<MidiClip>>>) {
*self.phrase_mut() = phrase.cloned();
self.color = phrase.map(|p|p.read().unwrap().color)
.unwrap_or(ItemPalette::from(ItemColor::from(TuiTheme::g(64))));
self.redraw();
}
}
impl std::fmt::Debug for PianoHorizontal {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("PianoHorizontal")
.field("time_zoom", &self.range.time_zoom)
.field("buffer", &format!("{}x{}", self.buffer.width, self.buffer.height))
.finish()
}
}
// Update sequencer playhead indicator
//self.now().set(0.);
//if let Some((ref started_at, Some(ref playing))) = self.player.play_phrase {
//let phrase = phrase.read().unwrap();
//if *playing.read().unwrap() == *phrase {
//let pulse = self.current().pulse.get();
//let start = started_at.pulse.get();
//let now = (pulse - start) % phrase.length as f64;
//self.now().set(now);
//}
//}

View file

@ -0,0 +1,33 @@
use crate::*;
use super::note_y_iter;
pub struct PianoHorizontalCursor<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalCursor<'a>|render(|to|Ok({
let style = Some(Style::default().fg(self.0.color.lightest.rgb));
let note_hi = self.0.note_hi();
let note_len = self.0.note_len();
let note_lo = self.0.note_lo().get();
let note_point = self.0.note_point();
let time_point = self.0.time_point();
let time_start = self.0.time_start().get();
let time_zoom = self.0.time_zoom().get();
let [x0, y0, w, _] = to.area().xywh();
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
if note == note_point {
for x in 0..w {
let screen_x = x0 + x;
let time_1 = time_start + x as usize * time_zoom;
let time_2 = time_1 + time_zoom;
if time_1 <= time_point && time_point < time_2 {
to.blit(&"", screen_x, screen_y, style);
let tail = note_len as u16 / time_zoom as u16;
for x_tail in (screen_x + 1)..(screen_x + tail) {
to.blit(&"", x_tail, screen_y, style);
}
break
}
}
break
}
}
})));

View file

@ -0,0 +1,56 @@
use crate::*;
use super::note_y_iter;
pub struct PianoHorizontalKeys<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalKeys<'a>|render(|to|Ok(render_keys_v(to, self))));
has_color!(|self: PianoHorizontalKeys<'a>|self.0.color.base);
impl<'a> NoteRange for PianoHorizontalKeys<'a> {
fn note_lo (&self) -> &AtomicUsize { &self.0.note_lo() }
fn note_axis (&self) -> &AtomicUsize { &self.0.note_axis() }
}
impl<'a> NotePoint for PianoHorizontalKeys<'a> {
fn note_len (&self) -> usize { self.0.note_len() }
fn set_note_len (&self, x: usize) { self.0.set_note_len(x) }
fn note_point (&self) -> usize { self.0.note_point() }
fn set_note_point (&self, x: usize) { self.0.set_note_point(x) }
}
pub fn render_keys_v <T: HasColor + NoteRange + NotePoint> (to: &mut TuiOutput, state: &T) {
let color = state.color();
let note_lo = state.note_lo().get();
let note_hi = state.note_hi();
let note_point = state.note_point();
let [x, y0, w, h] = to.area().xywh();
let key_style = Some(Style::default().fg(Color::Rgb(192, 192, 192)).bg(Color::Rgb(0, 0, 0)));
let off_style = Some(Style::default().fg(TuiTheme::g(160)));
let on_style = Some(Style::default().fg(TuiTheme::g(255)).bg(color.rgb).bold());
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
to.blit(&to_key(note), x, screen_y, key_style);
if note > 127 {
continue
}
if note == note_point {
to.blit(&format!("{:<5}", to_note_name(note)), x, screen_y, on_style)
} else {
to.blit(&to_note_name(note), x, screen_y, off_style)
};
}
}
fn to_key (note: usize) -> &'static str {
match note % 12 {
11 => "████▌",
10 => " ",
9 => "████▌",
8 => " ",
7 => "████▌",
6 => " ",
5 => "████▌",
4 => "████▌",
3 => " ",
2 => "████▌",
1 => " ",
0 => "████▌",
_ => unreachable!(),
}
}

View file

@ -0,0 +1,46 @@
use crate::*;
use super::note_y_iter;
pub struct PianoHorizontalNotes<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalNotes<'a>|render(|to|Ok({
let time_start = self.0.time_start().get();
let note_lo = self.0.note_lo().get();
let note_hi = self.0.note_hi();
let note_point = self.0.note_point();
let source = &self.0.buffer;
let [x0, y0, w, h] = to.area().xywh();
if h as usize != self.0.note_axis().get() {
panic!("area height mismatch");
}
for (area_x, screen_x) in (x0..x0+w).enumerate() {
for (area_y, screen_y, note) in note_y_iter(note_lo, note_hi, y0) {
//if area_x % 10 == 0 {
//to.blit(&format!("{area_y} {note}"), screen_x, screen_y, None);
//}
let source_x = time_start + area_x;
let source_y = note_hi - area_y;
////// TODO: enable loop rollover:
//////let source_x = (time_start + area_x) % source.width.max(1);
//////let source_y = (note_hi - area_y) % source.height.max(1);
let is_in_x = source_x < source.width;
let is_in_y = source_y < source.height;
if is_in_x && is_in_y {
if let Some(source_cell) = source.get(source_x, source_y) {
*to.buffer.get_mut(screen_x, screen_y) = source_cell.clone();
}
}
}
}
//let debug = true;
//if debug {
//let x0=20+x0;
//to.blit(&format!("KYP "), x0, y0, None);
//to.blit(&format!("x0={x0} "), x0, y0+1, None);
//to.blit(&format!("y0={y0} "), x0, y0+2, None);
//to.blit(&format!("note_lo={note_lo}, {} ", to_note_name(note_lo)), x0, y0+3, None);
//to.blit(&format!("note_hi={note_hi}, {} ", to_note_name(note_hi)), x0, y0+4, None);
//to.blit(&format!("note_point={note_point}, {} ", to_note_name(note_point)), x0, y0+5, None);
//to.blit(&format!("time_start={time_start} "), x0, y0+6, None);
//return Ok(());
//}
})));

View file

@ -0,0 +1,20 @@
use crate::*;
pub struct PianoHorizontalTimeline<'a>(pub(crate) &'a PianoHorizontal);
render!(<Tui>|self: PianoHorizontalTimeline<'a>|render(|to|{
let [x, y, w, h] = to.area();
let style = Some(Style::default().dim());
let length = self.0.phrase.as_ref().map(|p|p.read().unwrap().length).unwrap_or(1);
for (area_x, screen_x) in (0..w).map(|d|(d, d+x)) {
let t = area_x as usize * self.0.time_zoom().get();
if t < length {
to.blit(&"|", screen_x, y, style);
}
}
Ok(())
}));
//Tui::fg_bg(
//self.0.color.lightest.rgb,
//self.0.color.darkest.rgb,
//format!("{}*{}", self.0.time_start(), self.0.time_zoom()).as_str()
//));

1
src/piano_v.rs Normal file
View file

@ -0,0 +1 @@
// TODO

266
src/plugin.rs Normal file
View file

@ -0,0 +1,266 @@
use crate::*;
pub mod lv2; pub(crate) use lv2::*;
pub use self::lv2::LV2Plugin;
/// A plugin device.
#[derive(Debug)]
pub struct Plugin {
/// JACK client handle (needs to not be dropped for standalone mode to work).
pub jack: Arc<RwLock<JackClient>>,
pub name: String,
pub path: Option<String>,
pub plugin: Option<PluginKind>,
pub selected: usize,
pub mapping: bool,
pub midi_ins: Vec<Port<MidiIn>>,
pub midi_outs: Vec<Port<MidiOut>>,
pub audio_ins: Vec<Port<AudioIn>>,
pub audio_outs: Vec<Port<AudioOut>>,
}
/// Supported plugin formats.
#[derive(Default)]
pub enum PluginKind {
#[default] None,
LV2(LV2Plugin),
VST2 { instance: () /*::vst::host::PluginInstance*/ },
VST3,
}
impl Debug for PluginKind {
fn fmt (&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{}", match self {
Self::None => "(none)",
Self::LV2(_) => "LV2",
Self::VST2{..} => "VST2",
Self::VST3 => "VST3",
})
}
}
impl Plugin {
pub fn new_lv2 (
jack: &Arc<RwLock<JackClient>>,
name: &str,
path: &str,
) -> Usually<Self> {
Ok(Self {
jack: jack.clone(),
name: name.into(),
path: Some(String::from(path)),
plugin: Some(PluginKind::LV2(LV2Plugin::new(path)?)),
selected: 0,
mapping: false,
midi_ins: vec![],
midi_outs: vec![],
audio_ins: vec![],
audio_outs: vec![],
})
}
}
pub struct PluginAudio(Arc<RwLock<Plugin>>);
from!(|model: &Arc<RwLock<Plugin>>| PluginAudio = Self(model.clone()));
audio!(|self: PluginAudio, client, scope|{
let state = &mut*self.0.write().unwrap();
match state.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin {
features,
ref mut instance,
ref mut input_buffer,
..
})) => {
let urid = features.midi_urid();
input_buffer.clear();
for port in state.midi_ins.iter() {
let mut atom = ::livi::event::LV2AtomSequence::new(
&features,
scope.n_frames() as usize
);
for event in port.iter(scope) {
match event.bytes.len() {
3 => atom.push_midi_event::<3>(
event.time as i64,
urid,
&event.bytes[0..3]
).unwrap(),
_ => {}
}
}
input_buffer.push(atom);
}
let mut outputs = vec![];
for _ in state.midi_outs.iter() {
outputs.push(::livi::event::LV2AtomSequence::new(
features,
scope.n_frames() as usize
));
}
let ports = ::livi::EmptyPortConnections::new()
.with_atom_sequence_inputs(input_buffer.iter())
.with_atom_sequence_outputs(outputs.iter_mut())
.with_audio_inputs(state.audio_ins.iter().map(|o|o.as_slice(scope)))
.with_audio_outputs(state.audio_outs.iter_mut().map(|o|o.as_mut_slice(scope)));
unsafe {
instance.run(scope.n_frames() as usize, ports).unwrap()
};
},
_ => todo!("only lv2 is supported")
}
Control::Continue
});
//fn jack_from_lv2 (name: &str, plugin: &::livi::Plugin) -> Usually<Jack> {
//let counts = plugin.port_counts();
//let mut jack = Jack::new(name)?;
//for i in 0..counts.atom_sequence_inputs {
//jack = jack.midi_in(&format!("midi-in-{i}"))
//}
//for i in 0..counts.atom_sequence_outputs {
//jack = jack.midi_out(&format!("midi-out-{i}"));
//}
//for i in 0..counts.audio_inputs {
//jack = jack.audio_in(&format!("audio-in-{i}"));
//}
//for i in 0..counts.audio_outputs {
//jack = jack.audio_out(&format!("audio-out-{i}"));
//}
//Ok(jack)
//}
impl Plugin {
/// Create a plugin host device.
pub fn new (
jack: &Arc<RwLock<JackClient>>,
name: &str,
) -> Usually<Self> {
Ok(Self {
//_engine: Default::default(),
jack: jack.clone(),
name: name.into(),
path: None,
plugin: None,
selected: 0,
mapping: false,
audio_ins: vec![],
audio_outs: vec![],
midi_ins: vec![],
midi_outs: vec![],
//ports: JackPorts::default()
})
}
}
impl Render<Tui> for Plugin {
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
Ok(Some(to))
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
let area = to.area();
let [x, y, _, height] = area;
let mut width = 20u16;
match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, instance, .. })) => {
let start = self.selected.saturating_sub((height as usize / 2).saturating_sub(1));
let end = start + height as usize - 2;
//draw_box(buf, Rect { x, y, width, height });
for i in start..end {
if let Some(port) = port_list.get(i) {
let value = if let Some(value) = instance.control_input(port.index) {
value
} else {
port.default_value
};
//let label = &format!("C·· M·· {:25} = {value:.03}", port.name);
let label = &format!("{:25} = {value:.03}", port.name);
width = width.max(label.len() as u16 + 4);
let style = if i == self.selected {
Some(Style::default().green())
} else {
None
} ;
to.blit(&label, x + 2, y + 1 + i as u16 - start as u16, style);
} else {
break
}
}
},
_ => {}
};
draw_header(self, to, x, y, width)?;
Ok(())
}
}
fn draw_header (state: &Plugin, to: &mut TuiOutput, x: u16, y: u16, w: u16) -> Usually<()> {
let style = Style::default().gray();
let label1 = format!(" {}", state.name);
to.blit(&label1, x + 1, y, Some(style.white().bold()));
if let Some(ref path) = state.path {
let label2 = format!("{}", &path[..((w as usize - 10).min(path.len()))]);
to.blit(&label2, x + 2 + label1.len() as u16, y, Some(style.not_dim()));
}
Ok(())
//Ok(Rect { x, y, width: w, height: 1 })
}
handle!(<Tui>|self:Plugin, from|{
match from.event() {
key_pat!(KeyCode::Up) => {
self.selected = self.selected.saturating_sub(1);
Ok(Some(true))
},
key_pat!(KeyCode::Down) => {
self.selected = (self.selected + 1).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
key_pat!(KeyCode::PageUp) => {
self.selected = self.selected.saturating_sub(8);
Ok(Some(true))
},
key_pat!(KeyCode::PageDown) => {
self.selected = (self.selected + 10).min(match &self.plugin {
Some(PluginKind::LV2(LV2Plugin { port_list, .. })) => port_list.len() - 1,
_ => unimplemented!()
});
Ok(Some(true))
},
key_pat!(KeyCode::Char(',')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value - 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
key_pat!(KeyCode::Char('.')) => {
match self.plugin.as_mut() {
Some(PluginKind::LV2(LV2Plugin { port_list, ref mut instance, .. })) => {
let index = port_list[self.selected].index;
if let Some(value) = instance.control_input(index) {
instance.set_control_input(index, value + 0.01);
}
},
_ => {}
}
Ok(Some(true))
},
key_pat!(KeyCode::Char('g')) => {
match self.plugin {
//Some(PluginKind::LV2(ref mut plugin)) => {
//plugin.ui_thread = Some(run_lv2_ui(LV2PluginUI::new()?)?);
//},
Some(_) => unreachable!(),
None => {}
}
Ok(Some(true))
},
_ => Ok(None)
}
});

40
src/plugin/lv2.rs Normal file
View file

@ -0,0 +1,40 @@
use crate::*;
/// A LV2 plugin.
#[derive(Debug)]
pub struct LV2Plugin {
pub world: livi::World,
pub instance: livi::Instance,
pub plugin: livi::Plugin,
pub features: Arc<livi::Features>,
pub port_list: Vec<livi::Port>,
pub input_buffer: Vec<livi::event::LV2AtomSequence>,
pub ui_thread: Option<JoinHandle<()>>,
}
impl LV2Plugin {
const INPUT_BUFFER: usize = 1024;
pub fn new (uri: &str) -> Usually<Self> {
let world = livi::World::with_load_bundle(&uri);
let features = world
.build_features(livi::FeaturesBuilder {
min_block_length: 1,
max_block_length: 65536,
});
let plugin = world.iter_plugins().nth(0)
.unwrap_or_else(||panic!("plugin not found: {uri}"));
Ok(Self {
instance: unsafe {
plugin
.instantiate(features.clone(), 48000.0)
.expect(&format!("instantiate failed: {uri}"))
},
port_list: plugin.ports().collect::<Vec<_>>(),
input_buffer: Vec::with_capacity(Self::INPUT_BUFFER),
ui_thread: None,
world,
features,
plugin,
})
}
}

59
src/plugin/lv2_gui.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::*;
use std::thread::{spawn, JoinHandle};
use ::winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
window::{Window, WindowId},
platform::x11::EventLoopBuilderExtX11
};
//pub struct LV2PluginUI {
//write: (),
//controller: (),
//widget: (),
//features: (),
//transfer: (),
//}
pub fn run_lv2_ui (mut ui: LV2PluginUI) -> Usually<JoinHandle<()>> {
Ok(spawn(move||{
let event_loop = EventLoop::builder().with_x11().with_any_thread(true).build().unwrap();
event_loop.set_control_flow(ControlFlow::Wait);
event_loop.run_app(&mut ui).unwrap()
}))
}
/// A LV2 plugin's X11 UI.
pub struct LV2PluginUI {
pub window: Option<Window>
}
impl LV2PluginUI {
pub fn new () -> Usually<Self> {
Ok(Self { window: None })
}
}
impl ApplicationHandler for LV2PluginUI {
fn resumed (&mut self, event_loop: &ActiveEventLoop) {
self.window = Some(event_loop.create_window(Window::default_attributes()).unwrap());
}
fn window_event (&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
match event {
WindowEvent::CloseRequested => {
self.window.as_ref().unwrap().set_visible(false);
event_loop.exit();
},
WindowEvent::RedrawRequested => {
self.window.as_ref().unwrap().request_redraw();
}
_ => (),
}
}
}
fn lv2_ui_instantiate (kind: &str) {
//let host = Suil
}

47
src/plugin/lv2_tui.rs Normal file
View file

@ -0,0 +1,47 @@
use super::*;
use ::livi::{
World,
Instance,
Plugin as LiviPlugin,
Features,
FeaturesBuilder,
Port,
event::LV2AtomSequence,
};
use std::thread::JoinHandle;
/// A LV2 plugin.
pub struct LV2Plugin {
pub world: World,
pub instance: Instance,
pub plugin: LiviPlugin,
pub features: Arc<Features>,
pub port_list: Vec<Port>,
pub input_buffer: Vec<LV2AtomSequence>,
pub ui_thread: Option<JoinHandle<()>>,
}
impl LV2Plugin {
const INPUT_BUFFER: usize = 1024;
pub fn new (uri: &str) -> Usually<Self> {
// Get 1st plugin at URI
let world = World::with_load_bundle(&uri);
let features = FeaturesBuilder { min_block_length: 1, max_block_length: 65536 };
let features = world.build_features(features);
let mut plugin = None;
if let Some(p) = world.iter_plugins().next() { plugin = Some(p); }
let plugin = plugin.expect("plugin not found");
let err = &format!("init {uri}");
let instance = unsafe { plugin.instantiate(features.clone(), 48000.0).expect(&err) };
let mut port_list = vec![];
for port in plugin.ports() {
port_list.push(port);
}
let input_buffer = Vec::with_capacity(Self::INPUT_BUFFER);
// Instantiate
Ok(Self {
world, instance, port_list, plugin, features, input_buffer, ui_thread: None
})
}
}

14
src/plugin/vst2_tui.rs Normal file
View file

@ -0,0 +1,14 @@
use crate::*;
impl<E: Engine> ::vst::host::Host for Plugin<E> {}
fn set_vst_plugin <E: Engine> (host: &Arc<Mutex<Plugin<E>>>, _path: &str) -> Usually<PluginKind> {
let mut loader = ::vst::host::PluginLoader::load(
&std::path::Path::new("/nix/store/ij3sz7nqg5l7v2dygdvzy3w6cj62bd6r-helm-0.9.0/lib/lxvst/helm.so"),
host.clone()
)?;
Ok(PluginKind::VST2 {
instance: loader.instance()?
})
}

2
src/plugin/vst3_tui.rs Normal file
View file

@ -0,0 +1,2 @@
//! TODO

317
src/pool.rs Normal file
View file

@ -0,0 +1,317 @@
use super::*;
pub mod phrase_length;
pub(crate) use phrase_length::*;
pub mod phrase_rename;
pub(crate) use phrase_rename::*;
pub mod phrase_selector;
pub(crate) use phrase_selector::*;
#[derive(Debug)]
pub struct PoolModel {
pub(crate) visible: bool,
/// Collection of phrases
pub(crate) phrases: Vec<Arc<RwLock<MidiClip>>>,
/// Selected phrase
pub(crate) phrase: AtomicUsize,
/// Mode switch
pub(crate) mode: Option<PoolMode>,
/// Rendered size
size: Measure<Tui>,
/// Scroll offset
scroll: usize,
}
/// Modes for phrase pool
#[derive(Debug, Clone)]
pub enum PoolMode {
/// Renaming a pattern
Rename(usize, String),
/// Editing the length of a pattern
Length(usize, usize, PhraseLengthFocus),
/// Load phrase from disk
Import(usize, FileBrowser),
/// Save phrase to disk
Export(usize, FileBrowser),
}
#[derive(Clone, PartialEq, Debug)]
pub enum PoolCommand {
Show(bool),
/// Update the contents of the phrase pool
Phrase(PhrasePoolCommand),
/// Select a phrase from the phrase pool
Select(usize),
/// Rename a phrase
Rename(PhraseRenameCommand),
/// Change the length of a phrase
Length(PhraseLengthCommand),
/// Import from file
Import(FileBrowserCommand),
/// Export to file
Export(FileBrowserCommand),
}
command!(|self:PoolCommand, state: PoolModel|{
use PoolCommand::*;
match self {
Show(visible) => {
state.visible = visible;
Some(Self::Show(!visible))
}
Rename(command) => match command {
PhraseRenameCommand::Begin => {
let length = state.phrases()[state.phrase_index()].read().unwrap().length;
*state.phrases_mode_mut() = Some(
PoolMode::Length(state.phrase_index(), length, PhraseLengthFocus::Bar)
);
None
},
_ => command.execute(state)?.map(Rename)
},
Length(command) => match command {
PhraseLengthCommand::Begin => {
let name = state.phrases()[state.phrase_index()].read().unwrap().name.clone();
*state.phrases_mode_mut() = Some(
PoolMode::Rename(state.phrase_index(), name)
);
None
},
_ => command.execute(state)?.map(Length)
},
Import(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PoolMode::Import(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Import)
},
Export(command) => match command {
FileBrowserCommand::Begin => {
*state.phrases_mode_mut() = Some(
PoolMode::Export(state.phrase_index(), FileBrowser::new(None)?)
);
None
},
_ => command.execute(state)?.map(Export)
},
Select(phrase) => {
state.set_phrase_index(phrase);
None
},
Phrase(command) => command.execute(state)?.map(Phrase),
}
});
input_to_command!(PoolCommand:<Tui>|state: PoolModel,input|match state.phrases_mode() {
Some(PoolMode::Rename(..)) => Self::Rename(PhraseRenameCommand::input_to_command(state, input)?),
Some(PoolMode::Length(..)) => Self::Length(PhraseLengthCommand::input_to_command(state, input)?),
Some(PoolMode::Import(..)) => Self::Import(FileBrowserCommand::input_to_command(state, input)?),
Some(PoolMode::Export(..)) => Self::Export(FileBrowserCommand::input_to_command(state, input)?),
_ => to_phrases_command(state, input)?
});
fn to_phrases_command (state: &PoolModel, input: &TuiInput) -> Option<PoolCommand> {
use KeyCode::{Up, Down, Delete, Char};
use PoolCommand as Cmd;
let index = state.phrase_index();
let count = state.phrases().len();
Some(match input.event() {
key_pat!(Char('n')) => Cmd::Rename(PhraseRenameCommand::Begin),
key_pat!(Char('t')) => Cmd::Length(PhraseLengthCommand::Begin),
key_pat!(Char('m')) => Cmd::Import(FileBrowserCommand::Begin),
key_pat!(Char('x')) => Cmd::Export(FileBrowserCommand::Begin),
key_pat!(Char('c')) => Cmd::Phrase(PhrasePoolCommand::SetColor(index, ItemColor::random())),
key_pat!(Char('[')) | key_pat!(Up) => Cmd::Select(
index.overflowing_sub(1).0.min(state.phrases().len() - 1)
),
key_pat!(Char(']')) | key_pat!(Down) => Cmd::Select(
index.saturating_add(1) % state.phrases().len()
),
key_pat!(Char('<')) => if index > 1 {
state.set_phrase_index(state.phrase_index().saturating_sub(1));
Cmd::Phrase(PhrasePoolCommand::Swap(index - 1, index))
} else {
return None
},
key_pat!(Char('>')) => if index < count.saturating_sub(1) {
state.set_phrase_index(state.phrase_index() + 1);
Cmd::Phrase(PhrasePoolCommand::Swap(index + 1, index))
} else {
return None
},
key_pat!(Delete) => if index > 0 {
state.set_phrase_index(index.min(count.saturating_sub(1)));
Cmd::Phrase(PhrasePoolCommand::Delete(index))
} else {
return None
},
key_pat!(Char('a')) | key_pat!(Shift-Char('A')) => Cmd::Phrase(PhrasePoolCommand::Add(count, MidiClip::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
))),
key_pat!(Char('i')) => Cmd::Phrase(PhrasePoolCommand::Add(index + 1, MidiClip::new(
String::from("(new)"), true, 4 * PPQ, None, Some(ItemPalette::random())
))),
key_pat!(Char('d')) | key_pat!(Shift-Char('D')) => {
let mut phrase = state.phrases()[index].read().unwrap().duplicate();
phrase.color = ItemPalette::random_near(phrase.color, 0.25);
Cmd::Phrase(PhrasePoolCommand::Add(index + 1, phrase))
},
_ => return None
})
}
impl Default for PoolModel {
fn default () -> Self {
Self {
visible: true,
phrases: vec![RwLock::new(MidiClip::default()).into()],
phrase: 0.into(),
scroll: 0,
mode: None,
size: Measure::new(),
}
}
}
from!(|phrase:&Arc<RwLock<MidiClip>>|PoolModel = {
let mut model = Self::default();
model.phrases.push(phrase.clone());
model.phrase.store(1, Relaxed);
model
});
has_phrases!(|self: PoolModel|self.phrases);
has_phrase!(|self: PoolModel|self.phrases[self.phrase_index()]);
impl PoolModel {
pub(crate) fn phrase_index (&self) -> usize {
self.phrase.load(Relaxed)
}
pub(crate) fn set_phrase_index (&self, value: usize) {
self.phrase.store(value, Relaxed);
}
pub(crate) fn phrases_mode (&self) -> &Option<PoolMode> {
&self.mode
}
pub(crate) fn phrases_mode_mut (&mut self) -> &mut Option<PoolMode> {
&mut self.mode
}
}
pub struct PoolView<'a>(pub(crate) &'a PoolModel);
// TODO: Display phrases always in order of appearance
render!(<Tui>|self: PoolView<'a>|{
let PoolModel { phrases, mode, .. } = self.0;
let bg = TuiTheme::g(32);
let title_color = TuiTheme::ti1();
let upper_left = "Pool:";
let upper_right = format!("({})", phrases.len());
let color = self.0.phrase().read().unwrap().color;
Tui::bg(bg, lay!(move|add|{
add(&Fill::wh(Outer(Style::default().fg(color.base.rgb).bg(bg))))?;
//add(&Lozenge(Style::default().bg(border_bg).fg(border_color)))?;
add(&Tui::inset_xy(0, 1, Fill::wh(col!(move|add|match mode {
Some(PoolMode::Import(_, ref file_picker)) => add(file_picker),
Some(PoolMode::Export(_, ref file_picker)) => add(file_picker),
_ => Ok(for (i, phrase) in phrases.iter().enumerate() {
add(&lay!(|add|{
let MidiClip { ref name, color, length, .. } = *phrase.read().unwrap();
let mut length = PhraseLength::new(length, None);
if let Some(PoolMode::Length(phrase, new_length, focus)) = mode {
if i == *phrase {
length.pulses = *new_length;
length.focus = Some(*focus);
}
}
add(&Tui::bg(color.base.rgb, Fill::w(col!([
Fill::w(lay!(|add|{
add(&Fill::w(Align::w(format!(" {i}"))))?;
add(&Fill::w(Align::e(Tui::pull_x(1, length.clone()))))
})),
Tui::bold(true, {
let mut row2 = format!(" {name}");
if let Some(PoolMode::Rename(phrase, _)) = mode {
if i == *phrase {
row2 = format!("{row2}");
}
};
row2
}),
]))))?;
if i == self.0.phrase_index() {
add(&CORNERS)?;
}
Ok(())
}))?;
})
}))))?;
add(&Fill::w(Align::nw(Tui::push_x(1, Tui::fg(title_color, upper_left.to_string())))))?;
add(&Fill::w(Align::ne(Tui::pull_x(1, Tui::fg(title_color, upper_right.to_string())))))?;
add(&self.0.size)
}))
});
command!(|self: FileBrowserCommand, state: PoolModel|{
use PoolMode::*;
use FileBrowserCommand::*;
let mode = &mut state.mode;
match mode {
Some(Import(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Import(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
Confirm => if browser.is_file() {
let index = *index;
let path = browser.path();
*mode = None;
PhrasePoolCommand::Import(index, path).execute(state)?;
} else if browser.is_dir() {
*mode = Some(Import(*index, browser.chdir()?));
},
_ => todo!(),
},
Some(Export(index, ref mut browser)) => match self {
Cancel => { *mode = None; },
Chdir(cwd) => { *mode = Some(Export(*index, FileBrowser::new(Some(cwd))?)); },
Select(index) => { browser.index = index; },
_ => unreachable!()
},
_ => unreachable!(),
};
None
});
input_to_command!(FileBrowserCommand:<Tui>|state: PoolModel,from|{
use FileBrowserCommand::*;
use KeyCode::{Up, Down, Left, Right, Enter, Esc, Backspace, Char};
if let Some(PoolMode::Import(_index, browser)) = &state.mode {
match from.event() {
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0
.min(browser.len().saturating_sub(1))),
key_pat!(Down) => Select(browser.index.saturating_add(1)
% browser.len()),
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Cancel,
_ => return None
}
} else if let Some(PoolMode::Export(_index, browser)) = &state.mode {
match from.event() {
key_pat!(Up) => Select(browser.index.overflowing_sub(1).0
.min(browser.len())),
key_pat!(Down) => Select(browser.index.saturating_add(1)
% browser.len()),
key_pat!(Right) => Chdir(browser.cwd.clone()),
key_pat!(Left) => Chdir(browser.cwd.clone()),
key_pat!(Enter) => Confirm,
key_pat!(Char(_)) => { todo!() },
key_pat!(Backspace) => { todo!() },
key_pat!(Esc) => Cancel,
_ => return None
}
} else {
unreachable!()
}
});

144
src/pool/phrase_length.rs Normal file
View file

@ -0,0 +1,144 @@
use crate::*;
use super::*;
use PhraseLengthFocus::*;
use PhraseLengthCommand::*;
use KeyCode::{Up, Down, Left, Right, Enter, Esc};
/// Displays and edits phrase length.
#[derive(Clone)]
pub struct PhraseLength {
/// Pulses per beat (quaver)
pub ppq: usize,
/// Beats per bar
pub bpb: usize,
/// Length of phrase in pulses
pub pulses: usize,
/// Selected subdivision
pub focus: Option<PhraseLengthFocus>,
}
impl PhraseLength {
pub fn new (pulses: usize, focus: Option<PhraseLengthFocus>) -> Self {
Self { ppq: PPQ, bpb: 4, pulses, focus }
}
pub fn bars (&self) -> usize {
self.pulses / (self.bpb * self.ppq)
}
pub fn beats (&self) -> usize {
(self.pulses % (self.bpb * self.ppq)) / self.ppq
}
pub fn ticks (&self) -> usize {
self.pulses % self.ppq
}
pub fn bars_string (&self) -> String {
format!("{}", self.bars())
}
pub fn beats_string (&self) -> String {
format!("{}", self.beats())
}
pub fn ticks_string (&self) -> String {
format!("{:>02}", self.ticks())
}
}
/// Focused field of `PhraseLength`
#[derive(Copy, Clone, Debug)]
pub enum PhraseLengthFocus {
/// Editing the number of bars
Bar,
/// Editing the number of beats
Beat,
/// Editing the number of ticks
Tick,
}
impl PhraseLengthFocus {
pub fn next (&mut self) {
*self = match self {
Self::Bar => Self::Beat,
Self::Beat => Self::Tick,
Self::Tick => Self::Bar,
}
}
pub fn prev (&mut self) {
*self = match self {
Self::Bar => Self::Tick,
Self::Beat => Self::Bar,
Self::Tick => Self::Beat,
}
}
}
render!(<Tui>|self: PhraseLength|{
let bars = ||self.bars_string();
let beats = ||self.beats_string();
let ticks = ||self.ticks_string();
row!(move|add|match self.focus {
None =>
add(&row!([" ", bars(), ".", beats(), ".", ticks()])),
Some(PhraseLengthFocus::Bar) =>
add(&row!(["[", bars(), "]", beats(), ".", ticks()])),
Some(PhraseLengthFocus::Beat) =>
add(&row!([" ", bars(), "[", beats(), "]", ticks()])),
Some(PhraseLengthFocus::Tick) =>
add(&row!([" ", bars(), ".", beats(), "[", ticks()])),
})
});
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PhraseLengthCommand {
Begin,
Cancel,
Set(usize),
Next,
Prev,
Inc,
Dec,
}
command!(|self:PhraseLengthCommand,state:PoolModel|{
match state.phrases_mode_mut().clone() {
Some(PoolMode::Length(phrase, ref mut length, ref mut focus)) => match self {
Cancel => { *state.phrases_mode_mut() = None; },
Prev => { focus.prev() },
Next => { focus.next() },
Inc => match focus {
Bar => { *length += 4 * PPQ },
Beat => { *length += PPQ },
Tick => { *length += 1 },
},
Dec => match focus {
Bar => { *length = length.saturating_sub(4 * PPQ) },
Beat => { *length = length.saturating_sub(PPQ) },
Tick => { *length = length.saturating_sub(1) },
},
Set(length) => {
let mut phrase = state.phrases()[phrase].write().unwrap();
let old_length = phrase.length;
phrase.length = length;
std::mem::drop(phrase);
*state.phrases_mode_mut() = None;
return Ok(Some(Self::Set(old_length)))
},
_ => unreachable!()
},
_ => unreachable!()
};
None
});
input_to_command!(PhraseLengthCommand:<Tui>|state:PoolModel,from|{
if let Some(PoolMode::Length(_, length, _)) = state.phrases_mode() {
match from.event() {
key_pat!(Up) => Self::Inc,
key_pat!(Down) => Self::Dec,
key_pat!(Right) => Self::Next,
key_pat!(Left) => Self::Prev,
key_pat!(Enter) => Self::Set(*length),
key_pat!(Esc) => Self::Cancel,
_ => return None
}
} else {
unreachable!()
}
});

60
src/pool/phrase_rename.rs Normal file
View file

@ -0,0 +1,60 @@
use crate::*;
use super::*;
#[derive(Clone, Debug, PartialEq)]
pub enum PhraseRenameCommand {
Begin,
Cancel,
Confirm,
Set(String),
}
impl Command<PoolModel> for PhraseRenameCommand {
fn execute (self, state: &mut PoolModel) -> Perhaps<Self> {
use PhraseRenameCommand::*;
match state.phrases_mode_mut().clone() {
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())))
},
Confirm => {
let old_name = old_name.clone();
*state.phrases_mode_mut() = None;
return Ok(Some(Self::Set(old_name)))
},
Cancel => {
state.phrases()[phrase].write().unwrap().name = old_name.clone();
},
_ => unreachable!()
},
_ => unreachable!()
};
Ok(None)
}
}
impl InputToCommand<Tui, PoolModel> for PhraseRenameCommand {
fn input_to_command (state: &PoolModel, from: &TuiInput) -> Option<Self> {
use KeyCode::{Char, Backspace, Enter, Esc};
if let Some(PoolMode::Rename(_, ref old_name)) = state.phrases_mode() {
Some(match from.event() {
key_pat!(Char(c)) => {
let mut new_name = old_name.clone();
new_name.push(*c);
Self::Set(new_name)
},
key_pat!(Backspace) => {
let mut new_name = old_name.clone();
new_name.pop();
Self::Set(new_name)
},
key_pat!(Enter) => Self::Confirm,
key_pat!(Esc) => Self::Cancel,
_ => return None
})
} else {
unreachable!()
}
}
}

View file

@ -0,0 +1,65 @@
use crate::*;
pub struct PhraseSelector {
pub(crate) title: &'static str,
pub(crate) name: String,
pub(crate) color: ItemPalette,
pub(crate) time: String,
}
// TODO: Display phrases always in order of appearance
render!(<Tui>|self: PhraseSelector|Fixed::wh(24, 1, row!([
Tui::fg(self.color.lightest.rgb, Tui::bold(true, &self.title)),
Tui::fg_bg(self.color.lighter.rgb, self.color.base.rgb, row!([
format!("{:8}", &self.name[0..8.min(self.name.len())]),
Tui::bg(self.color.dark.rgb, &self.time),
])),
])));
impl PhraseSelector {
// beats elapsed
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)
} else {
("".to_string(), ItemPalette::from(TuiTheme::g(64)))
};
let time = if let Some(elapsed) = state.pulses_since_start_looped() {
format!("+{:>}", state.clock().timebase.format_beats_0(elapsed))
} else {
String::from(" ")
};
Self { title: "Now:|", time, name, color, }
}
// beats until switchover
pub fn next_phrase <T: HasPlayPhrase> (state: &T) -> Self {
let (time, name, color) = if let Some((t, Some(phrase))) = state.next_phrase() {
let MidiClip { ref name, color, .. } = *phrase.read().unwrap();
let time = {
let target = t.pulse.get();
let current = state.clock().playhead.pulse.get();
if target > current {
let remaining = target - current;
format!("-{:>}", state.clock().timebase.format_beats_0(remaining))
} else {
String::new()
}
};
(time, name.clone(), color)
} else if let Some((_, Some(phrase))) = state.play_phrase() {
let phrase = phrase.read().unwrap();
if phrase.looped {
(" ".into(), phrase.name.clone(), phrase.color)
} else {
(" ".into(), " ".into(), TuiTheme::g(64).into())
}
} else {
(" ".into(), " ".into(), TuiTheme::g(64).into())
};
Self { title: " Next|", time, name, color, }
}
}

233
src/sampler.rs Normal file
View file

@ -0,0 +1,233 @@
use crate::*;
use KeyCode::Char;
use std::fs::File;
use symphonia::{
core::{
formats::Packet,
codecs::{Decoder, CODEC_TYPE_NULL},
errors::Error,
io::MediaSourceStream,
probe::Hint,
audio::SampleBuffer,
},
default::get_codecs,
};
pub mod sample;
pub(crate) use self::sample::*;
pub use self::sample::Sample;
pub mod voice;
pub(crate) use self::voice::*;
pub use self::voice::Voice;
pub mod sampler_tui;
pub(crate) use self::sampler_tui::*;
pub use self::sampler_tui::SamplerTui;
pub mod sample_import;
pub(crate) use self::sample_import::*;
pub mod sample_viewer;
pub(crate) use self::sample_viewer::*;
pub use self::sample_viewer::SampleViewer;
/// The sampler plugin plays sounds.
#[derive(Debug)]
pub struct Sampler {
pub jack: Arc<RwLock<JackClient>>,
pub name: String,
pub mapped: [Option<Arc<RwLock<Sample>>>;128],
pub recording: Option<(usize, Arc<RwLock<Sample>>)>,
pub unmapped: Vec<Arc<RwLock<Sample>>>,
pub voices: Arc<RwLock<Vec<Voice>>>,
pub midi_in: Port<MidiIn>,
pub audio_ins: Vec<Port<AudioIn>>,
pub input_meter: Vec<f32>,
pub audio_outs: Vec<Port<AudioOut>>,
pub buffer: Vec<Vec<f32>>,
pub output_gain: f32
}
impl Sampler {
pub fn new (jack: &Arc<RwLock<JackClient>>, name: &str) -> Usually<Self> {
Ok(Self {
midi_in: jack.midi_in(&format!("M/{name}"))?,
audio_ins: vec![
jack.audio_in(&format!("L/{name}"))?,
jack.audio_in(&format!("R/{name}"))?
],
input_meter: vec![0.0;2],
audio_outs: vec![
jack.audio_out(&format!("{name}/L"))?,
jack.audio_out(&format!("{name}/R"))?,
],
jack: jack.clone(),
name: name.into(),
mapped: [const { None };128],
unmapped: vec![],
voices: Arc::new(RwLock::new(vec![])),
buffer: vec![vec![0.0;16384];2],
output_gain: 1.,
recording: None,
})
}
pub fn cancel_recording (&mut self) {
self.recording = None;
}
pub fn begin_recording (&mut self, index: usize) {
self.recording = Some((
index,
Arc::new(RwLock::new(Sample::new("(new)", 0, 0, vec![vec![];self.audio_ins.len()])))
));
}
pub fn finish_recording (&mut self) -> Option<Arc<RwLock<Sample>>> {
let recording = self.recording.take();
if let Some((index, sample)) = recording {
let old = self.mapped[index].clone();
self.mapped[index] = Some(sample);
old
} else {
None
}
}
}
pub enum SamplerCommand {
RecordBegin(u7),
RecordCancel,
RecordFinish,
SetSample(u7, Option<Arc<RwLock<Sample>>>),
SetStart(u7, usize),
SetGain(f32),
NoteOn(u7, u7),
NoteOff(u7),
}
command!(|self: SamplerCommand, state: Sampler|match self {
Self::SetSample(index, sample) => {
let i = index.as_int() as usize;
let old = state.mapped[i].clone();
state.mapped[i] = sample;
Some(Self::SetSample(index, old))
},
Self::RecordBegin(index) => {
state.begin_recording(index.as_int() as usize);
None
},
Self::RecordCancel => {
state.cancel_recording();
None
},
Self::RecordFinish => {
state.finish_recording();
None
},
_ => todo!()
});
audio!(|self: SamplerTui, client, scope|{
SamplerAudio(&mut self.state).process(client, scope)
});
pub struct SamplerAudio<'a>(pub &'a mut Sampler);
audio!(|self: SamplerAudio<'a>, _client, scope|{
self.0.process_midi_in(scope);
self.0.clear_output_buffer();
self.0.process_audio_out(scope);
self.0.write_output_buffer(scope);
self.0.process_audio_in(scope);
Control::Continue
});
impl Sampler {
pub fn process_audio_in (&mut self, scope: &ProcessScope) {
let Sampler { audio_ins, input_meter, recording, .. } = self;
if audio_ins.len() != input_meter.len() {
*input_meter = vec![0.0;audio_ins.len()];
}
if let Some((_, sample)) = recording {
let mut sample = sample.write().unwrap();
if sample.channels.len() != audio_ins.len() {
panic!("channel count mismatch");
}
let iterator = audio_ins.iter().zip(input_meter).zip(sample.channels.iter_mut());
let mut length = 0;
for ((input, meter), channel) in iterator {
let slice = input.as_slice(scope);
length = length.max(slice.len());
let total: f32 = slice.iter().map(|x|x.abs()).sum();
let count = slice.len() as f32;
*meter = 10. * (total / count).log10();
channel.extend_from_slice(slice);
}
sample.end += length;
} else {
for (input, meter) in audio_ins.iter().zip(input_meter) {
let slice = input.as_slice(scope);
let total: f32 = slice.iter().map(|x|x.abs()).sum();
let count = slice.len() as f32;
*meter = 10. * (total / count).log10();
}
}
}
/// Create [Voice]s from [Sample]s in response to MIDI input.
pub fn process_midi_in (&mut self, scope: &ProcessScope) {
let Sampler { midi_in, mapped, voices, .. } = self;
for RawMidi { time, bytes } in midi_in.iter(scope) {
if let LiveEvent::Midi { message, .. } = LiveEvent::parse(bytes).unwrap() {
match message {
MidiMessage::NoteOn { ref key, ref vel } => {
if let Some(ref sample) = mapped[key.as_int() as usize] {
voices.write().unwrap().push(Sample::play(sample, time as usize, vel));
}
},
_ => {}
}
}
}
}
/// Zero the output buffer.
pub fn clear_output_buffer (&mut self) {
for buffer in self.buffer.iter_mut() {
buffer.fill(0.0);
}
}
/// Mix all currently playing samples into the output.
pub fn process_audio_out (&mut self, scope: &ProcessScope) {
let Sampler { ref mut buffer, voices, output_gain, .. } = self;
let channel_count = buffer.len();
voices.write().unwrap().retain_mut(|voice|{
for index in 0..scope.n_frames() as usize {
if let Some(frame) = voice.next() {
for (channel, sample) in frame.iter().enumerate() {
// Averaging mixer:
//self.buffer[channel % channel_count][index] = (
//(self.buffer[channel % channel_count][index] + sample * self.output_gain) / 2.0
//);
buffer[channel % channel_count][index] += sample * *output_gain;
}
} else {
return false
}
}
true
});
}
/// Write output buffer to output ports.
pub fn write_output_buffer (&mut self, scope: &ProcessScope) {
let Sampler { ref mut audio_outs, buffer, .. } = self;
for (i, port) in audio_outs.iter_mut().enumerate() {
let buffer = &buffer[i];
for (i, value) in port.as_mut_slice(scope).iter_mut().enumerate() {
*value = *buffer.get(i).unwrap_or(&0.0);
}
}
}
}

121
src/sampler/sample.rs Normal file
View file

@ -0,0 +1,121 @@
use crate::*;
use super::*;
/// A sound sample.
#[derive(Default, Debug)]
pub struct Sample {
pub name: String,
pub start: usize,
pub end: usize,
pub channels: Vec<Vec<f32>>,
pub rate: Option<usize>,
pub gain: f32,
}
/// Load sample from WAV and assign to MIDI note.
#[macro_export] macro_rules! sample {
($note:expr, $name:expr, $src:expr) => {{
let (end, data) = read_sample_data($src)?;
(
u7::from_int_lossy($note).into(),
Sample::new($name, 0, end, data).into()
)
}};
}
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 play (sample: &Arc<RwLock<Self>>, after: usize, velocity: &u7) -> Voice {
Voice {
sample: sample.clone(),
after,
position: sample.read().unwrap().start,
velocity: velocity.as_int() as f32 / 127.0,
}
}
/// Read WAV from file
pub fn read_data (src: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
let mut channels: Vec<wavers::Samples<f32>> = vec![];
for channel in wavers::Wav::from_path(src)?.channels() {
channels.push(channel);
}
let mut end = 0;
let mut data: Vec<Vec<f32>> = vec![];
for samples in channels.iter() {
let channel = Vec::from(samples.as_ref());
end = end.max(channel.len());
data.push(channel);
}
Ok((end, data))
}
pub fn from_file (path: &PathBuf) -> Usually<Self> {
let name = path.file_name().unwrap().to_string_lossy().into();
let mut sample = Self { name, ..Default::default() };
// Use file extension if present
let mut hint = Hint::new();
if let Some(ext) = path.extension() {
hint.with_extension(&ext.to_string_lossy());
}
let probed = symphonia::default::get_probe().format(
&hint,
MediaSourceStream::new(
Box::new(File::open(path)?),
Default::default(),
),
&Default::default(),
&Default::default()
)?;
let mut format = probed.format;
let params = &format.tracks().iter()
.find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
.expect("no tracks found")
.codec_params;
let mut decoder = get_codecs().make(params, &Default::default())?;
loop {
match format.next_packet() {
Ok(packet) => sample.decode_packet(&mut decoder, packet)?,
Err(symphonia::core::errors::Error::IoError(_)) => break decoder.last_decoded(),
Err(err) => return Err(err.into()),
};
};
sample.end = sample.channels.iter().fold(0, |l, c|l + c.len());
Ok(sample)
}
fn decode_packet (
&mut self, decoder: &mut Box<dyn Decoder>, packet: Packet
) -> Usually<()> {
// Decode a packet
let decoded = decoder
.decode(&packet)
.map_err(|e|Box::<dyn crate::Error>::from(e))?;
// Determine sample rate
let spec = *decoded.spec();
if let Some(rate) = self.rate {
if rate != spec.rate as usize {
panic!("sample rate changed");
}
} else {
self.rate = Some(spec.rate as usize);
}
// Determine channel count
while self.channels.len() < spec.channels.count() {
self.channels.push(vec![]);
}
// Load sample
let mut samples = SampleBuffer::new(
decoded.frames() as u64,
spec
);
if samples.capacity() > 0 {
samples.copy_interleaved_ref(decoded);
for frame in samples.samples().chunks(spec.channels.count()) {
for (chan, frame) in frame.iter().enumerate() {
self.channels[chan].push(*frame)
}
}
}
Ok(())
}
}

View file

@ -0,0 +1,246 @@
use crate::*;
use super::*;
input_to_command!(FileBrowserCommand:<Tui>|state:SamplerTui,input|match input {
_ => return None
});
command!(|self:FileBrowserCommand,state:SamplerTui|match self {
_ => todo!()
});
pub struct AddSampleModal {
exited: bool,
dir: PathBuf,
subdirs: Vec<OsString>,
files: Vec<OsString>,
cursor: usize,
offset: usize,
sample: Arc<RwLock<Sample>>,
voices: Arc<RwLock<Vec<Voice>>>,
_search: Option<String>,
}
impl Exit for AddSampleModal {
fn exited (&self) -> bool {
self.exited
}
fn exit (&mut self) {
self.exited = true
}
}
impl AddSampleModal {
pub fn new (
sample: &Arc<RwLock<Sample>>,
voices: &Arc<RwLock<Vec<Voice>>>
) -> Usually<Self> {
let dir = std::env::current_dir()?;
let (subdirs, files) = scan(&dir)?;
Ok(Self {
exited: false,
dir,
subdirs,
files,
cursor: 0,
offset: 0,
sample: sample.clone(),
voices: voices.clone(),
_search: None
})
}
fn rescan (&mut self) -> Usually<()> {
scan(&self.dir).map(|(subdirs, files)|{
self.subdirs = subdirs;
self.files = files;
})
}
fn prev (&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
fn next (&mut self) {
self.cursor = self.cursor + 1;
}
fn try_preview (&mut self) -> Usually<()> {
if let Some(path) = self.cursor_file() {
if let Ok(sample) = Sample::from_file(&path) {
*self.sample.write().unwrap() = sample;
self.voices.write().unwrap().push(
Sample::play(&self.sample, 0, &u7::from(100u8))
);
}
//load_sample(&path)?;
//let src = std::fs::File::open(&path)?;
//let mss = MediaSourceStream::new(Box::new(src), Default::default());
//let mut hint = Hint::new();
//if let Some(ext) = path.extension() {
//hint.with_extension(&ext.to_string_lossy());
//}
//let meta_opts: MetadataOptions = Default::default();
//let fmt_opts: FormatOptions = Default::default();
//if let Ok(mut probed) = symphonia::default::get_probe()
//.format(&hint, mss, &fmt_opts, &meta_opts)
//{
//panic!("{:?}", probed.format.metadata());
//};
}
Ok(())
}
fn cursor_dir (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
Some(self.dir.join(&self.subdirs[self.cursor]))
} else {
None
}
}
fn cursor_file (&self) -> Option<PathBuf> {
if self.cursor < self.subdirs.len() {
return None
}
let index = self.cursor.saturating_sub(self.subdirs.len());
if index < self.files.len() {
Some(self.dir.join(&self.files[index]))
} else {
None
}
}
fn pick (&mut self) -> Usually<bool> {
if self.cursor == 0 {
if let Some(parent) = self.dir.parent() {
self.dir = parent.into();
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
}
if let Some(dir) = self.cursor_dir() {
self.dir = dir;
self.rescan()?;
self.cursor = 0;
return Ok(false)
}
if let Some(path) = self.cursor_file() {
let (end, channels) = read_sample_data(&path.to_string_lossy())?;
let mut sample = self.sample.write().unwrap();
sample.name = path.file_name().unwrap().to_string_lossy().into();
sample.end = end;
sample.channels = channels;
return Ok(true)
}
return Ok(false)
}
}
fn read_sample_data (_: &str) -> Usually<(usize, Vec<Vec<f32>>)> {
todo!();
}
fn scan (dir: &PathBuf) -> Usually<(Vec<OsString>, Vec<OsString>)> {
let (mut subdirs, mut files) = std::fs::read_dir(dir)?
.fold((vec!["..".into()], vec![]), |(mut subdirs, mut files), entry|{
let entry = entry.expect("failed to read drectory entry");
let meta = entry.metadata().expect("failed to read entry metadata");
if meta.is_file() {
files.push(entry.file_name());
} else if meta.is_dir() {
subdirs.push(entry.file_name());
}
(subdirs, files)
});
subdirs.sort();
files.sort();
Ok((subdirs, files))
}
fn draw_sample (
to: &mut TuiOutput, x: u16, y: u16, note: Option<&u7>, sample: &Sample, focus: bool
) -> Usually<usize> {
let style = if focus { Style::default().green() } else { Style::default() };
if focus {
to.blit(&"🬴", x+1, y, Some(style.bold()));
}
let label1 = format!("{:3} {:12}",
note.map(|n|n.to_string()).unwrap_or(String::default()),
sample.name);
let label2 = format!("{:>6} {:>6} +0.0",
sample.start,
sample.end);
to.blit(&label1, x+2, y, Some(style.bold()));
to.blit(&label2, x+3+label1.len()as u16, y, Some(style));
Ok(label1.len() + label2.len() + 4)
}
impl Render<Tui> for AddSampleModal {
fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
todo!()
//Align::Center(()).layout(to)
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
todo!()
//let area = to.area();
//to.make_dim();
//let area = center_box(
//area,
//64.max(area.w().saturating_sub(8)),
//20.max(area.w().saturating_sub(8)),
//);
//to.fill_fg(area, Color::Reset);
//to.fill_bg(area, Nord::bg_lo(true, true));
//to.fill_char(area, ' ');
//to.blit(&format!("{}", &self.dir.to_string_lossy()), area.x()+2, area.y()+1, Some(Style::default().bold()))?;
//to.blit(&"Select sample:", area.x()+2, area.y()+2, Some(Style::default().bold()))?;
//for (i, (is_dir, name)) in self.subdirs.iter()
//.map(|path|(true, path))
//.chain(self.files.iter().map(|path|(false, path)))
//.enumerate()
//.skip(self.offset)
//{
//if i >= area.h() as usize - 4 {
//break
//}
//let t = if is_dir { "" } else { "" };
//let line = format!("{t} {}", name.to_string_lossy());
//let line = &line[..line.len().min(area.w() as usize - 4)];
//to.blit(&line, area.x() + 2, area.y() + 3 + i as u16, Some(if i == self.cursor {
//Style::default().green()
//} else {
//Style::default().white()
//}))?;
//}
//Lozenge(Style::default()).draw(to)
}
}
//impl Handle<Tui> for AddSampleModal {
//fn handle (&mut self, from: &TuiInput) -> Perhaps<bool> {
//if from.handle_keymap(self, KEYMAP_ADD_SAMPLE)? {
//return Ok(Some(true))
//}
//Ok(Some(true))
//}
//}
//pub const KEYMAP_ADD_SAMPLE: &'static [KeyBinding<AddSampleModal>] = keymap!(AddSampleModal {
//[Esc, NONE, "sampler/add/close", "close help dialog", |modal: &mut AddSampleModal|{
//modal.exit();
//Ok(true)
//}],
//[Up, NONE, "sampler/add/prev", "select previous entry", |modal: &mut AddSampleModal|{
//modal.prev();
//Ok(true)
//}],
//[Down, NONE, "sampler/add/next", "select next entry", |modal: &mut AddSampleModal|{
//modal.next();
//Ok(true)
//}],
//[Enter, NONE, "sampler/add/enter", "activate selected entry", |modal: &mut AddSampleModal|{
//if modal.pick()? {
//modal.exit();
//}
//Ok(true)
//}],
//[Char('p'), NONE, "sampler/add/preview", "preview selected entry", |modal: &mut AddSampleModal|{
//modal.try_preview()?;
//Ok(true)
//}]
//});

View file

@ -0,0 +1,52 @@
use crate::*;
use std::ops::Deref;
use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Points, Line}}};
const EMPTY: &[(f64, f64)] = &[(0., 0.), (1., 1.), (2., 2.), (0., 2.), (2., 0.)];
pub struct SampleViewer(pub Option<Arc<RwLock<Sample>>>);
render!(<Tui>|self: SampleViewer|render(|to|{
let [x, y, width, height] = to.area();
let area = Rect { x, y, width, height };
let min_db = -40.0;
let (x_bounds, y_bounds, lines): ([f64;2], [f64;2], Vec<Line>) =
if let Some(sample) = &self.0 {
let sample = sample.read().unwrap();
let start = sample.start as f64;
let end = sample.end as f64;
let length = end - start;
let step = length / width as f64;
let mut t = start;
let mut lines = vec![];
while t < end {
let chunk = &sample.channels[0][t as usize..((t + step) as usize).min(sample.end)];
let total: f32 = chunk.iter().map(|x|x.abs()).sum();
let count = chunk.len() as f32;
let meter = 10. * (total / count).log10();
let x = t as f64;
let y = meter as f64;
lines.push(Line::new(x, min_db, x, y, Color::Green));
t += step / 2.;
}
(
[sample.start as f64, sample.end as f64],
[min_db, 0.],
lines
)
} else {
(
[0.0, width as f64],
[0.0, height as f64],
vec![
Line::new(0.0, 0.0, width as f64, height as f64, Color::Red),
Line::new(width as f64, 0.0, 0.0, height as f64, Color::Red),
]
)
};
Canvas::default().x_bounds(x_bounds).y_bounds(y_bounds).paint(|ctx|{
for line in lines.iter() {
ctx.draw(line)
}
}).render(area, &mut to.buffer);
Ok(())
}));

155
src/sampler/sampler_tui.rs Normal file
View file

@ -0,0 +1,155 @@
use crate::*;
use KeyCode::Char;
pub struct SamplerTui {
pub state: Sampler,
pub cursor: (usize, usize),
pub editing: Option<Arc<RwLock<Sample>>>,
pub mode: Option<SamplerMode>,
/// Size of actual notes area
pub size: Measure<Tui>,
/// Lowest note displayed
pub note_lo: AtomicUsize,
pub note_pt: AtomicUsize,
color: ItemPalette
}
impl SamplerTui {
/// Immutable reference to sample at cursor.
pub fn sample (&self) -> Option<&Arc<RwLock<Sample>>> {
for (i, sample) in self.state.mapped.iter().enumerate() {
if i == self.cursor.0 {
return sample.as_ref()
}
}
for (i, sample) in self.state.unmapped.iter().enumerate() {
if i + self.state.mapped.len() == self.cursor.0 {
return Some(sample)
}
}
None
}
}
from_jack!(|jack|SamplerTui{
Self {
cursor: (0, 0),
editing: None,
mode: None,
size: Measure::new(),
note_lo: 36.into(),
note_pt: 36.into(),
color: ItemPalette::from(Color::Rgb(64, 128, 32)),
state: Sampler::new(jack, "sampler")?,
}
});
render!(<Tui>|self: SamplerTui|{
let keys_width = 5;
let keys = move||"";//SamplerKeys(self);
let fg = self.color.base.rgb;
let bg = self.color.darkest.rgb;
let border = Fill::wh(Outer(Style::default().fg(fg).bg(bg)));
let with_border = |x|lay!([border, Fill::wh(&x)]);
let with_size = |x|lay!([self.size, x]);
Tui::bg(bg, Fill::wh(with_border(Bsp::s(
Tui::fg(self.color.light.rgb, Tui::bold(true, "Sampler")),
with_size(Tui::shrink_y(1, Bsp::e(
Fixed::w(keys_width, keys()),
Fill::wh(render(|to: &mut TuiOutput|Ok({
let x = to.area.x();
let bg_base = self.color.darkest.rgb;
let bg_selected = self.color.darker.rgb;
let style_empty = Style::default().fg(self.color.base.rgb);
let style_full = Style::default().fg(self.color.lighter.rgb);
let note_hi = self.note_hi();
let note_pt = self.note_point();
for y in 0..self.size.h() {
let note = note_hi - y as usize;
let bg = if note == note_pt { bg_selected } else { bg_base };
let style = Some(style_empty.bg(bg));
to.blit(&" (no sample) ", x, to.area.y() + y as u16, style)
}
})))
))),
))))
});
impl NoteRange for SamplerTui {
fn note_lo (&self) -> &AtomicUsize { &self.note_lo }
fn note_axis (&self) -> &AtomicUsize { &self.size.y }
}
impl NotePoint for SamplerTui {
fn note_len (&self) -> usize {0/*TODO*/}
fn set_note_len (&self, x: usize) {}
fn note_point (&self) -> usize { self.note_pt.load(Relaxed) }
fn set_note_point (&self, x: usize) { self.note_pt.store(x, Relaxed); }
}
pub enum SamplerMode {
// Load sample from path
Import(usize, FileBrowser),
}
handle!(<Tui>|self: SamplerTui, input|SamplerTuiCommand::execute_with_state(self, input));
pub enum SamplerTuiCommand {
Import(FileBrowserCommand),
SelectNote(usize),
SelectField(usize),
Sample(SamplerCommand),
}
input_to_command!(SamplerTuiCommand: <Tui>|state: SamplerTui, input|match state.mode {
Some(SamplerMode::Import(..)) => Self::Import(
FileBrowserCommand::input_to_command(state, input)?
),
_ => match input.event() {
// load sample
key_pat!(Shift-Char('L')) => {
Self::Import(FileBrowserCommand::Begin)
},
key_pat!(KeyCode::Up) => {
Self::SelectNote(state.note_point().overflowing_add(1).0.min(127))
},
key_pat!(KeyCode::Down) => {
Self::SelectNote(state.note_point().overflowing_sub(1).0.min(127))
},
_ => return None
}
//key_pat!(KeyCode::Char('p')) => if let Some(sample) = self.sample() {
//voices.write().unwrap().push(Sample::play(sample, 0, &100.into()));
//},
//key_pat!(KeyCode::Char('a')) => {
//let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
//self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
//unmapped.push(sample);
//},
//key_pat!(KeyCode::Char('r')) => if let Some(sample) = self.sample() {
//self.mode = None;//Some(Exit::boxed(AddSampleModal::new(&sample, &voices)?));
//},
//key_pat!(KeyCode::Enter) => if let Some(sample) = self.sample() {
//self.editing = Some(sample.clone());
//},
//_ => {
//return Ok(None)
//}
//}
});
command!(|self: SamplerTuiCommand, state: SamplerTui|match self {
Self::Import(FileBrowserCommand::Begin) => {
let voices = &state.state.voices;
let sample = Arc::new(RwLock::new(Sample::new("", 0, 0, vec![])));
state.mode = Some(SamplerMode::Import(0, FileBrowser::new(None)?));
None
},
Self::SelectNote(index) => {
let old = state.note_point();
state.set_note_point(index);
Some(Self::SelectNote(old))
},
Self::Sample(cmd) => cmd.execute(&mut state.state)?.map(Self::Sample),
_ => todo!()
});

30
src/sampler/voice.rs Normal file
View file

@ -0,0 +1,30 @@
use crate::*;
/// A currently playing instance of a sample.
#[derive(Default, Debug, Clone)]
pub struct Voice {
pub sample: Arc<RwLock<Sample>>,
pub after: usize,
pub position: usize,
pub velocity: f32,
}
impl Iterator for Voice {
type Item = [f32;2];
fn next (&mut self) -> Option<Self::Item> {
if self.after > 0 {
self.after -= 1;
return Some([0.0, 0.0])
}
let sample = self.sample.read().unwrap();
if self.position < sample.end {
let position = self.position;
self.position += 1;
return sample.channels[0].get(position).map(|_amplitude|[
sample.channels[0][position] * self.velocity * sample.gain,
sample.channels[0][position] * self.velocity * sample.gain,
])
}
None
}
}

178
src/sequencer.rs Normal file
View file

@ -0,0 +1,178 @@
use crate::*;
use ClockCommand::{Play, Pause};
use KeyCode::{Tab, Char};
use SequencerCommand::*;
use PhraseCommand::*;
use PhrasePoolCommand::*;
/// Root view for standalone `tek_sequencer`.
pub struct SequencerTui {
_jack: Arc<RwLock<JackClient>>,
pub transport: bool,
pub selectors: bool,
pub clock: ClockModel,
pub phrases: PoolModel,
pub player: MidiPlayer,
pub editor: MidiEditorModel,
pub size: Measure<Tui>,
pub status: bool,
pub note_buf: Vec<u8>,
pub midi_buf: Vec<Vec<Vec<u8>>>,
pub perf: PerfModel,
}
from_jack!(|jack|SequencerTui {
let clock = ClockModel::from(jack);
let phrase = Arc::new(RwLock::new(MidiClip::new(
"New", true, 4 * clock.timebase.ppq.get() as usize,
None, Some(ItemColor::random().into())
)));
Self {
_jack: jack.clone(),
transport: true,
selectors: true,
phrases: PoolModel::from(&phrase),
editor: MidiEditorModel::from(&phrase),
player: MidiPlayer::from((&clock, &phrase)),
size: Measure::new(),
midi_buf: vec![vec![];65536],
note_buf: vec![],
perf: PerfModel::default(),
status: true,
clock,
}
});
render!(<Tui>|self: SequencerTui|{
let w = self.size.w();
let phrase_w = if w > 60 { 20 } else if w > 40 { 15 } else { 10 };
let pool_w = if self.phrases.visible { phrase_w } else { 0 };
let pool = Tui::pull_y(1, Fill::h(Align::e(PoolView(&self.phrases))));
let with_pool = move|x|Tui::split_w(false, pool_w, pool, x);
let status = SequencerStatus::from(self);
let with_status = |x|Tui::split_n(false, if self.status { 2 } else { 0 }, status, x);
let with_editbar = |x|Tui::split_n(false, 1, MidiEditStatus(&self.editor), x);
let with_size = |x|lay!([self.size, x]);
let editor = with_editbar(with_pool(Fill::wh(&self.editor)));
let color = self.player.play_phrase().as_ref().map(|(_,p)|
p.as_ref().map(|p|p.read().unwrap().color)
).flatten().clone();
let toolbar = row!([
Fixed::wh(5, 2, PlayPause(self.clock.is_rolling())),
Fixed::h(2, TransportView::from((self, color, true))),
]).when(self.transport);
let play_queue = row!([
PhraseSelector::play_phrase(&self.player),
PhraseSelector::next_phrase(&self.player),
]).when(self.selectors);;
Tui::min_y(15, with_size(with_status(col!([
toolbar,
play_queue,
editor,
]))))
});
audio!(|self:SequencerTui, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
if Control::Quit == ClockAudio(self).process(client, scope) {
return Control::Quit
}
// Update MIDI sequencer
if Control::Quit == PlayerAudio(
&mut self.player, &mut self.note_buf, &mut self.midi_buf
).process(client, scope) {
return Control::Quit
}
// End profiling cycle
self.perf.update(t0, scope);
Control::Continue
});
has_size!(<Tui>|self:SequencerTui|&self.size);
has_clock!(|self:SequencerTui|&self.clock);
has_phrases!(|self:SequencerTui|self.phrases.phrases);
has_editor!(|self:SequencerTui|self.editor);
handle!(<Tui>|self:SequencerTui,input|SequencerCommand::execute_with_state(self, input));
#[derive(Clone, Debug)] pub enum SequencerCommand {
History(isize),
Clock(ClockCommand),
Pool(PoolCommand),
Editor(PhraseCommand),
Enqueue(Option<Arc<RwLock<MidiClip>>>),
}
input_to_command!(SequencerCommand: <Tui>|state: SequencerTui, input|match input.event() {
// TODO: k: toggle on-screen keyboard
key_pat!(Ctrl-Char('k')) => { todo!("keyboard") },
// Transport: Play/pause
key_pat!(Char(' ')) => Clock(
if state.clock().is_stopped() { Play(None) } else { Pause(None) }
),
// Transport: Play from start or rewind to start
key_pat!(Shift-Char(' ')) => Clock(
if state.clock().is_stopped() { Play(Some(0)) } else { Pause(Some(0)) }
),
// u: undo
key_pat!(Char('u')) => History(-1),
// Shift-U: redo
key_pat!(Char('U')) => History( 1),
// Tab: Toggle visibility of phrase pool column
key_pat!(Tab) => Pool(PoolCommand::Show(!state.phrases.visible)),
// q: Enqueue currently edited phrase
key_pat!(Char('q')) => Enqueue(Some(state.phrases.phrase().clone())),
// 0: Enqueue phrase 0 (stop all)
key_pat!(Char('0')) => Enqueue(Some(state.phrases.phrases()[0].clone())),
// e: Toggle between editing currently playing or other phrase
key_pat!(Char('e')) => if let Some((_, Some(playing))) = state.player.play_phrase() {
let editing = state.editor.phrase().as_ref().map(|p|p.read().unwrap().clone());
let selected = state.phrases.phrase().clone();
Editor(Show(Some(if Some(selected.read().unwrap().clone()) != editing {
selected
} else {
playing.clone()
})))
} else {
return None
},
// For the rest, use the default keybindings of the components.
// The ones defined above supersede them.
_ => if let Some(command) = PhraseCommand::input_to_command(&state.editor, input) {
Editor(command)
} else if let Some(command) = PoolCommand::input_to_command(&state.phrases, input) {
Pool(command)
} else {
return None
}
});
command!(|self: SequencerCommand, state: SequencerTui|match self {
Self::Pool(cmd) => {
let mut default = |cmd: PoolCommand|cmd
.execute(&mut state.phrases)
.map(|x|x.map(Pool));
match cmd {
// autoselect: automatically load selected phrase in editor
PoolCommand::Select(_) => {
let undo = default(cmd)?;
state.editor.set_phrase(Some(state.phrases.phrase()));
undo
},
// update color in all places simultaneously
PoolCommand::Phrase(SetColor(index, _)) => {
let undo = default(cmd)?;
state.editor.set_phrase(Some(state.phrases.phrase()));
undo
},
_ => default(cmd)?
}
},
Self::Editor(cmd) => {
let default = ||cmd.execute(&mut state.editor).map(|x|x.map(Editor));
match cmd {
_ => default()?
}
},
Self::Clock(cmd) => cmd.execute(state)?.map(Clock),
Self::Enqueue(phrase) => {
state.player.enqueue_next(phrase.as_ref());
None
},
Self::History(delta) => {
todo!("undo/redo")
},
});

236
src/space.rs Normal file
View file

@ -0,0 +1,236 @@
use crate::*;
use std::ops::{Add, Sub, Mul, Div};
use std::fmt::{Display, Debug};
// TODO: return impl Point and impl Size instead of [N;x]
// to disambiguate between usage of 2-"tuple"s
//////////////////////////////////////////////////////
pub(crate) mod align;
pub(crate) mod bsp;
pub(crate) mod cond; pub(crate) use cond::*;
pub(crate) mod fill;
pub(crate) mod fixed; pub(crate) use fixed::*;
pub(crate) mod inset_outset; pub(crate) use inset_outset::*;
pub(crate) mod layers; pub(crate) use layers::*;
pub(crate) mod measure; pub(crate) use measure::*;
pub(crate) mod min_max; pub(crate) use min_max::*;
pub(crate) mod push_pull; pub(crate) use push_pull::*;
pub(crate) mod scroll;
pub(crate) mod shrink_grow; pub(crate) use shrink_grow::*;
pub(crate) mod split; pub(crate) use split::*;
pub(crate) mod stack; pub(crate) use stack::*;
pub use self::{
align::*,
bsp::*,
fill::*,
};
#[derive(Copy, Clone, PartialEq)]
pub enum Direction { North, South, West, East, }
impl Direction {
pub fn is_north (&self) -> bool { matches!(self, Self::North) }
pub fn is_south (&self) -> bool { matches!(self, Self::South) }
pub fn is_east (&self) -> bool { matches!(self, Self::West) }
pub fn is_west (&self) -> bool { matches!(self, Self::East) }
/// Return next direction clockwise
pub fn cw (&self) -> Self {
match self {
Self::North => Self::East,
Self::South => Self::West,
Self::West => Self::North,
Self::East => Self::South,
}
}
/// Return next direction counterclockwise
pub fn ccw (&self) -> Self {
match self {
Self::North => Self::West,
Self::South => Self::East,
Self::West => Self::South,
Self::East => Self::North,
}
}
}
/// Standard numeric type.
pub trait Coordinate: Send + Sync + Copy
+ Add<Self, Output=Self>
+ Sub<Self, Output=Self>
+ Mul<Self, Output=Self>
+ Div<Self, Output=Self>
+ Ord + PartialEq + Eq
+ Debug + Display + Default
+ From<u16> + Into<u16>
+ Into<usize>
+ Into<f64>
{
fn minus (self, other: Self) -> Self {
if self >= other {
self - other
} else {
0.into()
}
}
fn zero () -> Self {
0.into()
}
}
impl<T> Coordinate for T where T: Send + Sync + Copy
+ Add<Self, Output=Self>
+ Sub<Self, Output=Self>
+ Mul<Self, Output=Self>
+ Div<Self, Output=Self>
+ Ord + PartialEq + Eq
+ Debug + Display + Default
+ From<u16> + Into<u16>
+ Into<usize>
+ Into<f64>
{}
pub trait Size<N: Coordinate> {
fn x (&self) -> N;
fn y (&self) -> N;
#[inline] fn w (&self) -> N { self.x() }
#[inline] fn h (&self) -> N { self.y() }
#[inline] fn wh (&self) -> [N;2] { [self.x(), self.y()] }
#[inline] fn clip_w (&self, w: N) -> [N;2] { [self.w().min(w), self.h()] }
#[inline] fn clip_h (&self, h: N) -> [N;2] { [self.w(), self.h().min(h)] }
#[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
if self.w() < w || self.h() < h {
Err(format!("min {w}x{h}").into())
} else {
Ok(self)
}
}
}
impl<N: Coordinate> Size<N> for (N, N) {
fn x (&self) -> N { self.0 }
fn y (&self) -> N { self.1 }
}
impl<N: Coordinate> Size<N> for [N;2] {
fn x (&self) -> N { self[0] }
fn y (&self) -> N { self[1] }
}
pub trait Area<N: Coordinate>: Copy {
fn x (&self) -> N;
fn y (&self) -> N;
fn w (&self) -> N;
fn h (&self) -> N;
fn x2 (&self) -> N { self.x() + self.w() }
fn y2 (&self) -> N { self.y() + self.h() }
#[inline] fn wh (&self) -> [N;2] { [self.w(), self.h()] }
#[inline] fn xywh (&self) -> [N;4] { [self.x(), self.y(), self.w(), self.h()] }
#[inline] fn lrtb (&self) -> [N;4] { [self.x(), self.x2(), self.y(), self.y2()] }
#[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] }
#[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] }
#[inline] fn shrink_x (&self, x: N) -> [N;4] {
[self.x(), self.y(), self.w().minus(x), self.h()]
}
#[inline] fn shrink_y (&self, y: N) -> [N;4] {
[self.x(), self.y(), self.w(), self.h().minus(y)]
}
#[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] }
#[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] }
#[inline] fn clip_h (&self, h: N) -> [N;4] {
[self.x(), self.y(), self.w(), self.h().min(h)]
}
#[inline] fn clip_w (&self, w: N) -> [N;4] {
[self.x(), self.y(), self.w().min(w), self.h()]
}
#[inline] fn clip (&self, wh: impl Size<N>) -> [N;4] {
[self.x(), self.y(), wh.w(), wh.h()]
}
#[inline] fn expect_min (&self, w: N, h: N) -> Usually<&Self> {
if self.w() < w || self.h() < h {
Err(format!("min {w}x{h}").into())
} else {
Ok(self)
}
}
#[inline] fn split_fixed (&self, direction: Direction, a: N) -> ([N;4],[N;4]) {
match direction {
Direction::North => (
[self.x(), (self.y()+self.h()).minus(a), self.w(), a],
[self.x(), self.y(), self.w(), self.h().minus(a)],
),
Direction::South => (
[self.x(), self.y(), self.w(), a],
[self.x(), self.y() + a, self.w(), self.h().minus(a)],
),
Direction::East => (
[self.x(), self.y(), a, self.h()],
[self.x() + a, self.y(), self.w().minus(a), self.h()],
),
Direction::West => (
[self.x() + self.w() - a, self.y(), a, self.h()],
[self.x(), self.y(), self.w() - a, self.h()],
),
}
}
}
impl<N: Coordinate> Area<N> for (N, N, N, N) {
#[inline] fn x (&self) -> N { self.0 }
#[inline] fn y (&self) -> N { self.1 }
#[inline] fn w (&self) -> N { self.2 }
#[inline] fn h (&self) -> N { self.3 }
}
impl<N: Coordinate> Area<N> for [N;4] {
#[inline] fn x (&self) -> N { self[0] }
#[inline] fn y (&self) -> N { self[1] }
#[inline] fn w (&self) -> N { self[2] }
#[inline] fn h (&self) -> N { self[3] }
}
#[macro_export] macro_rules! col {
([$($expr:expr),* $(,)?]) => {
Stack::down(move|add|{ $(add(&$expr)?;)* Ok(()) })
};
(![$($expr:expr),* $(,)?]) => {
Stack::down(|add|{ $(add(&$expr)?;)* Ok(()) })
};
($expr:expr) => {
Stack::down($expr)
};
($pat:pat in $collection:expr => $item:expr) => {
Stack::down(move|add|{ for $pat in $collection { add(&$item)?; } Ok(()) })
};
}
#[macro_export] macro_rules! col_up {
([$($expr:expr),* $(,)?]) => {
Stack::up(move|add|{ $(add(&$expr)?;)* Ok(()) })
};
(![$($expr:expr),* $(,)?]) => {
Stack::up(|add|{ $(add(&$expr)?;)* Ok(()) })
};
($expr:expr) => {
Stack::up(expr)
};
($pat:pat in $collection:expr => $item:expr) => {
Stack::up(move |add|{ for $pat in $collection { add(&$item)?; } Ok(()) })
};
}
#[macro_export] macro_rules! row {
([$($expr:expr),* $(,)?]) => {
Stack::right(move|add|{ $(add(&$expr)?;)* Ok(()) })
};
(![$($expr:expr),* $(,)?]) => {
Stack::right(|add|{ $(add(&$expr)?;)* Ok(()) })
};
($expr:expr) => {
Stack::right($expr)
};
($pat:pat in $collection:expr => $item:expr) => {
Stack::right(move|add|{ for $pat in $collection { add(&$item)?; } Ok(()) })
};
}

96
src/space/align.rs Normal file
View file

@ -0,0 +1,96 @@
use crate::*;
/// Override X and Y coordinates, aligning to corner, side, or center of area
pub enum Align<E: Engine, T: Render<E>> {
_Unused(PhantomData<E>),
/// Draw at center of container
Center(T),
/// Draw at center of X axis
X(T),
/// Draw at center of Y axis
Y(T),
/// Draw at upper left corner of contaier
NW(T),
/// Draw at center of upper edge of container
N(T),
/// Draw at right left corner of contaier
NE(T),
/// Draw at center of left edge of container
W(T),
/// Draw at center of right edge of container
E(T),
/// Draw at lower left corner of container
SW(T),
/// Draw at center of lower edge of container
S(T),
/// Draw at lower right edge of container
SE(T)
}
impl<E: Engine, T: Render<E>> Align<E, T> {
pub fn inner (&self) -> &T {
match self {
Self::Center(inner) => inner,
Self::X(inner) => inner,
Self::Y(inner) => inner,
Self::NW(inner) => inner,
Self::N(inner) => inner,
Self::NE(inner) => inner,
Self::W(inner) => inner,
Self::E(inner) => inner,
Self::SW(inner) => inner,
Self::S(inner) => inner,
Self::SE(inner) => inner,
_ => unreachable!(),
}
}
pub fn c (w: T) -> Self { Self::Center(w) }
pub fn x (w: T) -> Self { Self::X(w) }
pub fn y (w: T) -> Self { Self::Y(w) }
pub fn n (w: T) -> Self { Self::N(w) }
pub fn s (w: T) -> Self { Self::S(w) }
pub fn e (w: T) -> Self { Self::E(w) }
pub fn w (w: T) -> Self { Self::W(w) }
pub fn nw (w: T) -> Self { Self::NW(w) }
pub fn sw (w: T) -> Self { Self::SW(w) }
pub fn ne (w: T) -> Self { Self::NE(w) }
pub fn se (w: T) -> Self { Self::SE(w) }
}
fn align<E: Engine, T: Render<E>, N: Coordinate, R: Area<N> + From<[N;4]>> (align: &Align<E, T>, outer: R, inner: R) -> Option<R> {
if outer.w() < inner.w() || outer.h() < inner.h() {
None
} else {
let [ox, oy, ow, oh] = outer.xywh();
let [ix, iy, iw, ih] = inner.xywh();
Some(match align {
Align::Center(_) => [ox + (ow - iw) / 2.into(), oy + (oh - ih) / 2.into(), iw, ih,].into(),
Align::X(_) => [ox + (ow - iw) / 2.into(), iy, iw, ih,].into(),
Align::Y(_) => [ix, oy + (oh - ih) / 2.into(), iw, ih,].into(),
Align::NW(_) => [ox, oy, iw, ih,].into(),
Align::N(_) => [ox + (ow - iw) / 2.into(), oy, iw, ih,].into(),
Align::NE(_) => [ox + ow - iw, oy, iw, ih,].into(),
Align::W(_) => [ox, oy + (oh - ih) / 2.into(), iw, ih,].into(),
Align::E(_) => [ox + ow - iw, oy + (oh - ih) / 2.into(), iw, ih,].into(),
Align::SW(_) => [ox, oy + oh - ih, iw, ih,].into(),
Align::S(_) => [ox + (ow - iw) / 2.into(), oy + oh - ih, iw, ih,].into(),
Align::SE(_) => [ox + ow - iw, oy + oh - ih, iw, ih,].into(),
_ => unreachable!()
})
}
}
impl<E: Engine, T: Render<E>> Render<E> for Align<E, T> {
fn min_size (&self, outer_area: E::Size) -> Perhaps<E::Size> {
self.inner().min_size(outer_area)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
let outer_area = to.area();
Ok(if let Some(inner_size) = self.min_size(outer_area.wh().into())? {
let inner_area = outer_area.clip(inner_size);
if let Some(aligned) = align(&self, outer_area.into(), inner_area.into()) {
to.render_in(aligned, self.inner())?
}
})
}
}

103
src/space/bsp.rs Normal file
View file

@ -0,0 +1,103 @@
use crate::*;
pub enum Bsp<E: Engine, X: Render<E>, Y: Render<E>> {
/// X is north of Y
N(Option<X>, Option<Y>),
/// X is south of Y
S(Option<X>, Option<Y>),
/// X is east of Y
E(Option<X>, Option<Y>),
/// X is west of Y
W(Option<X>, Option<Y>),
/// X is above Y
A(Option<X>, Option<Y>),
/// X is below Y
B(Option<X>, Option<Y>),
/// Should be avoided.
Null(PhantomData<E>),
}
impl<E: Engine, X: Render<E>, Y: Render<E>> Bsp<E, X, Y> {
pub fn new (x: X) -> Self { Self::A(Some(x), None) }
pub fn n (x: X, y: Y) -> Self { Self::N(Some(x), Some(y)) }
pub fn s (x: X, y: Y) -> Self { Self::S(Some(x), Some(y)) }
pub fn e (x: X, y: Y) -> Self { Self::E(Some(x), Some(y)) }
pub fn w (x: X, y: Y) -> Self { Self::W(Some(x), Some(y)) }
pub fn a (x: X, y: Y) -> Self { Self::A(Some(x), Some(y)) }
pub fn b (x: X, y: Y) -> Self { Self::B(Some(x), Some(y)) }
}
impl<E: Engine, X: Render<E>, Y: Render<E>> Default for Bsp<E, X, Y> {
fn default () -> Self {
Self::Null(Default::default())
}
}
impl<E: Engine, X: Render<E>, Y: Render<E>> Render<E> for Bsp<E, X, Y> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(Some(match self {
Self::Null(_) => [0.into(), 0.into()].into(),
Self::S(a, b) => {
let a = a.min_size(to)?.unwrap_or([0.into(), 0.into()].into());
let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into());
[a.w().max(b.w()), a.h() + b.h()].into()
},
Self::E(a, b) => {
let a = a.min_size(to)?.unwrap_or([0.into(), 0.into()].into());
let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into());
[a.w() + b.w(), a.h().max(b.h())].into()
},
Self::W(a, b) => {
let a = a.min_size(to)?.unwrap_or([0.into(), 0.into()].into());
let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into());
[a.w() + b.w(), a.h().max(b.h())].into()
},
Self::N(a, b) => {
let a = a.min_size(to)?.unwrap_or([0.into(), 0.into()].into());
let b = b.min_size(to)?.unwrap_or([0.into(), 0.into()].into());
[a.w().max(b.w()), a.h() + b.h()].into()
},
_ => todo!()
}))
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
let n = [0.into(), 0.into()].into();
let s = to.area().wh().into();
Ok(match self {
Self::Null(_) => {},
Self::S(a, b) => {
let s_a = a.min_size(s)?.unwrap_or(n);
let _ = b.min_size(s)?.unwrap_or(n);
let h = s_a.h().into();
to.render_in(to.area().clip_h(h).into(), a)?;
to.render_in(to.area().shrink_y(h).push_y(h).into(), b)?;
},
Self::E(a, b) => {
let s_a = a.min_size(s)?.unwrap_or(n);
let _ = b.min_size(s)?.unwrap_or(n);
let w = s_a.w().into();
to.render_in(to.area().clip_w(w).into(), a)?;
to.render_in(to.area().push_x(w).shrink_x(w).into(), b)?;
},
Self::W(a, b) => {
let s_a = a.min_size(s)?.unwrap_or(n);
let _ = b.min_size(s)?.unwrap_or(n);
let w = (to.area().w() - s_a.w()).into();
to.render_in(to.area().push_x(w).into(), a)?;
to.render_in(to.area().shrink_x(w).into(), b)?;
},
Self::N(a, b) => {
let s_a = a.min_size(s)?.unwrap_or(n);
let _ = b.min_size(s)?.unwrap_or(n);
let h = to.area().h() - s_a.h();
to.render_in(to.area().push_y(h).into(), a)?;
to.render_in(to.area().shrink_y(h).into(), b)?;
},
_ => todo!()
})
}
}
#[cfg(test)] #[test] fn test_bsp () {
// TODO
}

79
src/space/collect.rs Normal file
View file

@ -0,0 +1,79 @@
use crate::*;
pub enum Collect<'a, E: Engine, const N: usize> {
Callback(CallbackCollection<'a, E>),
//Iterator(IteratorCollection<'a, E>),
Array(ArrayCollection<'a, E, N>),
Slice(SliceCollection<'a, E>),
}
impl<'a, E: Engine, const N: usize> Collect<'a, E, N> {
pub fn iter (&'a self) -> CollectIterator<'a, E, N> {
CollectIterator(0, &self)
}
}
impl<'a, E: Engine, const N: usize> From<CallbackCollection<'a, E>> for Collect<'a, E, N> {
fn from (callback: CallbackCollection<'a, E>) -> Self {
Self::Callback(callback)
}
}
impl<'a, E: Engine, const N: usize> From<SliceCollection<'a, E>> for Collect<'a, E, N> {
fn from (slice: SliceCollection<'a, E>) -> Self {
Self::Slice(slice)
}
}
impl<'a, E: Engine, const N: usize> From<ArrayCollection<'a, E, N>> for Collect<'a, E, N>{
fn from (array: ArrayCollection<'a, E, N>) -> Self {
Self::Array(array)
}
}
type CallbackCollection<'a, E> =
&'a dyn Fn(&'a mut dyn FnMut(&dyn Render<E>)->Usually<()>);
//type IteratorCollection<'a, E> =
//&'a mut dyn Iterator<Item = dyn Render<E>>;
type SliceCollection<'a, E> =
&'a [&'a dyn Render<E>];
type ArrayCollection<'a, E, const N: usize> =
[&'a dyn Render<E>; N];
pub struct CollectIterator<'a, E: Engine, const N: usize>(usize, &'a Collect<'a, E, N>);
impl<'a, E: Engine, const N: usize> Iterator for CollectIterator<'a, E, N> {
type Item = &'a dyn Render<E>;
fn next (&mut self) -> Option<Self::Item> {
match self.1 {
Collect::Callback(callback) => {
todo!()
},
//Collection::Iterator(iterator) => {
//iterator.next()
//},
Collect::Array(array) => {
if let Some(item) = array.get(self.0) {
self.0 += 1;
//Some(item)
None
} else {
None
}
}
Collect::Slice(slice) => {
if let Some(item) = slice.get(self.0) {
self.0 += 1;
//Some(item)
None
} else {
None
}
}
}
}
}

65
src/space/cond.rs Normal file
View file

@ -0,0 +1,65 @@
use crate::*;
pub enum Cond<E: Engine, A: Render<E>, B: Render<E>> {
_Unused(E),
When(bool, A),
Either(bool, A, B)
}
impl<E: Engine, R: Render<E>> LayoutCond<E> for R {}
pub trait LayoutCond<E: Engine>: Render<E> + Sized {
fn when (self, cond: bool) -> If<E, Self> {
If(Default::default(), cond, self)
}
fn or <B: Render<E>> (self, cond: bool, other: B) -> Either<E, Self, B> {
Either(Default::default(), cond, self, other)
}
}
impl<E: Engine> LayoutCondStatic<E> for E {}
pub trait LayoutCondStatic<E: Engine> {
fn either <A: Render<E>, B: Render<E>> (
condition: bool,
a: A,
b: B,
) -> Either<E, A, B> {
Either(Default::default(), condition, a, b)
}
}
/// Render widget if predicate is true
pub struct If<E: Engine, A: Render<E>>(PhantomData<E>, bool, A);
impl<E: Engine, A: Render<E>> Render<E> for If<E, A> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
if self.1 {
return self.2.min_size(to)
}
Ok(None)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
if self.1 {
return self.2.render(to)
}
Ok(())
}
}
/// Render widget A if predicate is true, otherwise widget B
pub struct Either<E: Engine, A: Render<E>, B: Render<E>>(
PhantomData<E>,
bool,
A,
B,
);
impl<E: Engine, A: Render<E>, B: Render<E>> Render<E> for Either<E, A, B> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
if self.1 { self.2.min_size(to) } else { self.3.min_size(to) }
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
if self.1 { self.2.render(to) } else { self.3.render(to) }
}
}

47
src/space/fill.rs Normal file
View file

@ -0,0 +1,47 @@
use crate::*;
pub enum Fill<E: Engine, W: Render<E>> {
X(W),
Y(W),
XY(W),
_Unused(PhantomData<E>)
}
impl<E: Engine, W: Render<E>> Fill<E, W> {
fn inner (&self) -> &W {
match self {
Self::X(inner) => &inner,
Self::Y(inner) => &inner,
Self::XY(inner) => &inner,
_ => unreachable!(),
}
}
pub fn w (fill: W) -> Self {
Self::X(fill)
}
pub fn h (fill: W) -> Self {
Self::Y(fill)
}
pub fn wh (fill: W) -> Self {
Self::XY(fill)
}
}
impl<E: Engine, W: Render<E>> Render<E> for Fill<E, W> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
let area = self.inner().min_size(to.into())?;
if let Some(area) = area {
Ok(Some(match self {
Self::X(_) => [to.w().into(), area.h()],
Self::Y(_) => [area.w(), to.h().into()],
Self::XY(_) => [to.w().into(), to.h().into()],
_ => unreachable!(),
}.into()))
} else {
Ok(None)
}
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
self.inner().render(to)
}
}

54
src/space/fixed.rs Normal file
View file

@ -0,0 +1,54 @@
use crate::*;
/// Enforce fixed size of drawing area
pub enum Fixed<E: Engine, T: Render<E>> {
_Unused(PhantomData<E>),
/// Enforce fixed width
X(E::Unit, T),
/// Enforce fixed height
Y(E::Unit, T),
/// Enforce fixed width and height
XY(E::Unit, E::Unit, T),
}
impl<E: Engine, T: Render<E>> Fixed<E, T> {
pub fn inner (&self) -> &T {
match self {
Self::X(_, i) => i,
Self::Y(_, i) => i,
Self::XY(_, _, i) => i,
_ => unreachable!(),
}
}
pub fn w (x: E::Unit, w: T) -> Self {
Self::X(x, w)
}
pub fn h (y: E::Unit, w: T) -> Self {
Self::Y(y, w)
}
pub fn wh (x: E::Unit, y: E::Unit, w: T) -> Self {
Self::XY(x, y, w)
}
}
impl<E: Engine, T: Render<E>> Render<E> for Fixed<E, T> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(match self {
Self::X(w, _) =>
if to.w() >= *w { Some([*w, to.h()].into()) } else { None },
Self::Y(h, _) =>
if to.h() >= *h { Some([to.w(), *h].into()) } else { None },
Self::XY(w, h, _)
=> if to.w() >= *w && to.h() >= *h { Some([*w, *h].into()) } else { None },
_ => unreachable!(),
})
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
// 🡘 🡙 ←🡙→
if let Some(size) = self.min_size(to.area().wh().into())? {
to.render_in(to.area().clip(size).into(), self.inner())
} else {
Ok(())
}
}
}

92
src/space/inset_outset.rs Normal file
View file

@ -0,0 +1,92 @@
use crate::*;
impl<E: Engine + LayoutPushPull<E> + LayoutShrinkGrow<E>> LayoutInsetOutset<E> for E {}
pub trait LayoutInsetOutset<E: Engine>: LayoutPushPull<E> + LayoutShrinkGrow<E> {
fn inset_x <W: Render<E>> (x: E::Unit, w: W) -> Inset<E, W> {
Inset::X(x, w)
}
fn inset_y <W: Render<E>> (y: E::Unit, w: W) -> Inset<E, W> {
Inset::Y(y, w)
}
fn inset_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Inset<E, W> {
Inset::XY(x, y, w)
}
fn outset_x <W: Render<E>> (x: E::Unit, w: W) -> Outset<E, W> {
Outset::X(x, w)
}
fn outset_y <W: Render<E>> (y: E::Unit, w: W) -> Outset<E, W> {
Outset::Y(y, w)
}
fn outset_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Outset<E, W> {
Outset::XY(x, y, w)
}
}
/// Shrink from each side
pub enum Inset<E: Engine, T> {
/// Decrease width
X(E::Unit, T),
/// Decrease height
Y(E::Unit, T),
/// Decrease width and height
XY(E::Unit, E::Unit, T),
}
impl<E: Engine, T: Render<E>> Inset<E, T> {
pub fn inner (&self) -> &T {
match self {
Self::X(_, i) => i,
Self::Y(_, i) => i,
Self::XY(_, _, i) => i,
}
}
}
impl<E: Engine, T: Render<E>> Render<E> for Inset<E, T> {
fn render (&self, to: &mut E::Output) -> Usually<()> {
match self {
Self::X(x, inner) => E::push_x(*x, E::shrink_x(*x, inner)),
Self::Y(y, inner) => E::push_y(*y, E::shrink_y(*y, inner)),
Self::XY(x, y, inner) => E::push_xy(*x, *y, E::shrink_xy(*x, *y, inner)),
}.render(to)
}
}
/// Grow on each side
pub enum Outset<E: Engine, T: Render<E>> {
/// Increase width
X(E::Unit, T),
/// Increase height
Y(E::Unit, T),
/// Increase width and height
XY(E::Unit, E::Unit, T),
}
impl<E: Engine, T: Render<E>> Outset<E, T> {
pub fn inner (&self) -> &T {
match self {
Self::X(_, i) => i,
Self::Y(_, i) => i,
Self::XY(_, _, i) => i,
}
}
}
impl<E: Engine, T: Render<E>> Render<E> for Outset<E, T> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
match *self {
Self::X(x, ref inner) => E::grow_x(x + x, inner),
Self::Y(y, ref inner) => E::grow_y(y + y, inner),
Self::XY(x, y, ref inner) => E::grow_xy(x + x, y + y, inner),
}.min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
match *self {
Self::X(x, ref inner) => E::push_x(x, inner),
Self::Y(y, ref inner) => E::push_y(y, inner),
Self::XY(x, y, ref inner) => E::push_xy(x, y, inner),
}.render(to)
}
}

53
src/space/layers.rs Normal file
View file

@ -0,0 +1,53 @@
use crate::*;
#[macro_export] macro_rules! lay {
([$($expr:expr),* $(,)?]) => {
Layers::new(move|add|{ $(add(&$expr)?;)* Ok(()) })
};
(![$($expr:expr),* $(,)?]) => {
Layers::new(|add|{ $(add(&$expr)?;)* Ok(()) })
};
($expr:expr) => {
Layers::new($expr)
};
}
pub struct Layers<
E: Engine,
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
>(pub F, PhantomData<E>);
impl<
E: Engine,
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
> Layers<E, F> {
#[inline]
pub fn new (build: F) -> Self {
Self(build, Default::default())
}
}
impl<E: Engine, F> Render<E> for Layers<E, F>
where
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
{
fn min_size (&self, area: E::Size) -> Perhaps<E::Size> {
let mut w: E::Unit = 0.into();
let mut h: E::Unit = 0.into();
(self.0)(&mut |layer| {
if let Some(layer_area) = layer.min_size(area)? {
w = w.max(layer_area.w());
h = h.max(layer_area.h());
}
Ok(())
})?;
Ok(Some([w, h].into()))
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
if let Some(size) = self.min_size(to.area().wh().into())? {
(self.0)(&mut |layer|to.render_in(to.area().clip(size).into(), layer))
} else {
Ok(())
}
}
}

33
src/space/map_reduce.rs Normal file
View file

@ -0,0 +1,33 @@
use crate::*;
impl<E: Engine, I: Iterator<Item=T>+Send+Sync, T, R: Render<E>> LayoutMapReduce<E, I, T, R> for E {}
pub trait LayoutMapReduce<E: Engine, I: Iterator<Item = T>+Send+Sync, T, R: Render<E>> {
fn map <F: Fn(T)->R> (iterator: I, callback: F) -> Map<E, T, I, R, F> {
Map(Default::default(), iterator, callback)
}
fn reduce <F: Fn(&dyn Render<E>, T)->R+Send+Sync> (iterator: I, callback: F) -> Reduce<E, T, I, R, F> {
Reduce(Default::default(), iterator, callback)
}
}
pub struct Map<E: Engine, T, I: Iterator<Item=T>, R: Render<E>, F: Fn(T)->R>(
PhantomData<E>,
I,
F
);
pub struct Reduce<E: Engine, T, I: Iterator<Item=T>, R: Render<E>, F: Fn(&dyn Render<E>, T)->R>(
PhantomData<(E, R)>,
I,
F
);
impl<E: Engine, T, I: Iterator<Item=T>+Send+Sync, R: Render<E>, F: Fn(&dyn Render<E>, T)->R+Send+Sync> Render<E> for Reduce<E, T, I, R, F> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
todo!()
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
todo!()
}
}

93
src/space/measure.rs Normal file
View file

@ -0,0 +1,93 @@
use crate::*;
pub trait HasSize<E: Engine> {
fn size (&self) -> &Measure<E>;
}
#[macro_export] macro_rules! has_size {
(<$E:ty>|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasSize<$E> for $Struct $(<$($L),*$($T),*>)? {
fn size (&$self) -> &Measure<$E> { $cb }
}
}
}
impl<E: Engine> LayoutDebug<E> for E {}
pub trait LayoutDebug<E: Engine> {
fn debug <W: Render<E>> (other: W) -> DebugOverlay<E, W> {
DebugOverlay(Default::default(), other)
}
}
pub struct DebugOverlay<E: Engine, W: Render<E>>(PhantomData<E>, pub W);
/// A widget that tracks its render width and height
#[derive(Default)]
pub struct Measure<E: Engine> {
_engine: PhantomData<E>,
pub x: Arc<AtomicUsize>,
pub y: Arc<AtomicUsize>,
}
impl<E: Engine> Clone for Measure<E> {
fn clone (&self) -> Self {
Self {
_engine: Default::default(),
x: self.x.clone(),
y: self.y.clone(),
}
}
}
impl<E: Engine> std::fmt::Debug for Measure<E> {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("Measure")
.field("width", &self.x)
.field("height", &self.y)
.finish()
}
}
impl<E: Engine> Measure<E> {
pub fn w (&self) -> usize { self.x.load(Relaxed) }
pub fn h (&self) -> usize { self.y.load(Relaxed) }
pub fn wh (&self) -> [usize;2] { [self.w(), self.h()] }
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_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 new () -> Self {
Self {
_engine: PhantomData::default(),
x: Arc::new(0.into()),
y: Arc::new(0.into()),
}
}
}
impl Render<Tui> for Measure<Tui> {
fn min_size (&self, _: [u16;2]) -> Perhaps<[u16;2]> {
Ok(Some([0u16.into(), 0u16.into()].into()))
}
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
self.set_w(to.area().w());
self.set_h(to.area().h());
Ok(())
}
}
impl Measure<Tui> {
pub fn debug (&self) -> ShowMeasure {
ShowMeasure(&self)
}
}
pub struct ShowMeasure<'a>(&'a Measure<Tui>);
render!(<Tui>|self: ShowMeasure<'a>|render(|to|Ok({
let w = self.0.w();
let h = self.0.h();
to.blit(&format!(" {w} x {h} "), to.area.x(), to.area.y(), Some(
Style::default().bold().italic().bg(Color::Rgb(255, 0, 255)).fg(Color::Rgb(0,0,0))
))
})));

95
src/space/min_max.rs Normal file
View file

@ -0,0 +1,95 @@
use crate::*;
impl<E: Engine> LayoutMinMax<E> for E {}
pub trait LayoutMinMax<E: Engine> {
fn min_x <W: Render<E>> (x: E::Unit, w: W) -> Min<E, W> {
Min::X(x, w)
}
fn min_y <W: Render<E>> (y: E::Unit, w: W) -> Min<E, W> {
Min::Y(y, w)
}
fn min_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Min<E, W> {
Min::XY(x, y, w)
}
fn max_x <W: Render<E>> (x: E::Unit, w: W) -> Max<E, W> {
Max::X(x, w)
}
fn max_y <W: Render<E>> (y: E::Unit, w: W) -> Max<E, W> {
Max::Y(y, w)
}
fn max_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Max<E, W> {
Max::XY(x, y, w)
}
}
/// Enforce minimum size of drawing area
pub enum Min<E: Engine, T: Render<E>> {
/// Enforce minimum width
X(E::Unit, T),
/// Enforce minimum height
Y(E::Unit, T),
/// Enforce minimum width and height
XY(E::Unit, E::Unit, T),
}
/// Enforce maximum size of drawing area
pub enum Max<E: Engine, T: Render<E>> {
/// Enforce maximum width
X(E::Unit, T),
/// Enforce maximum height
Y(E::Unit, T),
/// Enforce maximum width and height
XY(E::Unit, E::Unit, T),
}
impl<E: Engine, T: Render<E>> Min<E, T> {
pub fn inner (&self) -> &T {
match self {
Self::X(_, i) => i,
Self::Y(_, i) => i,
Self::XY(_, _, i) => i,
}
}
}
impl<E: Engine, T: Render<E>> Render<E> for Min<E, T> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(self.inner().min_size(to)?.map(|to|match *self {
Self::X(w, _) => [to.w().max(w), to.h()],
Self::Y(h, _) => [to.w(), to.h().max(h)],
Self::XY(w, h, _) => [to.w().max(w), to.h().max(h)],
}.into()))
}
// TODO: 🡘 🡙 ←🡙→
fn render (&self, to: &mut E::Output) -> Usually<()> {
Ok(self.min_size(to.area().wh().into())?
.map(|size|to.render_in(to.area().clip(size).into(), self.inner()))
.transpose()?.unwrap_or(()))
}
}
impl<E: Engine, T: Render<E>> Max<E, T> {
fn inner (&self) -> &T {
match self {
Self::X(_, i) => i,
Self::Y(_, i) => i,
Self::XY(_, _, i) => i,
}
}
}
impl<E: Engine, T: Render<E>> Render<E> for Max<E, T> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(self.inner().min_size(to)?.map(|to|match *self {
Self::X(w, _) => [to.w().min(w), to.h()],
Self::Y(h, _) => [to.w(), to.h().min(h)],
Self::XY(w, h, _) => [to.w().min(w), to.h().min(h)],
}.into()))
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
Ok(self.min_size(to.area().wh().into())?
.map(|size|to.render_in(to.area().clip(size).into(), self.inner()))
.transpose()?.unwrap_or(()))
}
}

128
src/space/push_pull.rs Normal file
View file

@ -0,0 +1,128 @@
use crate::*;
impl<E: Engine> LayoutPushPull<E> for E {}
pub trait LayoutPushPull<E: Engine> {
fn push_x <W: Render<E>> (x: E::Unit, w: W) -> Push<E, W> {
Push::X(x, w)
}
fn push_y <W: Render<E>> (y: E::Unit, w: W) -> Push<E, W> {
Push::Y(y, w)
}
fn push_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Push<E, W> {
Push::XY(x, y, w)
}
fn pull_x <W: Render<E>> (x: E::Unit, w: W) -> Pull<E, W> {
Pull::X(x, w)
}
fn pull_y <W: Render<E>> (y: E::Unit, w: W) -> Pull<E, W> {
Pull::Y(y, w)
}
fn pull_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Pull<E, W> {
Pull::XY(x, y, w)
}
}
/// Increment origin point of drawing area
pub enum Push<E: Engine, T: Render<E>> {
/// Move origin to the right
X(E::Unit, T),
/// Move origin downwards
Y(E::Unit, T),
/// Move origin to the right and downwards
XY(E::Unit, E::Unit, T),
}
impl<E: Engine, T: Render<E>> Push<E, T> {
pub fn inner (&self) -> &T {
match self {
Self::X(_, i) => i,
Self::Y(_, i) => i,
Self::XY(_, _, i) => i,
}
}
pub fn x (&self) -> E::Unit {
match self {
Self::X(x, _) => *x,
Self::Y(_, _) => E::Unit::default(),
Self::XY(x, _, _) => *x,
}
}
pub fn y (&self) -> E::Unit {
match self {
Self::X(_, _) => E::Unit::default(),
Self::Y(y, _) => *y,
Self::XY(_, y, _) => *y,
}
}
}
impl<E: Engine, T: Render<E>> Render<E> for Push<E, T> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
self.inner().min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
let area = to.area();
Ok(self.min_size(area.wh().into())?
.map(|size|to.render_in(match *self {
Self::X(x, _) => [area.x() + x, area.y(), size.w(), size.h()],
Self::Y(y, _) => [area.x(), area.y() + y, size.w(), size.h()],
Self::XY(x, y, _) => [area.x() + x, area.y() + y, size.w(), size.h()],
}.into(), self.inner())).transpose()?.unwrap_or(()))
}
}
/// Decrement origin point of drawing area
pub enum Pull<E: Engine, T: Render<E>> {
_Unused(PhantomData<E>),
/// Move origin to the right
X(E::Unit, T),
/// Move origin downwards
Y(E::Unit, T),
/// Move origin to the right and downwards
XY(E::Unit, E::Unit, T),
}
impl<E: Engine, T: Render<E>> Pull<E, T> {
pub fn inner (&self) -> &T {
match self {
Self::X(_, i) => i,
Self::Y(_, i) => i,
Self::XY(_, _, i) => i,
_ => unreachable!(),
}
}
pub fn x (&self) -> E::Unit {
match self {
Self::X(x, _) => *x,
Self::Y(_, _) => E::Unit::default(),
Self::XY(x, _, _) => *x,
_ => unreachable!(),
}
}
pub fn y (&self) -> E::Unit {
match self {
Self::X(_, _) => E::Unit::default(),
Self::Y(y, _) => *y,
Self::XY(_, y, _) => *y,
_ => unreachable!(),
}
}
}
impl<E: Engine, T: Render<E>> Render<E> for Pull<E, T> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
self.inner().min_size(to)
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
let area = to.area();
Ok(self.min_size(area.wh().into())?
.map(|size|to.render_in(match *self {
Self::X(x, _) => [area.x().minus(x), area.y(), size.w(), size.h()],
Self::Y(y, _) => [area.x(), area.y().minus(y), size.w(), size.h()],
Self::XY(x, y, _) => [area.x().minus(x), area.y().minus(y), size.w(), size.h()],
_ => unreachable!(),
}.into(), self.inner())).transpose()?.unwrap_or(()))
}
}

8
src/space/scroll.rs Normal file
View file

@ -0,0 +1,8 @@
use crate::*;
/// A scrollable area.
pub struct Scroll<
E: Engine,
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
>(pub F, pub Direction, pub u64, PhantomData<E>);

103
src/space/shrink_grow.rs Normal file
View file

@ -0,0 +1,103 @@
use crate::*;
impl<E: Engine> LayoutShrinkGrow<E> for E {}
pub trait LayoutShrinkGrow<E: Engine> {
fn shrink_x <W: Render<E>> (x: E::Unit, w: W) -> Shrink<E, W> {
Shrink::X(x, w)
}
fn shrink_y <W: Render<E>> (y: E::Unit, w: W) -> Shrink<E, W> {
Shrink::Y(y, w)
}
fn shrink_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Shrink<E, W> {
Shrink::XY(x, y, w)
}
fn grow_x <W: Render<E>> (x: E::Unit, w: W) -> Grow<E, W> {
Grow::X(x, w)
}
fn grow_y <W: Render<E>> (y: E::Unit, w: W) -> Grow<E, W> {
Grow::Y(y, w)
}
fn grow_xy <W: Render<E>> (x: E::Unit, y: E::Unit, w: W) -> Grow<E, W> {
Grow::XY(x, y, w)
}
}
/// Shrink drawing area
pub enum Shrink<E: Engine, T: Render<E>> {
/// Decrease width
X(E::Unit, T),
/// Decrease height
Y(E::Unit, T),
/// Decrease width and height
XY(E::Unit, E::Unit, T),
}
/// Expand drawing area
pub enum Grow<E: Engine, T: Render<E>> {
/// Increase width
X(E::Unit, T),
/// Increase height
Y(E::Unit, T),
/// Increase width and height
XY(E::Unit, E::Unit, T)
}
impl<E: Engine, T: Render<E>> Shrink<E, T> {
fn inner (&self) -> &T {
match self {
Self::X(_, i) => i,
Self::Y(_, i) => i,
Self::XY(_, _, i) => i,
}
}
}
impl<E: Engine, T: Render<E>> Grow<E, T> {
fn inner (&self) -> &T {
match self {
Self::X(_, i) => i,
Self::Y(_, i) => i,
Self::XY(_, _, i) => i,
}
}
}
impl<E: Engine, T: Render<E>> Render<E> for Shrink<E, T> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(self.inner().min_size(to)?.map(|to|match *self {
Self::X(w, _) => [
if to.w() > w { to.w() - w } else { 0.into() },
to.h()
],
Self::Y(h, _) => [
to.w(),
if to.h() > h { to.h() - h } else { 0.into() }
],
Self::XY(w, h, _) => [
if to.w() > w { to.w() - w } else { 0.into() },
if to.h() > h { to.h() - h } else { 0.into() }
],
}.into()))
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
Ok(self.min_size(to.area().wh().into())?
.map(|size|to.render_in(to.area().clip(size).into(), self.inner()))
.transpose()?.unwrap_or(()))
}
}
impl<E: Engine, T: Render<E>> Render<E> for Grow<E, T> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(self.inner().min_size(to)?.map(|to|match *self {
Self::X(w, _) => [to.w() + w, to.h()],
Self::Y(h, _) => [to.w(), to.h() + h],
Self::XY(w, h, _) => [to.w() + w, to.h() + h],
}.into()))
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
Ok(self.min_size(to.area().wh().into())?
.map(|size|to.render_in(to.area().clip(size).into(), self.inner()))
.transpose()?.unwrap_or(()))
}
}

71
src/space/split.rs Normal file
View file

@ -0,0 +1,71 @@
use crate::*;
use Direction::*;
impl<E: Engine> LayoutSplit<E> for E {}
pub trait LayoutSplit<E: Engine> {
fn split <A: Render<E>, B: Render<E>> (
flip: bool, direction: Direction, amount: E::Unit, a: A, b: B
) -> Split<E, A, B> {
Split::new(flip, direction, amount, a, b)
}
fn split_n <A: Render<E>, B: Render<E>> (
flip: bool, amount: E::Unit, a: A, b: B
) -> Split<E, A, B> {
Self::split(flip, North, amount, a, b)
}
fn split_s <A: Render<E>, B: Render<E>> (
flip: bool, amount: E::Unit, a: A, b: B
) -> Split<E, A, B> {
Self::split(flip, South, amount, a, b)
}
fn split_w <A: Render<E>, B: Render<E>> (
flip: bool, amount: E::Unit, a: A, b: B
) -> Split<E, A, B> {
Self::split(flip, West, amount, a, b)
}
fn split_e <A: Render<E>, B: Render<E>> (
flip: bool, amount: E::Unit, a: A, b: B
) -> Split<E, A, B> {
Self::split(flip, East, amount, a, b)
}
}
/// A binary split with fixed proportion
pub struct Split<E: Engine, A: Render<E>, B: Render<E>>(
pub bool, pub Direction, pub E::Unit, A, B, PhantomData<E>
);
impl<E: Engine, A: Render<E>, B: Render<E>> Split<E, A, B> {
pub fn new (flip: bool, direction: Direction, proportion: E::Unit, a: A, b: B) -> Self {
Self(flip, direction, proportion, a, b, Default::default())
}
pub fn up (flip: bool, proportion: E::Unit, a: A, b: B) -> Self {
Self(flip, North, proportion, a, b, Default::default())
}
pub fn down (flip: bool, proportion: E::Unit, a: A, b: B) -> Self {
Self(flip, South, proportion, a, b, Default::default())
}
pub fn left (flip: bool, proportion: E::Unit, a: A, b: B) -> Self {
Self(flip, West, proportion, a, b, Default::default())
}
pub fn right (flip: bool, proportion: E::Unit, a: A, b: B) -> Self {
Self(flip, East, proportion, a, b, Default::default())
}
}
impl<E: Engine, A: Render<E>, B: Render<E>> Render<E> for Split<E, A, B> {
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
Ok(Some(to))
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
let (a, b) = to.area().split_fixed(self.1, self.2);
Ok(if self.0 {
to.render_in(a.into(), &self.4)?;
to.render_in(b.into(), &self.3)?;
} else {
to.render_in(a.into(), &self.3)?;
to.render_in(b.into(), &self.4)?;
})
}
}

153
src/space/stack.rs Normal file
View file

@ -0,0 +1,153 @@
use crate::*;
use Direction::*;
pub struct Stack<
E: Engine,
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
>(pub F, pub Direction, PhantomData<E>);
impl<
E: Engine,
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
> Stack<E, F> {
#[inline] pub fn new (direction: Direction, build: F) -> Self {
Self(build, direction, Default::default())
}
#[inline] pub fn right (build: F) -> Self {
Self::new(East, build)
}
#[inline] pub fn down (build: F) -> Self {
Self::new(South, build)
}
#[inline] pub fn up (build: F) -> Self {
Self::new(North, build)
}
}
impl<E: Engine, F> Render<E> for Stack<E, F>
where
F: Send + Sync + Fn(&mut dyn FnMut(&dyn Render<E>)->Usually<()>)->Usually<()>
{
fn min_size (&self, to: E::Size) -> Perhaps<E::Size> {
match self.1 {
South => {
let mut w: E::Unit = 0.into();
let mut h: E::Unit = 0.into();
(self.0)(&mut |component: &dyn Render<E>| {
let max = to.h().minus(h);
if max > E::Unit::zero() {
let item = E::max_y(max, E::push_y(h, component));
let size = item.min_size(to)?.map(|size|size.wh());
if let Some([width, height]) = size {
h = h + height.into();
w = w.max(width);
}
}
Ok(())
})?;
Ok(Some([w, h].into()))
},
East => {
let mut w: E::Unit = 0.into();
let mut h: E::Unit = 0.into();
(self.0)(&mut |component: &dyn Render<E>| {
let max = to.w().minus(w);
if max > E::Unit::zero() {
let item = E::max_x(max, E::push_x(h, component));
let size = item.min_size(to)?.map(|size|size.wh());
if let Some([width, height]) = size {
w = w + width.into();
h = h.max(height);
}
}
Ok(())
})?;
Ok(Some([w, h].into()))
},
North => {
let mut w: E::Unit = 0.into();
let mut h: E::Unit = 0.into();
(self.0)(&mut |component: &dyn Render<E>| {
let max = to.h().minus(h);
if max > E::Unit::zero() {
let item = E::max_y(to.h() - h, component);
let size = item.min_size(to)?.map(|size|size.wh());
if let Some([width, height]) = size {
h = h + height.into();
w = w.max(width);
}
}
Ok(())
})?;
Ok(Some([w, h].into()))
},
West => {
let w: E::Unit = 0.into();
let h: E::Unit = 0.into();
(self.0)(&mut |component: &dyn Render<E>| {
if w < to.w() {
todo!();
}
Ok(())
})?;
Ok(Some([w, h].into()))
},
}
}
fn render (&self, to: &mut E::Output) -> Usually<()> {
let area = to.area();
let mut w = 0.into();
let mut h = 0.into();
match self.1 {
South => {
(self.0)(&mut |item| {
if h < area.h() {
let item = E::max_y(area.h() - h, E::push_y(h, item));
let show = item.min_size(area.wh().into())?.map(|s|s.wh());
if let Some([width, height]) = show {
item.render(to)?;
h = h + height;
if width > w { w = width }
};
}
Ok(())
})?;
},
East => {
(self.0)(&mut |item| {
if w < area.w() {
let item = E::max_x(area.w() - w, E::push_x(w, item));
let show = item.min_size(area.wh().into())?.map(|s|s.wh());
if let Some([width, height]) = show {
item.render(to)?;
w = width + w;
if height > h { h = height }
};
}
Ok(())
})?;
},
North => {
(self.0)(&mut |item| {
if h < area.h() {
let show = item.min_size([area.w(), area.h().minus(h)].into())?.map(|s|s.wh());
if let Some([width, height]) = show {
E::shrink_y(height, E::push_y(area.h() - height, item))
.render(to)?;
h = h + height;
if width > w { w = width }
};
}
Ok(())
})?;
},
_ => todo!()
};
Ok(())
}
}

4
src/status.rs Normal file
View file

@ -0,0 +1,4 @@
mod status_arranger; pub(crate) use self::status_arranger::*;
mod status_edit; pub(crate) use self::status_edit::*;
mod status_sequencer; pub(crate) use self::status_sequencer::*;
mod status_groovebox; pub(crate) use self::status_groovebox::*;

View file

@ -0,0 +1,52 @@
use crate::*;
/// 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:&ArrangerTui|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!(<Tui>|self: ArrangerStatus|Fixed::h(2, lay!([
Self::help(),
Fill::wh(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
])));
impl ArrangerStatus {
fn help () -> impl Render<Tui> {
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 Render<Tui> + use<'_> {
row!([&self.cpu, &self.size])
}
}

31
src/status/status_edit.rs Normal file
View file

@ -0,0 +1,31 @@
use crate::*;
pub struct MidiEditStatus<'a>(pub &'a MidiEditorModel);
render!(<Tui>|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 field = move|x, y|row!([
Tui::fg_bg(color.lighter.rgb, color.darker.rgb, Tui::bold(true, x)),
Tui::fg_bg(color.light.rgb, color.darker.rgb, Tui::bold(true, "")),
Tui::fg_bg(color.lightest.rgb, color.dark.rgb, &y),
]);
let bg = color.darkest.rgb;
let fg = color.lightest.rgb;
Tui::bg(bg, Fill::w(Tui::fg(fg, row!([
field(" Time", format!("{}/{}-{} ({}*{}) {}",
self.0.time_point(), self.0.time_start().get(), self.0.time_end(),
self.0.time_axis().get(), self.0.time_zoom().get(),
if self.0.time_lock().get() { "[lock]" } else { " " })),
" ",
field(" Note", format!("{} ({}) {} | {}-{} ({})",
self.0.note_point(), to_note_name(self.0.note_point()), self.0.note_len(),
to_note_name(self.0.note_lo().get()), to_note_name(self.0.note_hi()),
self.0.note_axis().get()))
]))))
});

View file

@ -0,0 +1,50 @@
use crate::*;
/// Status bar for sequencer app
#[derive(Clone)]
pub struct GrooveboxStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) playing: bool,
}
from!(|state:&GrooveboxTui|GrooveboxStatus = {
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!(<Tui>|self: GrooveboxStatus|Fixed::h(2, lay!([
Self::help(),
Fill::wh(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
])));
impl GrooveboxStatus {
fn help () -> impl Render<Tui> {
let single = |binding, command|row!([" ", col!([
Tui::fg(TuiTheme::yellow(), binding),
command
])]);
let double = |(b1, c1), (b2, c2)|col!([
row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]),
row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]),
]);
Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([
single("SPACE", "play/pause"),
double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
double(("a", "append"), ("s", "set note"),),
double((",.", "length"), ("<>", "triplet"), ),
double(("[]", "phrase"), ("{}", "order"), ),
double(("q", "enqueue"), ("e", "edit"), ),
double(("c", "color"), ("", ""),),
]))
}
fn stats (&self) -> impl Render<Tui> + use<'_> {
row!([&self.cpu, &self.size])
}
}

View file

@ -0,0 +1,50 @@
use crate::*;
/// Status bar for sequencer app
#[derive(Clone)]
pub struct SequencerStatus {
pub(crate) width: usize,
pub(crate) cpu: Option<String>,
pub(crate) size: String,
pub(crate) playing: bool,
}
from!(|state:&SequencerTui|SequencerStatus = {
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!(<Tui>|self: SequencerStatus|Fixed::h(2, lay!([
Self::help(),
Fill::wh(Align::se(Tui::fg_bg(TuiTheme::orange(), TuiTheme::g(25), self.stats()))),
])));
impl SequencerStatus {
fn help () -> impl Render<Tui> {
let single = |binding, command|row!([" ", col!([
Tui::fg(TuiTheme::yellow(), binding),
command
])]);
let double = |(b1, c1), (b2, c2)|col!([
row!([" ", Tui::fg(TuiTheme::yellow(), b1), " ", c1,]),
row!([" ", Tui::fg(TuiTheme::yellow(), b2), " ", c2,]),
]);
Tui::fg_bg(TuiTheme::g(255), TuiTheme::g(50), row!([
single("SPACE", "play/pause"),
double(("▲▼▶◀", "cursor"), ("Ctrl", "scroll"), ),
double(("a", "append"), ("s", "set note"),),
double((",.", "length"), ("<>", "triplet"), ),
double(("[]", "phrase"), ("{}", "order"), ),
double(("q", "enqueue"), ("e", "edit"), ),
double(("c", "color"), ("", ""),),
]))
}
fn stats (&self) -> impl Render<Tui> + use<'_> {
row!([&self.cpu, &self.size])
}
}

183
src/test.rs Normal file
View file

@ -0,0 +1,183 @@
//use crate::*;
//struct TestEngine([u16;4], Vec<Vec<char>>);
//impl Engine for TestEngine {
//type Unit = u16;
//type Size = [Self::Unit;2];
//type Area = [Self::Unit;4];
//type Input = Self;
//type Handled = bool;
//fn exited (&self) -> bool {
//true
//}
//}
//#[derive(Copy, Clone)]
//struct TestArea(u16, u16);
//impl Render<TestEngine> for TestArea {
//fn min_size (&self, to: [u16;2]) -> Perhaps<[u16;2]> {
//Ok(Some([to[0], to[1], self.0, self.1]))
//}
//fn render (&self, to: &mut TestEngine) -> Perhaps<[u16;4]> {
//if let Some(layout) = self.layout(to.area())? {
//for y in layout.y()..layout.y()+layout.h()-1 {
//for x in layout.x()..layout.x()+layout.w()-1 {
//to.1[y as usize][x as usize] = '*';
//}
//}
//Ok(Some(layout))
//} else {
//Ok(None)
//}
//}
//}
//#[test]
//fn test_plus_minus () -> Usually<()> {
//let area = [0, 0, 10, 10];
//let engine = TestEngine(area, vec![vec![' ';10];10]);
//let test = TestArea(4, 4);
//assert_eq!(test.layout(area)?, Some([0, 0, 4, 4]));
//assert_eq!(Push::X(1, test).layout(area)?, Some([1, 0, 4, 4]));
//Ok(())
//}
//#[test]
//fn test_outset_align () -> Usually<()> {
//let area = [0, 0, 10, 10];
//let engine = TestEngine(area, vec![vec![' ';10];10]);
//let test = TestArea(4, 4);
//assert_eq!(test.layout(area)?, Some([0, 0, 4, 4]));
//assert_eq!(Outset::X(1, test).layout(area)?, Some([0, 0, 6, 4]));
//assert_eq!(Align::X(test).layout(area)?, Some([3, 0, 4, 4]));
//assert_eq!(Align::X(Outset::X(1, test)).layout(area)?, Some([2, 0, 6, 4]));
//assert_eq!(Outset::X(1, Align::X(test)).layout(area)?, Some([2, 0, 6, 4]));
//Ok(())
//}
////#[test]
////fn test_misc () -> Usually<()> {
////let area: [u16;4] = [0, 0, 10, 10];
////let test = TestArea(4, 4);
////assert_eq!(test.layout(area)?,
////Some([0, 0, 4, 4]));
////assert_eq!(Align::Center(test).layout(area)?,
////Some([3, 3, 4, 4]));
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&test)?;
////add(&test)
////})).layout(area)?,
////Some([3, 1, 4, 8]));
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&Outset::XY(2, 2, test))?;
////add(&test)
////})).layout(area)?,
////Some([2, 0, 6, 10]));
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&Outset::XY(2, 2, test))?;
////add(&Inset::XY(2, 2, test))
////})).layout(area)?,
////Some([2, 1, 6, 8]));
////assert_eq!(Stack::down(|add|{
////add(&Outset::XY(2, 2, test))?;
////add(&Inset::XY(2, 2, test))
////}).layout(area)?,
////Some([0, 0, 6, 8]));
////assert_eq!(Stack::right(|add|{
////add(&Stack::down(|add|{
////add(&Outset::XY(2, 2, test))?;
////add(&Inset::XY(2, 2, test))
////}))?;
////add(&Align::Center(TestArea(2 ,2)))
////}).layout(area)?,
////Some([0, 0, 8, 8]));
////Ok(())
////}
////#[test]
////fn test_offset () -> Usually<()> {
////let area: [u16;4] = [50, 50, 100, 100];
////let test = TestArea(3, 3);
////assert_eq!(Push::X(1, test).layout(area)?, Some([51, 50, 3, 3]));
////assert_eq!(Push::Y(1, test).layout(area)?, Some([50, 51, 3, 3]));
////assert_eq!(Push::XY(1, 1, test).layout(area)?, Some([51, 51, 3, 3]));
////Ok(())
////}
////#[test]
////fn test_outset () -> Usually<()> {
////let area: [u16;4] = [50, 50, 100, 100];
////let test = TestArea(3, 3);
////assert_eq!(Outset::X(1, test).layout(area)?, Some([49, 50, 5, 3]));
////assert_eq!(Outset::Y(1, test).layout(area)?, Some([50, 49, 3, 5]));
////assert_eq!(Outset::XY(1, 1, test).layout(area)?, Some([49, 49, 5, 5]));
////Ok(())
////}
////#[test]
////fn test_inset () -> Usually<()> {
////let area: [u16;4] = [50, 50, 100, 100];
////let test = TestArea(3, 3);
////assert_eq!(Inset::X(1, test).layout(area)?, Some([51, 50, 1, 3]));
////assert_eq!(Inset::Y(1, test).layout(area)?, Some([50, 51, 3, 1]));
////assert_eq!(Inset::XY(1, 1, test).layout(area)?, Some([51, 51, 1, 1]));
////Ok(())
////}
////#[test]
////fn test_stuff () -> Usually<()> {
////let area: [u16;4] = [0, 0, 100, 100];
////assert_eq!("1".layout(area)?,
////Some([0, 0, 1, 1]));
////assert_eq!("333".layout(area)?,
////Some([0, 0, 3, 1]));
////assert_eq!(Layers::new(|add|{add(&"1")?;add(&"333")}).layout(area)?,
////Some([0, 0, 3, 1]));
////assert_eq!(Stack::down(|add|{add(&"1")?;add(&"333")}).layout(area)?,
////Some([0, 0, 3, 2]));
////assert_eq!(Stack::right(|add|{add(&"1")?;add(&"333")}).layout(area)?,
////Some([0, 0, 4, 1]));
////assert_eq!(Stack::down(|add|{
////add(&Stack::right(|add|{add(&"1")?;add(&"333")}))?;
////add(&"55555")
////}).layout(area)?,
////Some([0, 0, 5, 2]));
////let area: [u16;4] = [1, 1, 100, 100];
////assert_eq!(Outset::X(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
////Some([0, 1, 6, 1]));
////assert_eq!(Outset::Y(1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
////Some([1, 0, 4, 3]));
////assert_eq!(Outset::XY(1, 1, Stack::right(|add|{add(&"1")?;add(&"333")})).layout(area)?,
////Some([0, 0, 6, 3]));
////assert_eq!(Stack::down(|add|{
////add(&Outset::XY(1, 1, "1"))?;
////add(&Outset::XY(1, 1, "333"))
////}).layout(area)?,
////Some([1, 1, 5, 6]));
////let area: [u16;4] = [1, 1, 95, 100];
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&Outset::XY(1, 1, "1"))?;
////add(&Outset::XY(1, 1, "333"))
////})).layout(area)?,
////Some([46, 48, 5, 6]));
////assert_eq!(Align::Center(Stack::down(|add|{
////add(&Layers::new(|add|{
//////add(&Outset::XY(1, 1, Background(Color::Rgb(0,128,0))))?;
////add(&Outset::XY(1, 1, "1"))?;
////add(&Outset::XY(1, 1, "333"))?;
//////add(&Background(Color::Rgb(0,128,0)))?;
////Ok(())
////}))?;
////add(&Layers::new(|add|{
//////add(&Outset::XY(1, 1, Background(Color::Rgb(0,0,128))))?;
////add(&Outset::XY(1, 1, "555"))?;
////add(&Outset::XY(1, 1, "777777"))?;
//////add(&Background(Color::Rgb(0,0,128)))?;
////Ok(())
////}))
////})).layout(area)?,
////Some([46, 48, 5, 6]));
////Ok(())
////}

16
src/time.rs Normal file
View file

@ -0,0 +1,16 @@
pub(crate) mod clock; pub(crate) use clock::*;
pub(crate) mod moment; pub(crate) use moment::*;
pub(crate) mod perf; pub(crate) use perf::*;
pub(crate) mod pulse; pub(crate) use pulse::*;
pub(crate) mod sr; pub(crate) use sr::*;
pub(crate) mod unit; pub(crate) use unit::*;
//#[cfg(test)]
//mod test {
//use super::*;
//#[test]
//fn test_samples_to_ticks () {
//let ticks = Ticks(12.3).between_samples(0, 100).collect::<Vec<_>>();
//println!("{ticks:?}");
//}
//}

193
src/time/clock.rs Normal file
View file

@ -0,0 +1,193 @@
use crate::*;
pub trait HasClock: Send + Sync {
fn clock (&self) -> &ClockModel;
}
#[macro_export] macro_rules! has_clock {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?|$cb:expr) => {
impl $(<$($L),*$($T $(: $U)?),*>)? HasClock for $Struct $(<$($L),*$($T),*>)? {
fn clock (&$self) -> &ClockModel { $cb }
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ClockCommand {
Play(Option<u32>),
Pause(Option<u32>),
SeekUsec(f64),
SeekSample(f64),
SeekPulse(f64),
SetBpm(f64),
SetQuant(f64),
SetSync(f64),
}
impl<T: HasClock> Command<T> for ClockCommand {
fn execute (self, state: &mut T) -> Perhaps<Self> {
use ClockCommand::*;
match self {
Play(start) => state.clock().play_from(start)?,
Pause(pause) => state.clock().pause_at(pause)?,
SeekUsec(usec) => state.clock().playhead.update_from_usec(usec),
SeekSample(sample) => state.clock().playhead.update_from_sample(sample),
SeekPulse(pulse) => state.clock().playhead.update_from_pulse(pulse),
SetBpm(bpm) => return Ok(Some(SetBpm(state.clock().timebase().bpm.set(bpm)))),
SetQuant(quant) => return Ok(Some(SetQuant(state.clock().quant.set(quant)))),
SetSync(sync) => return Ok(Some(SetSync(state.clock().sync.set(sync)))),
};
Ok(None)
}
}
#[derive(Clone)]
pub struct ClockModel {
/// JACK transport handle.
pub transport: Arc<Transport>,
/// Global temporal resolution (shared by [Moment] fields)
pub timebase: Arc<Timebase>,
/// Current global sample and usec (monotonic from JACK clock)
pub global: Arc<Moment>,
/// Global sample and usec at which playback started
pub started: Arc<RwLock<Option<Moment>>>,
/// Playback offset (when playing not from start)
pub offset: Arc<Moment>,
/// Current playhead position
pub playhead: Arc<Moment>,
/// Note quantization factor
pub quant: Arc<Quantize>,
/// Launch quantization factor
pub sync: Arc<LaunchSync>,
/// Size of buffer in samples
pub chunk: Arc<AtomicUsize>,
}
from!(|jack: &Arc<RwLock<JackClient>>| ClockModel = {
let jack = jack.read().unwrap();
let chunk = jack.client().buffer_size();
let transport = jack.client().transport();
let timebase = Arc::new(Timebase::default());
Self {
quant: Arc::new(24.into()),
sync: Arc::new(384.into()),
transport: Arc::new(transport),
chunk: Arc::new((chunk as usize).into()),
global: Arc::new(Moment::zero(&timebase)),
playhead: Arc::new(Moment::zero(&timebase)),
offset: Arc::new(Moment::zero(&timebase)),
started: RwLock::new(None).into(),
timebase,
}
});
impl std::fmt::Debug for ClockModel {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("ClockModel")
.field("timebase", &self.timebase)
.field("chunk", &self.chunk)
.field("quant", &self.quant)
.field("sync", &self.sync)
.field("global", &self.global)
.field("playhead", &self.playhead)
.field("started", &self.started)
.finish()
}
}
impl ClockModel {
pub fn timebase (&self) -> &Arc<Timebase> {
&self.timebase
}
/// Current sample rate
pub fn sr (&self) -> &SampleRate {
&self.timebase.sr
}
/// Current tempo
pub fn bpm (&self) -> &BeatsPerMinute {
&self.timebase.bpm
}
/// Current MIDI resolution
pub fn ppq (&self) -> &PulsesPerQuaver {
&self.timebase.ppq
}
/// Next pulse that matches launch sync (for phrase switchover)
pub fn next_launch_pulse (&self) -> usize {
let sync = self.sync.get() as usize;
let pulse = self.playhead.pulse.get() as usize;
if pulse % sync == 0 {
pulse
} else {
(pulse / sync + 1) * sync
}
}
/// Start playing, optionally seeking to a given location beforehand
pub fn play_from (&self, start: Option<u32>) -> Usually<()> {
if let Some(start) = start {
self.transport.locate(start)?;
}
self.transport.start()?;
Ok(())
}
/// Pause, optionally seeking to a given location afterwards
pub fn pause_at (&self, pause: Option<u32>) -> Usually<()> {
self.transport.stop()?;
if let Some(pause) = pause {
self.transport.locate(pause)?;
}
Ok(())
}
/// Is currently paused?
pub fn is_stopped (&self) -> bool {
self.started.read().unwrap().is_none()
}
/// Is currently playing?
pub fn is_rolling (&self) -> bool {
self.started.read().unwrap().is_some()
}
/// Update chunk size
pub fn set_chunk (&self, n_frames: usize) {
self.chunk.store(n_frames, Relaxed);
}
pub fn update_from_scope (&self, scope: &ProcessScope) -> Usually<()> {
// Store buffer length
self.set_chunk(scope.n_frames() as usize);
// Store reported global frame and usec
let CycleTimes { current_frames, current_usecs, .. } = scope.cycle_times()?;
self.global.sample.set(current_frames as f64);
self.global.usec.set(current_usecs as f64);
// If transport has just started or just stopped,
// update starting point:
let mut started = self.started.write().unwrap();
match (self.transport.query_state()?, started.as_ref()) {
(TransportState::Rolling, None) => {
let moment = Moment::zero(&self.timebase);
moment.sample.set(current_frames as f64);
moment.usec.set(current_usecs as f64);
*started = Some(moment);
},
(TransportState::Stopped, Some(_)) => {
*started = None;
},
_ => {}
};
self.playhead.update_from_sample(started.as_ref()
.map(|started|current_frames as f64 - started.sample.get())
.unwrap_or(0.));
Ok(())
}
}
/// Hosts the JACK callback for updating the temporal pointer and playback status.
pub struct ClockAudio<'a, T: HasClock>(pub &'a mut T);
impl<T: HasClock> Audio for ClockAudio<'_, T> {
#[inline] fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
self.0.clock().update_from_scope(scope).unwrap();
Control::Continue
}
}

181
src/time/moment.rs Normal file
View file

@ -0,0 +1,181 @@
use crate::*;
#[derive(Debug, Clone)]
pub enum Moment2 {
None,
Zero,
Usec(Microsecond),
Sample(SampleCount),
Pulse(Pulse),
}
/// A point in time in all time scales (microsecond, sample, MIDI pulse)
#[derive(Debug, Default, Clone)]
pub struct Moment {
pub timebase: Arc<Timebase>,
/// Current time in microseconds
pub usec: Microsecond,
/// Current time in audio samples
pub sample: SampleCount,
/// Current time in MIDI pulses
pub pulse: Pulse,
}
impl Moment {
pub fn zero (timebase: &Arc<Timebase>) -> Self {
Self { usec: 0.into(), sample: 0.into(), pulse: 0.into(), timebase: timebase.clone() }
}
pub fn from_usec (timebase: &Arc<Timebase>, usec: f64) -> Self {
Self {
usec: usec.into(),
sample: timebase.sr.usecs_to_sample(usec).into(),
pulse: timebase.usecs_to_pulse(usec).into(),
timebase: timebase.clone(),
}
}
pub fn from_sample (timebase: &Arc<Timebase>, sample: f64) -> Self {
Self {
sample: sample.into(),
usec: timebase.sr.samples_to_usec(sample).into(),
pulse: timebase.samples_to_pulse(sample).into(),
timebase: timebase.clone(),
}
}
pub fn from_pulse (timebase: &Arc<Timebase>, pulse: f64) -> Self {
Self {
pulse: pulse.into(),
sample: timebase.pulses_to_sample(pulse).into(),
usec: timebase.pulses_to_usec(pulse).into(),
timebase: timebase.clone(),
}
}
#[inline] pub fn update_from_usec (&self, usec: f64) {
self.usec.set(usec);
self.pulse.set(self.timebase.usecs_to_pulse(usec));
self.sample.set(self.timebase.sr.usecs_to_sample(usec));
}
#[inline] pub fn update_from_sample (&self, sample: f64) {
self.usec.set(self.timebase.sr.samples_to_usec(sample));
self.pulse.set(self.timebase.samples_to_pulse(sample));
self.sample.set(sample);
}
#[inline] pub fn update_from_pulse (&self, pulse: f64) {
self.usec.set(self.timebase.pulses_to_usec(pulse));
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())
}
}
/// Temporal resolutions: sample rate, tempo, MIDI pulses per quaver (beat)
#[derive(Debug, Clone)]
pub struct Timebase {
/// Audio samples per second
pub sr: SampleRate,
/// MIDI beats per minute
pub bpm: BeatsPerMinute,
/// MIDI ticks per beat
pub ppq: PulsesPerQuaver,
}
impl Timebase {
/// Specify sample rate, BPM and PPQ
pub fn new (
s: impl Into<SampleRate>,
b: impl Into<BeatsPerMinute>,
p: impl Into<PulsesPerQuaver>
) -> Self {
Self { sr: s.into(), bpm: b.into(), ppq: p.into() }
}
/// Iterate over ticks between start and end.
#[inline] pub fn pulses_between_samples (&self, start: usize, end: usize) -> TicksIterator {
TicksIterator { spp: self.samples_per_pulse(), sample: start, start, end }
}
/// Return the duration fo a beat in microseconds
#[inline] pub fn usec_per_beat (&self) -> f64 { 60_000_000f64 / self.bpm.get() }
/// Return the number of beats in a second
#[inline] pub fn beat_per_second (&self) -> f64 { self.bpm.get() / 60f64 }
/// Return the number of microseconds corresponding to a note of the given duration
#[inline] pub fn note_to_usec (&self, (num, den): (f64, f64)) -> f64 {
4.0 * self.usec_per_beat() * num / den
}
/// Return duration of a pulse in microseconds (BPM-dependent)
#[inline] pub fn pulse_per_usec (&self) -> f64 { self.ppq.get() / self.usec_per_beat() }
/// Return duration of a pulse in microseconds (BPM-dependent)
#[inline] pub fn usec_per_pulse (&self) -> f64 { self.usec_per_beat() / self.ppq.get() }
/// Return number of pulses to which a number of microseconds corresponds (BPM-dependent)
#[inline] pub fn usecs_to_pulse (&self, usec: f64) -> f64 { usec * self.pulse_per_usec() }
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
#[inline] pub fn pulses_to_usec (&self, pulse: f64) -> f64 { pulse / self.usec_per_pulse() }
/// Return number of pulses in a second (BPM-dependent)
#[inline] pub fn pulses_per_second (&self) -> f64 { self.beat_per_second() * self.ppq.get() }
/// Return fraction of a pulse to which a sample corresponds (SR- and BPM-dependent)
#[inline] pub fn pulses_per_sample (&self) -> f64 {
self.usec_per_pulse() / self.sr.usec_per_sample()
}
/// Return number of samples in a pulse (SR- and BPM-dependent)
#[inline] pub fn samples_per_pulse (&self) -> f64 {
self.sr.get() / self.pulses_per_second()
}
/// Convert a number of pulses to a sample number (SR- and BPM-dependent)
#[inline] pub fn pulses_to_sample (&self, p: f64) -> f64 {
self.pulses_per_sample() * p
}
/// Convert a number of samples to a pulse number (SR- and BPM-dependent)
#[inline] pub fn samples_to_pulse (&self, s: f64) -> f64 {
s / self.pulses_per_sample()
}
/// Return the number of samples corresponding to a note of the given duration
#[inline] pub fn note_to_samples (&self, note: (f64, f64)) -> f64 {
self.usec_to_sample(self.note_to_usec(note))
}
/// Return the number of samples corresponding to the given number of microseconds
#[inline] pub fn usec_to_sample (&self, usec: f64) -> f64 {
usec * self.sr.get() / 1000f64
}
/// Return the quantized position of a moment in time given a step
#[inline] pub fn quantize (&self, step: (f64, f64), time: f64) -> (f64, f64) {
let step = self.note_to_usec(step);
(time / step, time % step)
}
/// Quantize a collection of events
#[inline] pub fn quantize_into <E: Iterator<Item=(f64, f64)> + Sized, T> (
&self, step: (f64, f64), events: E
) -> Vec<(f64, f64)> {
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 {
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 a number of pulses into Beat.Bar starting from 0
#[inline] pub fn format_beats_0_short (&self, pulse: f64) -> String {
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 a number of pulses into Beat.Bar.Pulse starting from 1
#[inline] pub fn format_beats_1 (&self, pulse: f64) -> String {
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 a number of pulses into Beat.Bar.Pulse starting from 1
#[inline] pub fn format_beats_1_short (&self, pulse: f64) -> String {
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)
}
}
impl Default for Timebase {
fn default () -> Self { Self::new(48000f64, 150f64, DEFAULT_PPQ) }
}

58
src/time/perf.rs Normal file
View file

@ -0,0 +1,58 @@
use crate::*;
/// Performance counter
pub struct PerfModel {
pub enabled: bool,
clock: quanta::Clock,
// In nanoseconds
used: AtomicF64,
// In microseconds
period: AtomicF64,
}
pub trait HasPerf {
fn perf (&self) -> &PerfModel;
}
impl Default for PerfModel {
fn default () -> Self {
Self {
enabled: true,
clock: quanta::Clock::new(),
used: Default::default(),
period: Default::default(),
}
}
}
impl PerfModel {
pub fn get_t0 (&self) -> Option<u64> {
if self.enabled {
Some(self.clock.raw())
} else {
None
}
}
pub fn update (&self, t0: Option<u64>, scope: &ProcessScope) {
if let Some(t0) = t0 {
let t1 = self.clock.raw();
self.used.store(
self.clock.delta_as_nanos(t0, t1) as f64,
Relaxed,
);
self.period.store(
scope.cycle_times().unwrap().period_usecs as f64,
Relaxed,
);
}
}
pub fn percentage (&self) -> Option<f64> {
let period = self.period.load(Relaxed) * 1000.0;
if period > 0.0 {
let used = self.used.load(Relaxed);
Some(100.0 * used / period)
} else {
None
}
}
}

71
src/time/pulse.rs Normal file
View file

@ -0,0 +1,71 @@
use crate::*;
pub const DEFAULT_PPQ: f64 = 96.0;
/// FIXME: remove this and use PPQ from timebase everywhere:
pub const PPQ: usize = 96;
/// MIDI resolution in PPQ (pulses per quarter note)
#[derive(Debug, Default)] pub struct PulsesPerQuaver(AtomicF64);
impl_time_unit!(PulsesPerQuaver);
/// Timestamp in MIDI pulses
#[derive(Debug, Default)] pub struct Pulse(AtomicF64);
impl_time_unit!(Pulse);
/// Tempo in beats per minute
#[derive(Debug, Default)] pub struct BeatsPerMinute(AtomicF64);
impl_time_unit!(BeatsPerMinute);
/// Quantization setting for launching clips
#[derive(Debug, Default)] pub struct LaunchSync(AtomicF64);
impl_time_unit!(LaunchSync);
impl LaunchSync {
pub fn next (&self) -> f64 {
Note::next(self.get() as usize) as f64
}
pub fn prev (&self) -> f64 {
Note::prev(self.get() as usize) as f64
}
}
/// Quantization setting for notes
#[derive(Debug, Default)] pub struct Quantize(AtomicF64);
impl_time_unit!(Quantize);
impl Quantize {
pub fn next (&self) -> f64 {
Note::next(self.get() as usize) as f64
}
pub fn prev (&self) -> f64 {
Note::prev(self.get() as usize) as f64
}
}
/// Iterator that emits subsequent ticks within a range.
pub struct TicksIterator {
pub spp: f64,
pub sample: usize,
pub start: usize,
pub end: usize,
}
impl Iterator for TicksIterator {
type Item = (usize, usize);
fn next (&mut self) -> Option<Self::Item> {
loop {
if self.sample > self.end { return None }
let spp = self.spp;
let sample = self.sample as f64;
let start = self.start;
let end = self.end;
self.sample += 1;
//println!("{spp} {sample} {start} {end}");
let jitter = sample.rem_euclid(spp); // ramps
let next_jitter = (sample + 1.0).rem_euclid(spp);
if jitter > next_jitter { // at crossing:
let time = (sample as usize) % (end as usize-start as usize);
let tick = (sample / spp) as usize;
return Some((time, tick))
}
}
}
}

39
src/time/sr.rs Normal file
View file

@ -0,0 +1,39 @@
use crate::*;
/// Timestamp in microseconds
#[derive(Debug, Default)] pub struct Microsecond(AtomicF64);
impl_time_unit!(Microsecond);
impl Microsecond {
#[inline] pub fn format_msu (&self) -> String {
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}")
}
}
/// Audio sample rate in Hz (samples per second)
#[derive(Debug, Default)] pub struct SampleRate(AtomicF64);
impl_time_unit!(SampleRate);
impl SampleRate {
/// Return the duration of a sample in microseconds (floating)
#[inline] pub fn usec_per_sample (&self) -> f64 {
1_000_000f64 / self.get()
}
/// Return the duration of a sample in microseconds (floating)
#[inline] pub fn sample_per_usec (&self) -> f64 {
self.get() / 1_000_000f64
}
/// Convert a number of samples to microseconds (floating)
#[inline] pub fn samples_to_usec (&self, samples: f64) -> f64 {
self.usec_per_sample() * samples
}
/// Convert a number of microseconds to samples (floating)
#[inline] pub fn usecs_to_sample (&self, usecs: f64) -> f64 {
self.sample_per_usec() * usecs
}
}
/// Timestamp in audio samples
#[derive(Debug, Default)] pub struct SampleCount(AtomicF64);
impl_time_unit!(SampleCount);

59
src/time/unit.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::*;
/// A unit of time, represented as an atomic 64-bit float.
///
/// According to https://stackoverflow.com/a/873367, as per IEEE754,
/// every integer between 1 and 2^53 can be represented exactly.
/// This should mean that, even at 192kHz sampling rate, over 1 year of audio
/// can be clocked in microseconds with f64 without losing precision.
pub trait TimeUnit: InteriorMutable<f64> {}
/// Implement an arithmetic operation for a unit of time
#[macro_export] macro_rules! impl_op {
($T:ident, $Op:ident, $method:ident, |$a:ident,$b:ident|{$impl:expr}) => {
impl $Op<Self> for $T {
type Output = Self; #[inline] fn $method (self, other: Self) -> Self::Output {
let $a = self.get(); let $b = other.get(); Self($impl.into())
}
}
impl $Op<usize> for $T {
type Output = Self; #[inline] fn $method (self, other: usize) -> Self::Output {
let $a = self.get(); let $b = other as f64; Self($impl.into())
}
}
impl $Op<f64> for $T {
type Output = Self; #[inline] fn $method (self, other: f64) -> Self::Output {
let $a = self.get(); let $b = other; Self($impl.into())
}
}
}
}
/// Define and implement a unit of time
#[macro_export] macro_rules! impl_time_unit {
($T:ident) => {
impl Gettable<f64> for $T {
fn get (&self) -> f64 { self.0.load(Relaxed) }
}
impl InteriorMutable<f64> for $T {
fn set (&self, value: f64) -> f64 {
let old = self.get();
self.0.store(value, Relaxed);
old
}
}
impl TimeUnit for $T {}
impl_op!($T, Add, add, |a, b|{a + b});
impl_op!($T, Sub, sub, |a, b|{a - b});
impl_op!($T, Mul, mul, |a, b|{a * b});
impl_op!($T, Div, div, |a, b|{a / b});
impl_op!($T, Rem, rem, |a, b|{a % b});
impl From<f64> for $T { fn from (value: f64) -> Self { Self(value.into()) } }
impl From<usize> for $T { fn from (value: usize) -> Self { Self((value as f64).into()) } }
impl From<$T> for f64 { fn from (value: $T) -> Self { value.get() } }
impl From<$T> for usize { fn from (value: $T) -> Self { value.get() as usize } }
impl From<&$T> for f64 { fn from (value: &$T) -> Self { value.get() } }
impl From<&$T> for usize { fn from (value: &$T) -> Self { value.get() as usize } }
impl Clone for $T { fn clone (&self) -> Self { Self(self.get().into()) } }
}
}

374
src/transport.rs Normal file
View file

@ -0,0 +1,374 @@
use crate::*;
use ClockCommand::{Play, Pause, SetBpm, SetQuant, SetSync};
use TransportCommand::{Focus, Clock};
use FocusCommand::{Next, Prev};
use KeyCode::{Enter, Left, Right, Char};
/// Transport clock app.
pub struct TransportTui {
pub jack: Arc<RwLock<JackClient>>,
pub clock: ClockModel,
pub size: Measure<Tui>,
pub cursor: (usize, usize),
pub focus: TransportFocus,
}
from_jack!(|jack|TransportTui Self {
jack: jack.clone(),
clock: ClockModel::from(jack),
size: Measure::new(),
cursor: (0, 0),
focus: TransportFocus::PlayPause
});
has_clock!(|self:TransportTui|&self.clock);
audio!(|self:TransportTui,client,scope|ClockAudio(self).process(client, scope));
handle!(<Tui>|self:TransportTui,from|TransportCommand::execute_with_state(self, from));
render!(<Tui>|self: TransportTui|TransportView::from((self, None, true)));
impl std::fmt::Debug for TransportTui {
fn fmt (&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
f.debug_struct("TransportTui")
.field("jack", &self.jack)
.field("size", &self.size)
.field("cursor", &self.cursor)
.finish()
}
}
pub struct TransportView {
color: ItemPalette,
focused: bool,
sr: String,
chunk: String,
latency: String,
bpm: String,
ppq: String,
beat: String,
global_sample: String,
global_second: String,
started: bool,
current_sample: f64,
current_second: f64,
}
impl<T: HasClock> From<(&T, Option<ItemPalette>, bool)> for TransportView {
fn from ((state, color, focused): (&T, Option<ItemPalette>, bool)) -> Self {
let clock = state.clock();
let rate = clock.timebase.sr.get();
let chunk = clock.chunk.load(Relaxed);
let latency = chunk as f64 / rate * 1000.;
let sr = format!("{:.1}k", rate / 1000.0);
let bpm = format!("{:.3}", clock.timebase.bpm.get());
let ppq = format!("{:.0}", clock.timebase.ppq.get());
let chunk = format!("{chunk}");
let latency = format!("{latency}");
let color = color.unwrap_or(ItemPalette::from(TuiTheme::g(32)));
if let Some(started) = clock.started.read().unwrap().as_ref() {
let current_sample = (clock.global.sample.get() - started.sample.get())/1000.;
let current_usec = clock.global.usec.get() - started.usec.get();
let current_second = current_usec/1000000.;
Self {
color, focused, sr, bpm, ppq, chunk, latency,
started: true,
global_sample: format!("{:.0}k", started.sample.get()/1000.),
global_second: format!("{:.1}s", started.usec.get()/1000.),
current_sample,
current_second,
beat: clock.timebase.format_beats_0(
clock.timebase.usecs_to_pulse(current_usec)
),
}
} else {
Self {
color, focused, sr, bpm, ppq, chunk, latency,
started: false,
global_sample: format!("{:.0}k", clock.global.sample.get()/1000.),
global_second: format!("{:.1}s", clock.global.usec.get()/1000000.),
current_sample: 0.0,
current_second: 0.0,
beat: format!("000.0.00")
}
}
}
}
render!(<Tui>|self: TransportView|{
let color = self.color;
struct Field<'a>(&'a str, &'a str, &'a ItemPalette);
render!(<Tui>|self: Field<'a>|row!([
Tui::fg_bg(self.2.lightest.rgb, self.2.base.rgb, Tui::bold(true, self.0)),
Tui::fg_bg(self.2.base.rgb, self.2.darkest.rgb, ""),
Tui::fg_bg(self.2.lightest.rgb, self.2.darkest.rgb, format!("{:>10}", self.1)),
Tui::fg_bg(self.2.darkest.rgb, self.2.base.rgb, ""),
]));
Tui::bg(color.base.rgb, Fill::w(row!([
//PlayPause(self.started), " ",
col!([
Field(" Beat", self.beat.as_str(), &color),
Field(" Time", format!("{:.1}s", self.current_second).as_str(), &color),
]),
col!([
Field(" BPM", self.bpm.as_str(), &color),
//Field(" Smpl", format!("{:.1}k", self.current_sample).as_str(), &color),
Field(" Rate", format!("{}", self.sr).as_str(), &color),
//Field(" CPU%", format!("{:.1}ms", self.perf).as_str(), &color),
]),
col!([
Field(" Chunk", format!("{}", self.chunk).as_str(), &color),
Field(" Lag", format!("{:.3}ms", self.latency).as_str(), &color),
]),
])))
});
pub struct PlayPause(pub bool);
render!(<Tui>|self: PlayPause|Tui::bg(
if self.0{Color::Rgb(0,128,0)}else{Color::Rgb(128,64,0)},
Fixed::w(5, col!(|add|if self.0 {
add(&Tui::fg(Color::Rgb(0, 255, 0), col!([
" 🭍🭑🬽 ",
" 🭞🭜🭘 ",
])))
} else {
add(&Tui::fg(Color::Rgb(255, 128, 0), col!([
" ▗▄▖ ",
" ▝▀▘ ",
])))
}))
));
impl HasFocus for TransportTui {
type Item = TransportFocus;
fn focused (&self) -> Self::Item {
self.focus
}
fn set_focused (&mut self, to: Self::Item) {
self.focus = to
}
}
/// Which item of the transport toolbar is focused
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TransportFocus {
Bpm,
Sync,
PlayPause,
Clock,
Quant,
}
impl FocusWrap<TransportFocus> for TransportFocus {
fn wrap <'a, W: Render<Tui>> (self, focus: TransportFocus, content: &'a W)
-> impl Render<Tui> + 'a
{
let focused = focus == self;
let corners = focused.then_some(CORNERS);
//let highlight = focused.then_some(Tui::bg(Color::Rgb(60, 70, 50)));
lay!([corners, /*highlight,*/ *content])
}
}
impl FocusWrap<TransportFocus> for Option<TransportFocus> {
fn wrap <'a, W: Render<Tui>> (self, focus: TransportFocus, content: &'a W)
-> impl Render<Tui> + 'a
{
let focused = Some(focus) == self;
let corners = focused.then_some(CORNERS);
//let highlight = focused.then_some(Background(Color::Rgb(60, 70, 50)));
lay!([corners, /*highlight,*/ *content])
}
}
pub trait TransportControl<T>: HasClock + {
fn transport_focused (&self) -> Option<TransportFocus>;
}
impl TransportControl<TransportFocus> for TransportTui {
fn transport_focused (&self) -> Option<TransportFocus> {
Some(self.focus)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum TransportCommand {
Focus(FocusCommand<TransportFocus>),
Clock(ClockCommand),
}
command!(|self:TransportCommand,state:TransportTui|match self {
//Self::Focus(cmd) => cmd.execute(state)?.map(Self::Focus),
Self::Clock(cmd) => cmd.execute(state)?.map(Self::Clock),
_ => unreachable!(),
});
//command!(|self:TransportFocus,state:TransportTui|{
//if let FocusCommand::Set(to) = self { state.set_focused(to); }
//Ok(None)
//});
impl InputToCommand<Tui, TransportTui> for TransportCommand {
fn input_to_command (state: &TransportTui, input: &TuiInput) -> Option<Self> {
to_transport_command(state, input)
.or_else(||to_focus_command(input).map(TransportCommand::Focus))
}
}
pub fn to_transport_command <T, U> (state: &T, input: &TuiInput) -> Option<TransportCommand>
where
T: TransportControl<U>,
U: Into<Option<TransportFocus>>,
{
Some(match input.event() {
key_pat!(Left) => Focus(Prev),
key_pat!(Right) => Focus(Next),
key_pat!(Char(' ')) => Clock(if state.clock().is_stopped() {
Play(None)
} else {
Pause(None)
}),
key_pat!(Shift-Char(' ')) => Clock(if state.clock().is_stopped() {
Play(Some(0))
} else {
Pause(Some(0))
}),
_ => match state.transport_focused().unwrap() {
TransportFocus::Bpm => to_bpm_command(input, state.clock().bpm().get())?,
TransportFocus::Quant => to_quant_command(input, &state.clock().quant)?,
TransportFocus::Sync => to_sync_command(input, &state.clock().sync)?,
TransportFocus::Clock => to_seek_command(input)?,
TransportFocus::PlayPause => match input.event() {
key_pat!(Enter) => Clock(
if state.clock().is_stopped() {
Play(None)
} else {
Pause(None)
}
),
key_pat!(Shift-Enter) => Clock(
if state.clock().is_stopped() {
Play(Some(0))
} else {
Pause(Some(0))
}
),
_ => return None,
},
}
})
}
fn to_bpm_command (input: &TuiInput, bpm: f64) -> Option<TransportCommand> {
Some(match input.event() {
key_pat!(Char(',')) => Clock(SetBpm(bpm - 1.0)),
key_pat!(Char('.')) => Clock(SetBpm(bpm + 1.0)),
key_pat!(Char('<')) => Clock(SetBpm(bpm - 0.001)),
key_pat!(Char('>')) => Clock(SetBpm(bpm + 0.001)),
_ => return None,
})
}
fn to_quant_command (input: &TuiInput, quant: &Quantize) -> Option<TransportCommand> {
Some(match input.event() {
key_pat!(Char(',')) => Clock(SetQuant(quant.prev())),
key_pat!(Char('.')) => Clock(SetQuant(quant.next())),
key_pat!(Char('<')) => Clock(SetQuant(quant.prev())),
key_pat!(Char('>')) => Clock(SetQuant(quant.next())),
_ => return None,
})
}
fn to_sync_command (input: &TuiInput, sync: &LaunchSync) -> Option<TransportCommand> {
Some(match input.event() {
key_pat!(Char(',')) => Clock(SetSync(sync.prev())),
key_pat!(Char('.')) => Clock(SetSync(sync.next())),
key_pat!(Char('<')) => Clock(SetSync(sync.prev())),
key_pat!(Char('>')) => Clock(SetSync(sync.next())),
_ => return None,
})
}
fn to_seek_command (input: &TuiInput) -> Option<TransportCommand> {
Some(match input.event() {
key_pat!(Char(',')) => todo!("transport seek bar"),
key_pat!(Char('.')) => todo!("transport seek bar"),
key_pat!(Char('<')) => todo!("transport seek beat"),
key_pat!(Char('>')) => todo!("transport seek beat"),
_ => return None,
})
}
///////////////////////////////////////////////////////////////////////////////////////////////////
//struct Field(&'static str, String);
//render!(|self: Field|{
//Tui::to_east("│", Tui::to_east(
//Tui::bold(true, self.0),
//Tui::bg(Color::Rgb(0, 0, 0), self.1.as_str()),
//))
//});
//pub struct TransportView {
//pub(crate) state: Option<TransportState>,
//pub(crate) selected: Option<TransportFocus>,
//pub(crate) focused: bool,
//pub(crate) bpm: f64,
//pub(crate) sync: f64,
//pub(crate) quant: f64,
//pub(crate) beat: String,
//pub(crate) msu: String,
//}
////)?;
////match *state {
////Some(TransportState::Rolling) => {
////add(&row!(
////"│",
////TuiStyle::fg("▶ PLAYING", Color::Rgb(0, 255, 0)),
////format!("│0 (0)"),
////format!("│00m00s000u"),
////format!("│00B 0b 00/00")
////))?;
////add(&row!("│Now ", row!(
////format!("│0 (0)"), //sample(chunk)
////format!("│00m00s000u"), //msu
////format!("│00B 0b 00/00"), //bbt
////)))?;
////},
////_ => {
////add(&row!("│", TuiStyle::fg("⏹ STOPPED", Color::Rgb(255, 128, 0))))?;
////add(&"")?;
////}
////}
////Ok(())
////}).fill_x().bg(Color::Rgb(40, 50, 30))
////});
//impl<'a, T: HasClock> From<&'a T> for TransportView where Option<TransportFocus>: From<&'a T> {
//fn from (state: &'a T) -> Self {
//let selected = state.into();
//Self {
//selected,
//focused: selected.is_some(),
//state: Some(state.clock().transport.query_state().unwrap()),
//bpm: state.clock().bpm().get(),
//sync: state.clock().sync.get(),
//quant: state.clock().quant.get(),
//beat: state.clock().playhead.format_beat(),
//msu: state.clock().playhead.usec.format_msu(),
//}
//}
//}
//row!(
////selected.wrap(TransportFocus::PlayPause, &play_pause.fixed_xy(10, 3)),
//row!(
//col!(
//Field("SR ", format!("192000")),
//Field("BUF ", format!("1024")),
//Field("LEN ", format!("21300")),
//Field("CPU ", format!("00.0%"))
//),
//col!(
//Field("PUL ", format!("000000000")),
//Field("PPQ ", format!("96")),
//Field("BBT ", format!("00B0b00p"))
//),
//col!(
//Field("SEC ", format!("000000.000")),
//Field("BPM ", format!("000.000")),
//Field("MSU ", format!("00m00s00u"))
//),
//),
//selected.wrap(TransportFocus::Bpm, &Outset::X(1u16, {
//row! {
//"BPM ",
//format!("{}.{:03}", *bpm as usize, (bpm * 1000.0) % 1000.0)
//}
//})),
//selected.wrap(TransportFocus::Sync, &Outset::X(1u16, row! {
//"SYNC ", pulses_to_name(*sync as usize)
//})),
//selected.wrap(TransportFocus::Quant, &Outset::X(1u16, row! {
//"QUANT ", pulses_to_name(*quant as usize)
//})),
//selected.wrap(TransportFocus::Clock, &{
//row!("B" , beat.as_str(), " T", msu.as_str()).outset_x(1)
//}).align_e().fill_x(),
//).fill_x().bg(Color::Rgb(40, 50, 30))

Some files were not shown because too many files have changed in this diff Show more