mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-02-01 08:36:42 +01:00
flatten workspace into 1 crate
This commit is contained in:
parent
7c4e1e2166
commit
d926422c67
147 changed files with 66 additions and 126 deletions
65
src/README.md
Normal file
65
src/README.md
Normal 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
1
src/api.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
mod sampler; pub(crate) use sampler::*;
|
||||
4
src/architecture.svg
Normal file
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
162
src/arranger.rs
Normal 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));
|
||||
339
src/arranger/arranger_command.rs
Normal file
339
src/arranger/arranger_command.rs
Normal 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
|
||||
});
|
||||
1
src/arranger/arranger_h.rs
Normal file
1
src/arranger/arranger_h.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
// TODO
|
||||
26
src/arranger/arranger_mode.rs
Normal file
26
src/arranger/arranger_mode.rs
Normal 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()))
|
||||
}
|
||||
91
src/arranger/arranger_scene.rs
Normal file
91
src/arranger/arranger_scene.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
60
src/arranger/arranger_select.rs
Normal file
60
src/arranger/arranger_select.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/arranger/arranger_track.rs
Normal file
111
src/arranger/arranger_track.rs
Normal 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
|
||||
}
|
||||
}
|
||||
23
src/arranger/arranger_v.rs
Normal file
23
src/arranger/arranger_v.rs
Normal 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)),
|
||||
])
|
||||
}
|
||||
}
|
||||
62
src/arranger/arranger_v/v_clips.rs
Normal file
62
src/arranger/arranger_v/v_clips.rs
Normal 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(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
81
src/arranger/arranger_v/v_cursor.rs
Normal file
81
src/arranger/arranger_v/v_cursor.rs
Normal 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)?
|
||||
})
|
||||
}));
|
||||
83
src/arranger/arranger_v/v_head.rs
Normal file
83
src/arranger/arranger_v/v_head.rs
Normal 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 = ¤t.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)
|
||||
}
|
||||
}
|
||||
26
src/arranger/arranger_v/v_io.rs
Normal file
26
src/arranger/arranger_v/v_io.rs
Normal 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>|());
|
||||
48
src/arranger/arranger_v/v_sep.rs
Normal file
48
src/arranger/arranger_v/v_sep.rs
Normal 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
1
src/audio.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
use crate::*;
|
||||
69
src/core.rs
Normal file
69
src/core.rs
Normal 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
105
src/core/color.rs
Normal 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
116
src/core/command.rs
Normal 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
49
src/core/engine.rs
Normal 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
308
src/core/focus.rs
Normal 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
68
src/core/input.rs
Normal 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
160
src/core/output.rs
Normal 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
49
src/core/test.rs
Normal 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
201
src/edn.rs
Normal 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
268
src/groovebox.rs
Normal 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
377
src/jack.rs
Normal 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
50
src/jack/activate.rs
Normal 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
50
src/jack/client.rs
Normal 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
69
src/jack/jack_event.rs
Normal 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
109
src/jack/ports.rs
Normal 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
90
src/lib.rs
Normal 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
8
src/meter.rs
Normal 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
352
src/midi.rs
Normal 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
109
src/midi/midi_clip.rs
Normal 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
225
src/midi/midi_editor.rs
Normal 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
34
src/midi/midi_launch.rs
Normal 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
36
src/midi/midi_note.rs
Normal 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
149
src/midi/midi_play.rs
Normal 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
50
src/midi/midi_point.rs
Normal 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
93
src/midi/midi_pool.rs
Normal 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
64
src/midi/midi_range.rs
Normal 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
82
src/midi/midi_rec.rs
Normal 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
26
src/midi/midi_view.rs
Normal 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
263
src/mixer.rs
Normal 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
244
src/piano_h.rs
Normal 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);
|
||||
//}
|
||||
//}
|
||||
33
src/piano_h/piano_h_cursor.rs
Normal file
33
src/piano_h/piano_h_cursor.rs
Normal 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
|
||||
}
|
||||
}
|
||||
})));
|
||||
56
src/piano_h/piano_h_keys.rs
Normal file
56
src/piano_h/piano_h_keys.rs
Normal 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!(),
|
||||
}
|
||||
}
|
||||
46
src/piano_h/piano_h_notes.rs
Normal file
46
src/piano_h/piano_h_notes.rs
Normal 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(());
|
||||
//}
|
||||
})));
|
||||
20
src/piano_h/piano_h_time.rs
Normal file
20
src/piano_h/piano_h_time.rs
Normal 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
1
src/piano_v.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
// TODO
|
||||
266
src/plugin.rs
Normal file
266
src/plugin.rs
Normal 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
40
src/plugin/lv2.rs
Normal 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
59
src/plugin/lv2_gui.rs
Normal 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
47
src/plugin/lv2_tui.rs
Normal 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
14
src/plugin/vst2_tui.rs
Normal 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
2
src/plugin/vst3_tui.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
//! TODO
|
||||
|
||||
317
src/pool.rs
Normal file
317
src/pool.rs
Normal 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
144
src/pool/phrase_length.rs
Normal 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
60
src/pool/phrase_rename.rs
Normal 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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/pool/phrase_selector.rs
Normal file
65
src/pool/phrase_selector.rs
Normal 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
233
src/sampler.rs
Normal 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
121
src/sampler/sample.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
246
src/sampler/sample_import.rs
Normal file
246
src/sampler/sample_import.rs
Normal 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)
|
||||
//}]
|
||||
//});
|
||||
52
src/sampler/sample_viewer.rs
Normal file
52
src/sampler/sample_viewer.rs
Normal 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
155
src/sampler/sampler_tui.rs
Normal 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
30
src/sampler/voice.rs
Normal 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
178
src/sequencer.rs
Normal 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
236
src/space.rs
Normal 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
96
src/space/align.rs
Normal 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
103
src/space/bsp.rs
Normal 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
79
src/space/collect.rs
Normal 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
65
src/space/cond.rs
Normal 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
47
src/space/fill.rs
Normal 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
54
src/space/fixed.rs
Normal 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
92
src/space/inset_outset.rs
Normal 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
53
src/space/layers.rs
Normal 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
33
src/space/map_reduce.rs
Normal 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
93
src/space/measure.rs
Normal 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
95
src/space/min_max.rs
Normal 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
128
src/space/push_pull.rs
Normal 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
8
src/space/scroll.rs
Normal 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
103
src/space/shrink_grow.rs
Normal 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
71
src/space/split.rs
Normal 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
153
src/space/stack.rs
Normal 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
4
src/status.rs
Normal 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::*;
|
||||
52
src/status/status_arranger.rs
Normal file
52
src/status/status_arranger.rs
Normal 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
31
src/status/status_edit.rs
Normal 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()))
|
||||
]))))
|
||||
});
|
||||
50
src/status/status_groovebox.rs
Normal file
50
src/status/status_groovebox.rs
Normal 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])
|
||||
}
|
||||
}
|
||||
50
src/status/status_sequencer.rs
Normal file
50
src/status/status_sequencer.rs
Normal 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
183
src/test.rs
Normal 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
16
src/time.rs
Normal 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
193
src/time/clock.rs
Normal 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
181
src/time/moment.rs
Normal 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
58
src/time/perf.rs
Normal 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
71
src/time/pulse.rs
Normal 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
39
src/time/sr.rs
Normal 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
59
src/time/unit.rs
Normal 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
374
src/transport.rs
Normal 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
Loading…
Add table
Add a link
Reference in a new issue