mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-01-31 16:36:40 +01:00
Compare commits
4 commits
7b432d12b4
...
4cf82af950
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cf82af950 | |||
| 77703d83a5 | |||
| c367a0444e | |||
| 8adbdc5bc7 |
115 changed files with 980 additions and 966 deletions
48
Cargo.lock
generated
48
Cargo.lock
generated
|
|
@ -1518,11 +1518,8 @@ dependencies = [
|
||||||
"proptest",
|
"proptest",
|
||||||
"proptest-derive",
|
"proptest-derive",
|
||||||
"rand",
|
"rand",
|
||||||
"tek_jack",
|
"tek_device",
|
||||||
"tek_midi",
|
"tek_engine",
|
||||||
"tek_plugin",
|
|
||||||
"tek_sampler",
|
|
||||||
"tek_time",
|
|
||||||
"tengri",
|
"tengri",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
@ -1536,53 +1533,26 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tek_jack"
|
name = "tek_device"
|
||||||
version = "0.2.1"
|
|
||||||
dependencies = [
|
|
||||||
"jack",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tek_midi"
|
|
||||||
version = "0.2.1"
|
|
||||||
dependencies = [
|
|
||||||
"midly",
|
|
||||||
"tek_jack",
|
|
||||||
"tek_time",
|
|
||||||
"tengri",
|
|
||||||
"uuid",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tek_plugin"
|
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"livi",
|
"livi",
|
||||||
"tek_jack",
|
|
||||||
"tek_midi",
|
|
||||||
"tek_time",
|
|
||||||
"tengri",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tek_sampler"
|
|
||||||
version = "0.2.1"
|
|
||||||
dependencies = [
|
|
||||||
"symphonia",
|
"symphonia",
|
||||||
"tek_jack",
|
"tek_engine",
|
||||||
"tek_midi",
|
|
||||||
"tek_time",
|
|
||||||
"tengri",
|
"tengri",
|
||||||
|
"uuid",
|
||||||
"wavers",
|
"wavers",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tek_time"
|
name = "tek_engine"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic_float",
|
"atomic_float",
|
||||||
"tek_jack",
|
"jack",
|
||||||
|
"midly",
|
||||||
"tengri",
|
"tengri",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
14
Cargo.toml
14
Cargo.toml
|
|
@ -5,13 +5,10 @@ version = "0.2.1"
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
|
"./crates/engine",
|
||||||
|
"./crates/device",
|
||||||
"./crates/app",
|
"./crates/app",
|
||||||
"./crates/cli",
|
"./crates/cli",
|
||||||
"./crates/jack",
|
|
||||||
"./crates/midi",
|
|
||||||
"./crates/plugin",
|
|
||||||
"./crates/sampler",
|
|
||||||
"./crates/time"
|
|
||||||
]
|
]
|
||||||
exclude = [
|
exclude = [
|
||||||
"./deps/tengri"
|
"./deps/tengri"
|
||||||
|
|
@ -37,13 +34,10 @@ path = "./deps/rust-jack"
|
||||||
#default-features = false
|
#default-features = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
tek_device = { path = "./crates/device" }
|
||||||
|
tek_engine = { path = "./crates/engine" }
|
||||||
tek = { path = "./crates/app" }
|
tek = { path = "./crates/app" }
|
||||||
tek_cli = { path = "./crates/cli" }
|
tek_cli = { path = "./crates/cli" }
|
||||||
tek_jack = { path = "./crates/jack" }
|
|
||||||
tek_midi = { path = "./crates/midi" }
|
|
||||||
tek_plugin = { path = "./crates/plugin", default-features = false }
|
|
||||||
tek_sampler = { path = "./crates/sampler" }
|
|
||||||
tek_time = { path = "./crates/time" }
|
|
||||||
|
|
||||||
atomic_float = { version = "1.0.0" }
|
atomic_float = { version = "1.0.0" }
|
||||||
backtrace = { version = "0.3.72" }
|
backtrace = { version = "0.3.72" }
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,8 @@ version = { workspace = true }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tengri = { workspace = true }
|
tengri = { workspace = true }
|
||||||
|
|
||||||
tek_jack = { workspace = true }
|
tek_engine = { workspace = true }
|
||||||
tek_time = { workspace = true }
|
tek_device = { workspace = true }
|
||||||
tek_midi = { workspace = true }
|
|
||||||
tek_sampler = { workspace = true }
|
|
||||||
tek_plugin = { workspace = true, optional = true }
|
|
||||||
|
|
||||||
backtrace = { workspace = true }
|
backtrace = { workspace = true }
|
||||||
clap = { workspace = true, optional = true }
|
clap = { workspace = true, optional = true }
|
||||||
|
|
@ -25,4 +22,4 @@ proptest-derive = { workspace = true }
|
||||||
[features]
|
[features]
|
||||||
default = ["cli"]
|
default = ["cli"]
|
||||||
cli = ["clap"]
|
cli = ["clap"]
|
||||||
host = ["tek_plugin"]
|
host = ["tek_device/lv2"]
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ expose!([self: Tek]
|
||||||
(":track" self.selected.track()))
|
(":track" self.selected.track()))
|
||||||
([MaybeClip]
|
([MaybeClip]
|
||||||
(":clip" match self.selected {
|
(":clip" match self.selected {
|
||||||
Selection::Clip(t, s) => self.scenes[s].clips[t].clone(),
|
Selection::TrackClip { track, scene } => self.scenes[scene].clips[track].clone(),
|
||||||
_ => None
|
_ => None
|
||||||
}))
|
}))
|
||||||
([Selection]
|
([Selection]
|
||||||
|
|
@ -70,7 +70,7 @@ impose!([app: Tek]
|
||||||
(0, 0) => Self::Select(Selection::Mix),
|
(0, 0) => Self::Select(Selection::Mix),
|
||||||
(t, 0) => Self::Select(Selection::Track(t)),
|
(t, 0) => Self::Select(Selection::Track(t)),
|
||||||
(0, s) => Self::Select(Selection::Scene(s)),
|
(0, s) => Self::Select(Selection::Scene(s)),
|
||||||
(t, s) => Self::Select(Selection::Clip(t, s)) })))
|
(t, s) => Self::Select(Selection::TrackClip { track: t, scene: s }) })))
|
||||||
|
|
||||||
(ClipCommand:
|
(ClipCommand:
|
||||||
("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
||||||
|
|
|
||||||
3
crates/app/src/editor.rs
Normal file
3
crates/app/src/editor.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod editor_api;
|
||||||
|
mod editor_model;
|
||||||
|
mod editor_view;
|
||||||
97
crates/app/src/editor/editor_api.rs
Normal file
97
crates/app/src/editor/editor_api.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
provide!(bool: |self: MidiEditor| {
|
||||||
|
":true" => true,
|
||||||
|
":false" => false,
|
||||||
|
":time-lock" => self.time_lock().get(),
|
||||||
|
":time-lock-toggle" => !self.time_lock().get(),
|
||||||
|
});
|
||||||
|
|
||||||
|
provide!(usize: |self: MidiEditor| {
|
||||||
|
":note-length" => self.note_len(),
|
||||||
|
|
||||||
|
":note-pos" => self.note_pos(),
|
||||||
|
":note-pos-next" => self.note_pos() + 1,
|
||||||
|
":note-pos-prev" => self.note_pos().saturating_sub(1),
|
||||||
|
":note-pos-next-octave" => self.note_pos() + 12,
|
||||||
|
":note-pos-prev-octave" => self.note_pos().saturating_sub(12),
|
||||||
|
|
||||||
|
":note-len" => self.note_len(),
|
||||||
|
":note-len-next" => self.note_len() + 1,
|
||||||
|
":note-len-prev" => self.note_len().saturating_sub(1),
|
||||||
|
|
||||||
|
":note-range" => self.note_axis().get(),
|
||||||
|
":note-range-prev" => self.note_axis().get() + 1,
|
||||||
|
":note-range-next" => self.note_axis().get().saturating_sub(1),
|
||||||
|
|
||||||
|
":time-pos" => self.time_pos(),
|
||||||
|
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
|
||||||
|
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
|
||||||
|
|
||||||
|
":time-zoom" => self.time_zoom().get(),
|
||||||
|
":time-zoom-next" => self.time_zoom().get() + 1,
|
||||||
|
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
|
||||||
|
});
|
||||||
|
|
||||||
|
atom_command!(MidiEditCommand: |state: MidiEditor| {
|
||||||
|
("note/append" [] Some(Self::AppendNote))
|
||||||
|
("note/put" [] Some(Self::PutNote))
|
||||||
|
("note/del" [] Some(Self::DelNote))
|
||||||
|
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
|
||||||
|
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
|
||||||
|
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
|
||||||
|
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
|
||||||
|
("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock"))))
|
||||||
|
("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get())))
|
||||||
|
});
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)] pub enum MidiEditCommand {
|
||||||
|
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
||||||
|
AppendNote,
|
||||||
|
PutNote,
|
||||||
|
DelNote,
|
||||||
|
SetNoteCursor(usize),
|
||||||
|
SetNoteLength(usize),
|
||||||
|
SetNoteScroll(usize),
|
||||||
|
SetTimeCursor(usize),
|
||||||
|
SetTimeScroll(usize),
|
||||||
|
SetTimeZoom(usize),
|
||||||
|
SetTimeLock(bool),
|
||||||
|
Show(Option<Arc<RwLock<MidiClip>>>),
|
||||||
|
}
|
||||||
|
|
||||||
|
handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) {
|
||||||
|
command.execute(self)?;
|
||||||
|
Some(true)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}));
|
||||||
|
|
||||||
|
impl Command<MidiEditor> for MidiEditCommand {
|
||||||
|
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
||||||
|
use MidiEditCommand::*;
|
||||||
|
match self {
|
||||||
|
Show(clip) => { state.set_clip(clip.as_ref()); },
|
||||||
|
DelNote => {},
|
||||||
|
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) => {
|
||||||
|
let note_len = state.note_len();
|
||||||
|
let time_zoom = state.time_zoom().get();
|
||||||
|
state.set_note_len(x);
|
||||||
|
//if note_len / time_zoom != x / time_zoom {
|
||||||
|
state.redraw();
|
||||||
|
//}
|
||||||
|
},
|
||||||
|
SetTimeCursor(x) => { state.set_time_pos(x); },
|
||||||
|
SetNoteCursor(note) => { state.set_note_pos(note.min(127)); },
|
||||||
|
//_ => todo!("{:?}", self)
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -51,39 +51,6 @@ from!(|clip: Option<Arc<RwLock<MidiClip>>>|MidiEditor = {
|
||||||
model
|
model
|
||||||
});
|
});
|
||||||
|
|
||||||
provide!(bool: |self: MidiEditor| {
|
|
||||||
":true" => true,
|
|
||||||
":false" => false,
|
|
||||||
":time-lock" => self.time_lock().get(),
|
|
||||||
":time-lock-toggle" => !self.time_lock().get(),
|
|
||||||
});
|
|
||||||
|
|
||||||
provide!(usize: |self: MidiEditor| {
|
|
||||||
":note-length" => self.note_len(),
|
|
||||||
|
|
||||||
":note-pos" => self.note_pos(),
|
|
||||||
":note-pos-next" => self.note_pos() + 1,
|
|
||||||
":note-pos-prev" => self.note_pos().saturating_sub(1),
|
|
||||||
":note-pos-next-octave" => self.note_pos() + 12,
|
|
||||||
":note-pos-prev-octave" => self.note_pos().saturating_sub(12),
|
|
||||||
|
|
||||||
":note-len" => self.note_len(),
|
|
||||||
":note-len-next" => self.note_len() + 1,
|
|
||||||
":note-len-prev" => self.note_len().saturating_sub(1),
|
|
||||||
|
|
||||||
":note-range" => self.note_axis().get(),
|
|
||||||
":note-range-prev" => self.note_axis().get() + 1,
|
|
||||||
":note-range-next" => self.note_axis().get().saturating_sub(1),
|
|
||||||
|
|
||||||
":time-pos" => self.time_pos(),
|
|
||||||
":time-pos-next" => self.time_pos() + self.time_zoom().get(),
|
|
||||||
":time-pos-prev" => self.time_pos().saturating_sub(self.time_zoom().get()),
|
|
||||||
|
|
||||||
":time-zoom" => self.time_zoom().get(),
|
|
||||||
":time-zoom-next" => self.time_zoom().get() + 1,
|
|
||||||
":time-zoom-prev" => self.time_zoom().get().saturating_sub(1).max(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
impl MidiEditor {
|
impl MidiEditor {
|
||||||
|
|
||||||
/// Put note at current position
|
/// Put note at current position
|
||||||
|
|
@ -181,64 +148,3 @@ impl MidiViewer for MidiEditor {
|
||||||
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
fn set_clip (&mut self, p: Option<&Arc<RwLock<MidiClip>>>) { self.mode.set_clip(p) }
|
||||||
}
|
}
|
||||||
|
|
||||||
atom_command!(MidiEditCommand: |state: MidiEditor| {
|
|
||||||
("note/append" [] Some(Self::AppendNote))
|
|
||||||
("note/put" [] Some(Self::PutNote))
|
|
||||||
("note/del" [] Some(Self::DelNote))
|
|
||||||
("note/pos" [a: usize] Some(Self::SetNoteCursor(a.expect("no note cursor"))))
|
|
||||||
("note/len" [a: usize] Some(Self::SetNoteLength(a.expect("no note length"))))
|
|
||||||
("time/pos" [a: usize] Some(Self::SetTimeCursor(a.expect("no time cursor"))))
|
|
||||||
("time/zoom" [a: usize] Some(Self::SetTimeZoom(a.expect("no time zoom"))))
|
|
||||||
("time/lock" [a: bool] Some(Self::SetTimeLock(a.expect("no time lock"))))
|
|
||||||
("time/lock" [] Some(Self::SetTimeLock(!state.time_lock().get())))
|
|
||||||
});
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)] pub enum MidiEditCommand {
|
|
||||||
// TODO: 1-9 seek markers that by default start every 8th of the clip
|
|
||||||
AppendNote,
|
|
||||||
PutNote,
|
|
||||||
DelNote,
|
|
||||||
SetNoteCursor(usize),
|
|
||||||
SetNoteLength(usize),
|
|
||||||
SetNoteScroll(usize),
|
|
||||||
SetTimeCursor(usize),
|
|
||||||
SetTimeScroll(usize),
|
|
||||||
SetTimeZoom(usize),
|
|
||||||
SetTimeLock(bool),
|
|
||||||
Show(Option<Arc<RwLock<MidiClip>>>),
|
|
||||||
}
|
|
||||||
|
|
||||||
handle!(TuiIn: |self: MidiEditor, input|Ok(if let Some(command) = self.keys.command(self, input) {
|
|
||||||
command.execute(self)?;
|
|
||||||
Some(true)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}));
|
|
||||||
|
|
||||||
impl Command<MidiEditor> for MidiEditCommand {
|
|
||||||
fn execute (self, state: &mut MidiEditor) -> Perhaps<Self> {
|
|
||||||
use MidiEditCommand::*;
|
|
||||||
match self {
|
|
||||||
Show(clip) => { state.set_clip(clip.as_ref()); },
|
|
||||||
DelNote => {},
|
|
||||||
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) => {
|
|
||||||
let note_len = state.note_len();
|
|
||||||
let time_zoom = state.time_zoom().get();
|
|
||||||
state.set_note_len(x);
|
|
||||||
//if note_len / time_zoom != x / time_zoom {
|
|
||||||
state.redraw();
|
|
||||||
//}
|
|
||||||
},
|
|
||||||
SetTimeCursor(x) => { state.set_time_pos(x); },
|
|
||||||
SetNoteCursor(note) => { state.set_note_pos(note.min(127)); },
|
|
||||||
//_ => todo!("{:?}", self)
|
|
||||||
}
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -325,3 +325,39 @@ fn to_key (note: usize) -> &'static str {
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct OctaveVertical {
|
||||||
|
on: [bool; 12],
|
||||||
|
colors: [Color; 3]
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for OctaveVertical {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self {
|
||||||
|
on: [false; 12],
|
||||||
|
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OctaveVertical {
|
||||||
|
fn color (&self, pitch: usize) -> Color {
|
||||||
|
let pitch = pitch % 12;
|
||||||
|
self.colors[if self.on[pitch] { 2 } else {
|
||||||
|
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Content<TuiOut> for OctaveVertical {
|
||||||
|
fn content (&self) -> impl Render<TuiOut> {
|
||||||
|
row!(
|
||||||
|
Tui::fg_bg(self.color(0), self.color(1), "▙"),
|
||||||
|
Tui::fg_bg(self.color(2), self.color(3), "▙"),
|
||||||
|
Tui::fg_bg(self.color(4), self.color(5), "▌"),
|
||||||
|
Tui::fg_bg(self.color(6), self.color(7), "▟"),
|
||||||
|
Tui::fg_bg(self.color(8), self.color(9), "▟"),
|
||||||
|
Tui::fg_bg(self.color(10), self.color(11), "▟"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,6 +41,9 @@ mod audio; pub use self::audio::*;
|
||||||
mod model; pub use self::model::*;
|
mod model; pub use self::model::*;
|
||||||
mod view; pub use self::view::*;
|
mod view; pub use self::view::*;
|
||||||
|
|
||||||
|
mod pool;
|
||||||
|
mod editor;
|
||||||
|
|
||||||
#[cfg(test)] #[test] fn test_model () {
|
#[cfg(test)] #[test] fn test_model () {
|
||||||
let mut tek = Tek::default();
|
let mut tek = Tek::default();
|
||||||
let _ = tek.clip();
|
let _ = tek.clip();
|
||||||
|
|
|
||||||
|
|
@ -121,10 +121,11 @@ impl Tek {
|
||||||
|
|
||||||
/// Add and focus a track
|
/// Add and focus a track
|
||||||
pub(crate) fn track_add_focus (&mut self) -> Usually<usize> {
|
pub(crate) fn track_add_focus (&mut self) -> Usually<usize> {
|
||||||
|
use Selection::*;
|
||||||
let index = self.track_add(None, None, &[], &[])?.0;
|
let index = self.track_add(None, None, &[], &[])?.0;
|
||||||
self.selected = match self.selected {
|
self.selected = match self.selected {
|
||||||
Selection::Track(t) => Selection::Track(index),
|
Track(_) => Track(index),
|
||||||
Selection::Clip(t, s) => Selection::Clip(index, s),
|
TrackClip { track, scene } => TrackClip { track: index, scene },
|
||||||
_ => self.selected
|
_ => self.selected
|
||||||
};
|
};
|
||||||
Ok(index)
|
Ok(index)
|
||||||
|
|
@ -177,10 +178,11 @@ impl Tek {
|
||||||
|
|
||||||
/// Add and focus an empty scene
|
/// Add and focus an empty scene
|
||||||
pub fn scene_add_focus (&mut self) -> Usually<usize> {
|
pub fn scene_add_focus (&mut self) -> Usually<usize> {
|
||||||
|
use Selection::*;
|
||||||
let index = self.scene_add(None, None)?.0;
|
let index = self.scene_add(None, None)?.0;
|
||||||
self.selected = match self.selected {
|
self.selected = match self.selected {
|
||||||
Selection::Scene(s) => Selection::Scene(index),
|
Scene(_) => Scene(index),
|
||||||
Selection::Clip(t, s) => Selection::Clip(t, index),
|
TrackClip { track, scene } => TrackClip { track, scene: index },
|
||||||
_ => self.selected
|
_ => self.selected
|
||||||
};
|
};
|
||||||
Ok(index)
|
Ok(index)
|
||||||
|
|
@ -215,15 +217,15 @@ impl Tek {
|
||||||
// Create new clip in pool when entering empty cell
|
// Create new clip in pool when entering empty cell
|
||||||
pub fn clip_auto_create (&mut self) {
|
pub fn clip_auto_create (&mut self) {
|
||||||
if let Some(ref pool) = self.pool
|
if let Some(ref pool) = self.pool
|
||||||
&& let Selection::Clip(t, s) = self.selected
|
&& let Selection::TrackClip { track, scene } = self.selected
|
||||||
&& let Some(scene) = self.scenes.get_mut(s)
|
&& let Some(scene) = self.scenes.get_mut(scene)
|
||||||
&& let Some(slot) = scene.clips.get_mut(t)
|
&& let Some(slot) = scene.clips.get_mut(track)
|
||||||
&& slot.is_none()
|
&& slot.is_none()
|
||||||
{
|
{
|
||||||
let (index, mut clip) = pool.add_new_clip();
|
let (index, mut clip) = pool.add_new_clip();
|
||||||
// autocolor: new clip colors from scene and track color
|
// autocolor: new clip colors from scene and track color
|
||||||
clip.write().unwrap().color = ItemColor::random_near(
|
clip.write().unwrap().color = ItemColor::random_near(
|
||||||
self.tracks[t].color.base.mix(
|
self.tracks[track].color.base.mix(
|
||||||
scene.color.base,
|
scene.color.base,
|
||||||
0.5
|
0.5
|
||||||
),
|
),
|
||||||
|
|
@ -239,9 +241,9 @@ impl Tek {
|
||||||
// Remove clip from arrangement when exiting empty clip editor
|
// Remove clip from arrangement when exiting empty clip editor
|
||||||
pub fn clip_auto_remove (&mut self) {
|
pub fn clip_auto_remove (&mut self) {
|
||||||
if let Some(ref mut pool) = self.pool
|
if let Some(ref mut pool) = self.pool
|
||||||
&& let Selection::Clip(t, s) = self.selected
|
&& let Selection::TrackClip { track, scene } = self.selected
|
||||||
&& let Some(scene) = self.scenes.get_mut(s)
|
&& let Some(scene) = self.scenes.get_mut(scene)
|
||||||
&& let Some(slot) = scene.clips.get_mut(t)
|
&& let Some(slot) = scene.clips.get_mut(track)
|
||||||
&& let Some(clip) = slot.as_mut()
|
&& let Some(clip) = slot.as_mut()
|
||||||
{
|
{
|
||||||
let mut swapped = None;
|
let mut swapped = None;
|
||||||
|
|
@ -304,8 +306,10 @@ impl Tek {
|
||||||
// autoedit: load focused clip in editor.
|
// autoedit: load focused clip in editor.
|
||||||
if let Some(ref mut editor) = self.editor {
|
if let Some(ref mut editor) = self.editor {
|
||||||
editor.set_clip(match self.selected {
|
editor.set_clip(match self.selected {
|
||||||
Selection::Clip(t, s) if let Some(Some(Some(clip))) = self
|
Selection::TrackClip { track, scene }
|
||||||
.scenes.get(s).map(|s|s.clips.get(t)) => Some(clip),
|
if let Some(Some(Some(clip))) = self
|
||||||
|
.scenes.get(scene)
|
||||||
|
.map(|s|s.clips.get(track)) => Some(clip),
|
||||||
_ => None
|
_ => None
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -320,14 +324,15 @@ impl Tek {
|
||||||
|
|
||||||
/// Launch a clip or scene
|
/// Launch a clip or scene
|
||||||
pub(crate) fn launch (&mut self) {
|
pub(crate) fn launch (&mut self) {
|
||||||
|
use Selection::*;
|
||||||
match self.selected {
|
match self.selected {
|
||||||
Selection::Track(t) => {
|
Track(t) => {
|
||||||
self.tracks[t].player.enqueue_next(None)
|
self.tracks[t].player.enqueue_next(None)
|
||||||
},
|
},
|
||||||
Selection::Clip(t, s) => {
|
TrackClip { track, scene } => {
|
||||||
self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref())
|
self.tracks[track].player.enqueue_next(self.scenes[scene].clips[track].as_ref())
|
||||||
},
|
},
|
||||||
Selection::Scene(s) => {
|
Scene(s) => {
|
||||||
for t in 0..self.tracks.len() {
|
for t in 0..self.tracks.len() {
|
||||||
self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref())
|
self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref())
|
||||||
}
|
}
|
||||||
|
|
@ -348,25 +353,26 @@ impl Tek {
|
||||||
|
|
||||||
/// Set the color of the selected entity
|
/// Set the color of the selected entity
|
||||||
pub fn set_color (&mut self, palette: Option<ItemTheme>) -> Option<ItemTheme> {
|
pub fn set_color (&mut self, palette: Option<ItemTheme>) -> Option<ItemTheme> {
|
||||||
|
use Selection::*;
|
||||||
let palette = palette.unwrap_or_else(||ItemTheme::random());
|
let palette = palette.unwrap_or_else(||ItemTheme::random());
|
||||||
Some(match self.selected {
|
Some(match self.selected {
|
||||||
Selection::Mix => {
|
Mix => {
|
||||||
let old = self.color;
|
let old = self.color;
|
||||||
self.color = palette;
|
self.color = palette;
|
||||||
old
|
old
|
||||||
},
|
},
|
||||||
Selection::Track(t) => {
|
Scene(s) => {
|
||||||
let old = self.tracks[t].color;
|
|
||||||
self.tracks[t].color = palette;
|
|
||||||
old
|
|
||||||
}
|
|
||||||
Selection::Scene(s) => {
|
|
||||||
let old = self.scenes[s].color;
|
let old = self.scenes[s].color;
|
||||||
self.scenes[s].color = palette;
|
self.scenes[s].color = palette;
|
||||||
old
|
old
|
||||||
}
|
}
|
||||||
Selection::Clip(t, s) => {
|
Track(t) => {
|
||||||
if let Some(ref clip) = self.scenes[s].clips[t] {
|
let old = self.tracks[t].color;
|
||||||
|
self.tracks[t].color = palette;
|
||||||
|
old
|
||||||
|
}
|
||||||
|
TrackClip { track, scene } => {
|
||||||
|
if let Some(ref clip) = self.scenes[scene].clips[track] {
|
||||||
let mut clip = clip.write().unwrap();
|
let mut clip = clip.write().unwrap();
|
||||||
let old = clip.color;
|
let old = clip.color;
|
||||||
clip.color = palette;
|
clip.color = palette;
|
||||||
|
|
@ -374,7 +380,8 @@ impl Tek {
|
||||||
} else {
|
} else {
|
||||||
return None
|
return None
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
_ => todo!()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -426,12 +433,22 @@ pub enum Modal {
|
||||||
pub enum Selection {
|
pub enum Selection {
|
||||||
/// The whole mix is selected
|
/// The whole mix is selected
|
||||||
#[default] Mix,
|
#[default] Mix,
|
||||||
/// A track is selected.
|
/// A MIDI input is selected.
|
||||||
Track(usize),
|
Input(usize),
|
||||||
|
/// A MIDI output is selected.
|
||||||
|
Output(usize),
|
||||||
/// A scene is selected.
|
/// A scene is selected.
|
||||||
Scene(usize),
|
Scene(usize),
|
||||||
|
/// A track is selected.
|
||||||
|
Track(usize),
|
||||||
/// A clip (track × scene) is selected.
|
/// A clip (track × scene) is selected.
|
||||||
Clip(usize, usize),
|
TrackClip { track: usize, scene: usize },
|
||||||
|
/// A track's MIDI input connection is selected.
|
||||||
|
TrackInput { track: usize, port: usize },
|
||||||
|
/// A track's MIDI output connection is selected.
|
||||||
|
TrackOutput { track: usize, port: usize },
|
||||||
|
/// A track device slot is selected.
|
||||||
|
TrackDevice { track: usize, device: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Focus identification methods
|
/// Focus identification methods
|
||||||
|
|
@ -446,70 +463,101 @@ impl Selection {
|
||||||
matches!(self, Self::Scene(_))
|
matches!(self, Self::Scene(_))
|
||||||
}
|
}
|
||||||
pub fn is_clip (&self) -> bool {
|
pub fn is_clip (&self) -> bool {
|
||||||
matches!(self, Self::Clip(_, _))
|
matches!(self, Self::TrackClip {..})
|
||||||
}
|
}
|
||||||
pub fn track (&self) -> Option<usize> {
|
pub fn track (&self) -> Option<usize> {
|
||||||
use Selection::*;
|
use Selection::*;
|
||||||
match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None }
|
match self {
|
||||||
|
Track(track)
|
||||||
|
| TrackClip { track, .. }
|
||||||
|
| TrackInput { track, .. }
|
||||||
|
| TrackOutput { track, .. }
|
||||||
|
| TrackDevice { track, .. } => Some(*track),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn track_next (&self, len: usize) -> Self {
|
pub fn track_next (&self, len: usize) -> Self {
|
||||||
|
use Selection::*;
|
||||||
match self {
|
match self {
|
||||||
Selection::Mix => Selection::Track(0),
|
Mix => Track(0),
|
||||||
Selection::Track(t) if t + 1 < len => Selection::Track(t + 1),
|
Scene(s) => TrackClip { track: 0, scene: *s },
|
||||||
Selection::Track(t) => Selection::Mix,
|
Track(t) => if t + 1 < len {
|
||||||
Selection::Scene(s) => Selection::Clip(0, *s),
|
Track(t + 1)
|
||||||
Selection::Clip(t, s) if t + 1 < len => Selection::Clip(t + 1, *s),
|
} else {
|
||||||
Selection::Clip(t, s) => Selection::Scene(*s),
|
Mix
|
||||||
|
},
|
||||||
|
TrackClip {track, scene} => if track + 1 < len {
|
||||||
|
TrackClip { track: track + 1, scene: *scene }
|
||||||
|
} else {
|
||||||
|
Scene(*scene)
|
||||||
|
},
|
||||||
|
_ => todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn track_prev (&self) -> Self {
|
pub fn track_prev (&self) -> Self {
|
||||||
|
use Selection::*;
|
||||||
match self {
|
match self {
|
||||||
Selection::Mix => Selection::Mix,
|
Mix => Mix,
|
||||||
Selection::Scene(s) => Selection::Scene(*s),
|
Scene(s) => Scene(*s),
|
||||||
Selection::Track(0) => Selection::Mix,
|
Track(0) => Mix,
|
||||||
Selection::Track(t) => Selection::Track(t - 1),
|
Track(t) => Track(t - 1),
|
||||||
Selection::Clip(0, s) => Selection::Scene(*s),
|
TrackClip { track: 0, scene } => Scene(*scene),
|
||||||
Selection::Clip(t, s) => Selection::Clip(t - 1, *s),
|
TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene },
|
||||||
|
_ => todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn scene (&self) -> Option<usize> {
|
pub fn scene (&self) -> Option<usize> {
|
||||||
use Selection::*;
|
use Selection::*;
|
||||||
match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None }
|
match self {
|
||||||
|
Scene(scene) | TrackClip { scene, .. } => Some(*scene),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn scene_next (&self, len: usize) -> Self {
|
pub fn scene_next (&self, len: usize) -> Self {
|
||||||
|
use Selection::*;
|
||||||
match self {
|
match self {
|
||||||
Selection::Mix => Selection::Scene(0),
|
Mix => Scene(0),
|
||||||
Selection::Track(t) => Selection::Clip(*t, 0),
|
Track(t) => TrackClip { track: *t, scene: 0 },
|
||||||
Selection::Scene(s) if s + 1 < len => Selection::Scene(s + 1),
|
Scene(s) => if s + 1 < len {
|
||||||
Selection::Scene(s) => Selection::Mix,
|
Scene(s + 1)
|
||||||
Selection::Clip(t, s) if s + 1 < len => Selection::Clip(*t, s + 1),
|
} else {
|
||||||
Selection::Clip(t, s) => Selection::Track(*t),
|
Mix
|
||||||
|
},
|
||||||
|
TrackClip { track, scene } => if scene + 1 < len {
|
||||||
|
TrackClip { track: *track, scene: scene + 1 }
|
||||||
|
} else {
|
||||||
|
Track(*track)
|
||||||
|
},
|
||||||
|
_ => todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn scene_prev (&self) -> Self {
|
pub fn scene_prev (&self) -> Self {
|
||||||
|
use Selection::*;
|
||||||
match self {
|
match self {
|
||||||
Selection::Mix => Selection::Mix,
|
Mix | Scene(0) => Mix,
|
||||||
Selection::Track(t) => Selection::Track(*t),
|
Scene(s) => Scene(s - 1),
|
||||||
Selection::Scene(0) => Selection::Mix,
|
Track(t) => Track(*t),
|
||||||
Selection::Scene(s) => Selection::Scene(s - 1),
|
TrackClip { track, scene: 0 } => Track(*track),
|
||||||
Selection::Clip(t, 0) => Selection::Track(*t),
|
TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 },
|
||||||
Selection::Clip(t, s) => Selection::Clip(*t, s - 1),
|
_ => todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc<str> {
|
pub fn describe (&self, tracks: &[Track], scenes: &[Scene]) -> Arc<str> {
|
||||||
|
use Selection::*;
|
||||||
format!("{}", match self {
|
format!("{}", match self {
|
||||||
Self::Mix => "Everything".to_string(),
|
Mix => "Everything".to_string(),
|
||||||
Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
||||||
.unwrap_or_else(||"T??".into()),
|
|
||||||
Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
|
||||||
.unwrap_or_else(||"S??".into()),
|
.unwrap_or_else(||"S??".into()),
|
||||||
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
||||||
(Some(_), Some(scene)) => match scene.clip(*t) {
|
.unwrap_or_else(||"T??".into()),
|
||||||
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
|
||||||
None => format!("T{t} S{s}: Empty")
|
(Some(_), Some(s)) => match s.clip(*track) {
|
||||||
|
Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name),
|
||||||
|
None => format!("T{track} S{scene}: Empty")
|
||||||
},
|
},
|
||||||
_ => format!("T{t} S{s}: Empty"),
|
_ => format!("T{track} S{scene}: Empty"),
|
||||||
}
|
},
|
||||||
|
_ => todo!()
|
||||||
}).into()
|
}).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -739,11 +787,3 @@ impl HasTracks for Tek {
|
||||||
fn tracks (&self) -> &Vec<Track> { &self.tracks }
|
fn tracks (&self) -> &Vec<Track> { &self.tracks }
|
||||||
fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
|
fn tracks_mut (&mut self) -> &mut Vec<Track> { &mut self.tracks }
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Device {
|
|
||||||
Sequencer(MidiPlayer),
|
|
||||||
Sampler(Sampler),
|
|
||||||
#[cfg(feature="host")]
|
|
||||||
Plugin(Plugin),
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ pub enum PoolMode {
|
||||||
/// Save clip to disk
|
/// Save clip to disk
|
||||||
Export(usize, FileBrowser),
|
Export(usize, FileBrowser),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,11 +181,12 @@ impl Tek {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
pub(crate) fn tracks_with_sizes (&self) -> impl TracksSizes<'_> {
|
||||||
|
use Selection::*;
|
||||||
let mut x = 0;
|
let mut x = 0;
|
||||||
let editing = self.is_editing();
|
let editing = self.is_editing();
|
||||||
let active = match self.selected() {
|
let active = match self.selected() {
|
||||||
Selection::Track(t) if editing => Some(t),
|
Track(t) if editing => Some(t),
|
||||||
Selection::Clip(t, _) if editing => Some(t),
|
TrackClip { track, .. } if editing => Some(track),
|
||||||
_ => None
|
_ => None
|
||||||
};
|
};
|
||||||
let bigger = self.editor_w();
|
let bigger = self.editor_w();
|
||||||
|
|
@ -200,10 +201,11 @@ impl Tek {
|
||||||
pub(crate) fn scenes_with_sizes (&self, editing: bool, height: usize, larger: usize)
|
pub(crate) fn scenes_with_sizes (&self, editing: bool, height: usize, larger: usize)
|
||||||
-> impl ScenesSizes<'_>
|
-> impl ScenesSizes<'_>
|
||||||
{
|
{
|
||||||
|
use Selection::*;
|
||||||
let (selected_track, selected_scene) = match self.selected() {
|
let (selected_track, selected_scene) = match self.selected() {
|
||||||
Selection::Track(t) => (Some(*t), None),
|
Track(t) => (Some(*t), None),
|
||||||
Selection::Scene(s) => (None, Some(*s)),
|
Scene(s) => (None, Some(*s)),
|
||||||
Selection::Clip(t, s) => (Some(*t), Some(*s)),
|
TrackClip { track, scene } => (Some(*track), Some(*scene)),
|
||||||
_ => (None, None)
|
_ => (None, None)
|
||||||
};
|
};
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
|
|
@ -304,8 +306,10 @@ impl<'a> ArrangerView<'a> {
|
||||||
|
|
||||||
/// Render input matrix.
|
/// Render input matrix.
|
||||||
pub(crate) fn inputs (&'a self) -> impl Content<TuiOut> + 'a {
|
pub(crate) fn inputs (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
Tui::bg(Color::Reset,
|
Tui::bg(Color::Reset, Bsp::s(
|
||||||
Bsp::s(Bsp::s(self.input_routes(), self.input_ports()), self.input_intos()))
|
Bsp::s(self.input_routes(), self.input_ports()),
|
||||||
|
self.input_intos()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_routes (&'a self) -> impl Content<TuiOut> + 'a {
|
fn input_routes (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
|
|
@ -313,13 +317,12 @@ impl<'a> ArrangerView<'a> {
|
||||||
.left(self.width_side,
|
.left(self.width_side,
|
||||||
io_ports(Tui::g(224), Tui::g(32), ||self.app.inputs_with_sizes()))
|
io_ports(Tui::g(224), Tui::g(32), ||self.app.inputs_with_sizes()))
|
||||||
.middle(self.width_mid,
|
.middle(self.width_mid,
|
||||||
per_track_top(
|
per_track_top(self.width_mid, ||self.tracks_with_sizes_scrolled(),
|
||||||
self.width_mid,
|
move|_, &Track { color, .. }|io_conns(
|
||||||
||self.app.tracks_with_sizes(),
|
color.dark.rgb,
|
||||||
move|_, &Track { color, .. }|{
|
color.darker.rgb,
|
||||||
io_conns(color.dark.rgb, color.darker.rgb, ||self.app.inputs_with_sizes())
|
||self.app.inputs_with_sizes()
|
||||||
}
|
)))
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_ports (&'a self) -> impl Content<TuiOut> + 'a {
|
fn input_ports (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
|
|
@ -331,7 +334,7 @@ impl<'a> ArrangerView<'a> {
|
||||||
.middle(self.width_mid,
|
.middle(self.width_mid,
|
||||||
per_track_top(
|
per_track_top(
|
||||||
self.width_mid,
|
self.width_mid,
|
||||||
||self.app.tracks_with_sizes(),
|
||self.tracks_with_sizes_scrolled(),
|
||||||
move|t, track|{
|
move|t, track|{
|
||||||
let rec = track.player.recording;
|
let rec = track.player.recording;
|
||||||
let mon = track.player.monitoring;
|
let mon = track.player.monitoring;
|
||||||
|
|
@ -353,14 +356,12 @@ impl<'a> ArrangerView<'a> {
|
||||||
fn input_intos (&'a self) -> impl Content<TuiOut> + 'a {
|
fn input_intos (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
Tryptich::top(2)
|
Tryptich::top(2)
|
||||||
.left(self.width_side,
|
.left(self.width_side,
|
||||||
Bsp::s(Align::e("Input:"), Align::e("Into:")))
|
Bsp::s(Align::e("Input:"), Align::e("Into clip:")))
|
||||||
.middle(self.width_mid,
|
.middle(self.width_mid,
|
||||||
per_track_top(
|
per_track_top(
|
||||||
self.width_mid,
|
self.width_mid,
|
||||||
||self.app.tracks_with_sizes(),
|
||self.tracks_with_sizes_scrolled(),
|
||||||
|_, _|{
|
|_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))))
|
||||||
Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render output matrix.
|
/// Render output matrix.
|
||||||
|
|
@ -373,30 +374,35 @@ impl<'a> ArrangerView<'a> {
|
||||||
|
|
||||||
fn output_nexts (&'a self) -> impl Content<TuiOut> + 'a {
|
fn output_nexts (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
Tryptich::top(2)
|
Tryptich::top(2)
|
||||||
.left(self.width_side, Align::ne("From:"))
|
.left(self.width_side, Align::ne("From clip:"))
|
||||||
.middle(self.width_mid, per_track_top(
|
.middle(self.width_mid, per_track_top(
|
||||||
self.width_mid,
|
self.width_mid,
|
||||||
||self.tracks_with_sizes_scrolled(),
|
||self.tracks_with_sizes_scrolled(),
|
||||||
|_, _|{
|
|_, _|Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default())))))
|
||||||
Tui::bg(Reset, Align::c(Bsp::s(" ------ ", OctaveVertical::default())))
|
}
|
||||||
|
|
||||||
|
fn output_froms (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
|
let label = Align::ne("Next clip:");
|
||||||
|
Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top(
|
||||||
|
self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{
|
||||||
|
let queued = track.player.next_clip.is_some();
|
||||||
|
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ "));
|
||||||
|
let queued_clip = Thunk::new(||{
|
||||||
|
let title = if let Some((_, clip)) = track.player.next_clip.as_ref() {
|
||||||
|
if let Some(clip) = clip {
|
||||||
|
clip.read().unwrap().name.as_ref().clone()
|
||||||
|
} else {
|
||||||
|
"Stop"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
Tui::bg(Reset, title)
|
||||||
|
});
|
||||||
|
Either(queued, queued_clip, queued_blank)
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
fn output_froms (&'a self) -> impl Content<TuiOut> + 'a {
|
|
||||||
Tryptich::top(2)
|
|
||||||
.left(self.width_side, Align::ne("Next:"))
|
|
||||||
.middle(self.width_mid, per_track_top(
|
|
||||||
self.width_mid,
|
|
||||||
||self.tracks_with_sizes_scrolled(),
|
|
||||||
|t, track|Either(
|
|
||||||
track.player.next_clip.is_some(),
|
|
||||||
Thunk::new(||Tui::bg(Reset, format!("{:?}",
|
|
||||||
track.player.next_clip.as_ref()
|
|
||||||
.map(|(moment, clip)|clip.as_ref()
|
|
||||||
.map(|clip|clip.read().unwrap().name.clone()))
|
|
||||||
.flatten().as_ref()))),
|
|
||||||
Thunk::new(||Tui::bg(Reset, " ------ "))
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
fn output_ports (&'a self) -> impl Content<TuiOut> + 'a {
|
fn output_ports (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
Tryptich::top(1)
|
Tryptich::top(1)
|
||||||
.left(self.width_side,
|
.left(self.width_side,
|
||||||
|
|
@ -412,13 +418,18 @@ impl<'a> ArrangerView<'a> {
|
||||||
let solo = false;
|
let solo = false;
|
||||||
let mute = if mute { White } else { t.color.darkest.rgb };
|
let mute = if mute { White } else { t.color.darkest.rgb };
|
||||||
let solo = if solo { White } else { t.color.darkest.rgb };
|
let solo = if solo { White } else { t.color.darkest.rgb };
|
||||||
let bg_1 = if self.track_selected == Some(i) { t.color.light.rgb } else { t.color.base.rgb };
|
let bg_1 = if self.track_selected == Some(i) {
|
||||||
|
t.color.light.rgb
|
||||||
|
} else {
|
||||||
|
t.color.base.rgb
|
||||||
|
};
|
||||||
let bg_2 = if i > 0 { t.color.base.rgb } else { Reset };
|
let bg_2 = if i > 0 { t.color.base.rgb } else { Reset };
|
||||||
let mute = Tui::fg_bg(mute, bg_1, "Play ");
|
let mute = Tui::fg_bg(mute, bg_1, "Play ");
|
||||||
let solo = Tui::fg_bg(solo, bg_1, "Solo ");
|
let solo = Tui::fg_bg(solo, bg_1, "Solo ");
|
||||||
wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo))))
|
wrap(bg_1, Tui::g(224), Tui::bold(true, Fill::x(Bsp::e(mute, solo))))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_conns (&'a self) -> impl Content<TuiOut> + 'a {
|
fn output_conns (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
Tryptich::top(self.outputs_height)
|
Tryptich::top(self.outputs_height)
|
||||||
.left(self.width_side,
|
.left(self.width_side,
|
||||||
|
|
@ -740,8 +751,9 @@ pub(crate) fn heading <'a> (
|
||||||
pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
|
pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
|
||||||
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
|
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
|
||||||
) -> impl Content<TuiOut> + 'a {
|
) -> impl Content<TuiOut> + 'a {
|
||||||
Map::new(iter,
|
Map::new(iter, move|(
|
||||||
move|(index, name, connections, y, y2): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
|
index, name, connections, y, y2
|
||||||
|
): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
|
||||||
map_south(y as u16, (y2-y) as u16, Bsp::s(
|
map_south(y as u16, (y2-y) as u16, Bsp::s(
|
||||||
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" ", name))))),
|
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(Bsp::e(" ", name))))),
|
||||||
Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1,
|
Map::new(||connections.iter(), move|connect: &'a PortConnect, index|map_south(index as u16, 1,
|
||||||
|
|
@ -752,8 +764,9 @@ pub(crate) fn io_ports <'a, T: PortsSizes<'a>> (
|
||||||
pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
|
pub(crate) fn io_conns <'a, T: PortsSizes<'a>> (
|
||||||
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
|
fg: Color, bg: Color, iter: impl Fn()->T + Send + Sync + 'a
|
||||||
) -> impl Content<TuiOut> + 'a {
|
) -> impl Content<TuiOut> + 'a {
|
||||||
Map::new(iter,
|
Map::new(iter, move|(
|
||||||
move|(index, name, connections, y, y2): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
|
index, name, connections, y, y2
|
||||||
|
): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
|
||||||
map_south(y as u16, (y2-y) as u16, Bsp::s(
|
map_south(y as u16, (y2-y) as u16, Bsp::s(
|
||||||
Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))),
|
Fill::x(Tui::bold(true, wrap(bg, fg, Fill::x(Align::w("▞▞▞▞ ▞▞▞▞"))))),
|
||||||
Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1,
|
Map::new(||connections.iter(), move|connect, index|map_south(index as u16, 1,
|
||||||
|
|
|
||||||
|
|
@ -185,12 +185,12 @@ impl Cli {
|
||||||
_ => vec![]
|
_ => vec![]
|
||||||
},
|
},
|
||||||
scenes,
|
scenes,
|
||||||
selected: Selection::Clip(0, 0),
|
selected: Selection::TrackClip { track: 0, scene: 0 },
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
if let &LaunchMode::Arranger { scenes, tracks, track_width, .. } = mode {
|
if let &LaunchMode::Arranger { scenes, tracks, track_width, .. } = mode {
|
||||||
app.arranger = Default::default();
|
app.arranger = Default::default();
|
||||||
app.selected = Selection::Clip(1, 1);
|
app.selected = Selection::TrackClip { track: 1, scene: 1 };
|
||||||
app.scenes_add(scenes)?;
|
app.scenes_add(scenes)?;
|
||||||
app.tracks_add(tracks, Some(track_width), &[], &[])?;
|
app.tracks_add(tracks, Some(track_width), &[], &[])?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
crates/device/Cargo.toml
Normal file
18
crates/device/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_device"
|
||||||
|
edition = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tengri = { workspace = true }
|
||||||
|
tek_engine = { workspace = true }
|
||||||
|
uuid = { workspace = true, optional = true }
|
||||||
|
livi = { workspace = true, optional = true }
|
||||||
|
symphonia = { workspace = true, optional = true }
|
||||||
|
wavers = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = [ "sequencer", "sampler" ]
|
||||||
|
lv2 = [ "livi" ]
|
||||||
|
sampler = [ "symphonia", "wavers" ]
|
||||||
|
sequencer = [ "uuid" ]
|
||||||
35
crates/device/src/lib.rs
Normal file
35
crates/device/src/lib.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
#![feature(let_chains)]
|
||||||
|
|
||||||
|
pub(crate) use std::cmp::Ord;
|
||||||
|
pub(crate) use std::fmt::{Debug, Formatter};
|
||||||
|
pub(crate) use std::thread::JoinHandle;
|
||||||
|
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
|
||||||
|
pub(crate) use std::fs::File;
|
||||||
|
pub(crate) use std::path::PathBuf;
|
||||||
|
pub(crate) use std::error::Error;
|
||||||
|
pub(crate) use std::ffi::OsString;
|
||||||
|
|
||||||
|
pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}};
|
||||||
|
pub(crate) use ::tek_engine::*;
|
||||||
|
pub(crate) use ::tek_engine::midi::{u7, LiveEvent, MidiMessage};
|
||||||
|
pub(crate) use ::tek_engine::jack::{Control, ProcessScope, MidiWriter, RawMidi};
|
||||||
|
pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}};
|
||||||
|
|
||||||
|
#[cfg(feature = "sequencer")] mod sequencer;
|
||||||
|
#[cfg(feature = "sequencer")] pub use self::sequencer::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "sampler")] mod sampler;
|
||||||
|
#[cfg(feature = "sampler")] pub use self::sampler::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "plugin")] mod plugin;
|
||||||
|
#[cfg(feature = "plugin")] pub use self::plugin::*;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Device {
|
||||||
|
#[cfg(feature = "sequencer")]
|
||||||
|
Sequencer(MidiPlayer),
|
||||||
|
#[cfg(feature = "sampler")]
|
||||||
|
Sampler(Sampler),
|
||||||
|
#[cfg(feature = "plugin")]
|
||||||
|
Plugin(Plugin),
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
mod lv2;
|
||||||
|
mod lv2_gui;
|
||||||
|
mod lv2_tui;
|
||||||
|
mod vst2_tui;
|
||||||
|
mod vst3_tui;
|
||||||
|
|
||||||
/// A plugin device.
|
/// A plugin device.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Plugin {
|
pub struct Plugin {
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
#![feature(let_chains)]
|
use crate::*;
|
||||||
|
|
||||||
pub(crate) use ::tek_jack::{*, jack::*};
|
|
||||||
pub(crate) use ::tek_midi::{*, midly::{*, live::*, num::*}};
|
|
||||||
pub(crate) use ::tengri::{dsl::*, input::*, output::*, tui::{*, ratatui::prelude::*}};
|
|
||||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, Ordering::Relaxed}};
|
|
||||||
pub(crate) use std::fs::File;
|
|
||||||
pub(crate) use std::path::PathBuf;
|
|
||||||
pub(crate) use std::error::Error;
|
|
||||||
pub(crate) use std::ffi::OsString;
|
|
||||||
pub(crate) use symphonia::{
|
pub(crate) use symphonia::{
|
||||||
core::{
|
core::{
|
||||||
formats::Packet,
|
formats::Packet,
|
||||||
|
|
@ -19,7 +11,6 @@ pub(crate) use symphonia::{
|
||||||
},
|
},
|
||||||
default::get_codecs,
|
default::get_codecs,
|
||||||
};
|
};
|
||||||
pub(crate) use ratatui::{prelude::Rect, widgets::{Widget, canvas::{Canvas, Line}}};
|
|
||||||
|
|
||||||
mod sampler_api; pub use self::sampler_api::*;
|
mod sampler_api; pub use self::sampler_api::*;
|
||||||
mod sampler_audio; pub use self::sampler_audio::*;
|
mod sampler_audio; pub use self::sampler_audio::*;
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
mod clip_editor; pub use self::clip_editor::*;
|
use crate::*;
|
||||||
mod clip_launch; pub use self::clip_launch::*;
|
|
||||||
mod clip_model; pub use self::clip_model::*;
|
mod seq_clip; pub use self::seq_clip::*;
|
||||||
mod clip_play; pub use self::clip_play::*;
|
mod seq_launch; pub use self::seq_launch::*;
|
||||||
mod clip_view; pub use self::clip_view::*;
|
mod seq_model; pub use self::seq_model::*;
|
||||||
|
mod seq_view; pub use self::seq_view::*;
|
||||||
|
|
||||||
pub trait HasEditor {
|
pub trait HasEditor {
|
||||||
fn editor (&self) -> &Option<MidiEditor>;
|
fn editor (&self) -> &Option<MidiEditor>;
|
||||||
|
|
@ -87,4 +87,3 @@ pub trait HasPlayClip: HasClock {
|
||||||
FieldV(color, "Next:", format!("{} {}", time, name))
|
FieldV(color, "Next:", format!("{} {}", time, name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
452
crates/device/src/sequencer/seq_model.rs
Normal file
452
crates/device/src/sequencer/seq_model.rs
Normal file
|
|
@ -0,0 +1,452 @@
|
||||||
|
//! MIDI player
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
|
||||||
|
|
||||||
|
impl MidiPlayerApi for MidiPlayer {}
|
||||||
|
|
||||||
|
/// Contains state for playing a clip
|
||||||
|
pub struct MidiPlayer {
|
||||||
|
/// State of clock and playhead
|
||||||
|
pub clock: Clock,
|
||||||
|
/// Start time and clip being played
|
||||||
|
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||||
|
/// Start time and next clip
|
||||||
|
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
||||||
|
/// 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_ins: Vec<JackMidiIn>,
|
||||||
|
/// Play from current sequence to MIDI ports
|
||||||
|
pub midi_outs: Vec<JackMidiOut>,
|
||||||
|
/// Notes currently held at input
|
||||||
|
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// Notes currently held at output
|
||||||
|
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
||||||
|
/// MIDI output buffer
|
||||||
|
pub note_buf: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for MidiPlayer {
|
||||||
|
fn default () -> Self {
|
||||||
|
Self {
|
||||||
|
play_clip: None,
|
||||||
|
next_clip: None,
|
||||||
|
recording: false,
|
||||||
|
monitoring: false,
|
||||||
|
overdub: false,
|
||||||
|
|
||||||
|
notes_in: RwLock::new([false;128]).into(),
|
||||||
|
notes_out: RwLock::new([false;128]).into(),
|
||||||
|
note_buf: vec![0;8],
|
||||||
|
reset: true,
|
||||||
|
|
||||||
|
midi_ins: vec![],
|
||||||
|
midi_outs: vec![],
|
||||||
|
clock: Clock::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MidiPlayer {
|
||||||
|
pub fn new (
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
jack: &Jack,
|
||||||
|
clock: Option<&Clock>,
|
||||||
|
clip: Option<&Arc<RwLock<MidiClip>>>,
|
||||||
|
midi_from: &[PortConnect],
|
||||||
|
midi_to: &[PortConnect],
|
||||||
|
) -> Usually<Self> {
|
||||||
|
let _name = name.as_ref();
|
||||||
|
let clock = clock.cloned().unwrap_or_default();
|
||||||
|
Ok(Self {
|
||||||
|
midi_ins: vec![JackMidiIn::new(jack, format!("M/{}", name.as_ref()), midi_from)?,],
|
||||||
|
midi_outs: vec![JackMidiOut::new(jack, format!("{}/M", name.as_ref()), midi_to)?, ],
|
||||||
|
play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))),
|
||||||
|
clock,
|
||||||
|
note_buf: vec![0;8],
|
||||||
|
reset: true,
|
||||||
|
recording: false,
|
||||||
|
monitoring: false,
|
||||||
|
overdub: false,
|
||||||
|
next_clip: None,
|
||||||
|
notes_in: RwLock::new([false;128]).into(),
|
||||||
|
notes_out: RwLock::new([false;128]).into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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_clip", &self.play_clip)
|
||||||
|
.field("next_clip", &self.next_clip)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
has_clock!(|self: MidiPlayer|self.clock);
|
||||||
|
|
||||||
|
impl HasMidiIns for MidiPlayer {
|
||||||
|
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
|
||||||
|
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn> { &mut self.midi_ins }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasMidiOuts for MidiPlayer {
|
||||||
|
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
|
||||||
|
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut> { &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 clip 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 clip 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 HasPlayClip for MidiPlayer {
|
||||||
|
fn reset (&self) -> bool {
|
||||||
|
self.reset
|
||||||
|
}
|
||||||
|
fn reset_mut (&mut self) -> &mut bool {
|
||||||
|
&mut self.reset
|
||||||
|
}
|
||||||
|
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||||
|
&self.play_clip
|
||||||
|
}
|
||||||
|
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||||
|
&mut self.play_clip
|
||||||
|
}
|
||||||
|
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||||
|
&self.next_clip
|
||||||
|
}
|
||||||
|
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
||||||
|
&mut self.next_clip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiRecordApi: HasClock + HasPlayClip + 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 overdub (&self) -> bool;
|
||||||
|
|
||||||
|
fn overdub_mut (&mut self) -> &mut bool;
|
||||||
|
|
||||||
|
fn toggle_overdub (&mut self) {
|
||||||
|
*self.overdub_mut() = !self.overdub();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.port().iter(scope)) {
|
||||||
|
if let LiveEvent::Midi { message, .. } = event {
|
||||||
|
if monitoring {
|
||||||
|
midi_buf[sample].push(bytes.to_vec());
|
||||||
|
}
|
||||||
|
// FIXME: don't lock on every event!
|
||||||
|
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_clip().clone() {
|
||||||
|
self.record_clip(scope, started, clip, midi_buf);
|
||||||
|
}
|
||||||
|
if let Some((_start_at, _clip)) = &self.next_clip() {
|
||||||
|
self.record_next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_clip (
|
||||||
|
&mut self,
|
||||||
|
scope: &ProcessScope,
|
||||||
|
started: Moment,
|
||||||
|
clip: &Option<Arc<RwLock<MidiClip>>>,
|
||||||
|
_midi_buf: &mut Vec<Vec<Vec<u8>>>
|
||||||
|
) {
|
||||||
|
if let Some(clip) = clip {
|
||||||
|
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 clip = clip.write().unwrap();
|
||||||
|
let length = clip.length;
|
||||||
|
for input in self.midi_ins_mut().iter() {
|
||||||
|
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
|
||||||
|
if let LiveEvent::Midi { message, .. } = event {
|
||||||
|
clip.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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MidiPlaybackApi: HasPlayClip + 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
|
||||||
|
) {
|
||||||
|
let n_frames = (scope.n_frames() as usize).min(out.len());
|
||||||
|
for frame in &mut out[0..n_frames] {
|
||||||
|
frame.clear();
|
||||||
|
}
|
||||||
|
if reset {
|
||||||
|
all_notes_off(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Output notes from clip 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 clip is playing, write a chunk of MIDI events from it to the output buffer.
|
||||||
|
// If no clip is playing, prepare for switchover immediately.
|
||||||
|
self.play_clip().as_ref().map_or(true, |(started, clip)|{
|
||||||
|
self.play_chunk(scope, note_buf, out, started, clip)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle switchover from current to next playing clip.
|
||||||
|
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, clip)) = &self.next_clip() {
|
||||||
|
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 clip:
|
||||||
|
if start <= sample0.saturating_sub(sample) {
|
||||||
|
// Samples elapsed since clip was supposed to start
|
||||||
|
let _skipped = sample0 - start;
|
||||||
|
// Switch over to enqueued clip
|
||||||
|
let started = Moment::from_sample(self.clock().timebase(), start as f64);
|
||||||
|
// Launch enqueued clip
|
||||||
|
*self.play_clip_mut() = Some((started, clip.clone()));
|
||||||
|
// Unset enqueuement (TODO: where to implement looping?)
|
||||||
|
*self.next_clip_mut() = None;
|
||||||
|
// Fill in remaining ticks of chunk from next clip.
|
||||||
|
self.play(scope, note_buf, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_chunk (
|
||||||
|
&self,
|
||||||
|
scope: &ProcessScope,
|
||||||
|
note_buf: &mut Vec<u8>,
|
||||||
|
out: &mut [Vec<Vec<u8>>],
|
||||||
|
started: &Moment,
|
||||||
|
clip: &Option<Arc<RwLock<MidiClip>>>
|
||||||
|
) -> bool {
|
||||||
|
// First sample to populate. Greater than 0 means that the first
|
||||||
|
// pulse of the clip 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 clip 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 = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
|
||||||
|
for (sample, pulse) in pulses {
|
||||||
|
// If a next clip is enqueued, and we're past the end of the current one,
|
||||||
|
// break the loop here (FIXME count pulse correctly)
|
||||||
|
let past_end = if clip.is_some() { pulse >= length } else { true };
|
||||||
|
if self.next_clip().is_some() && past_end {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// If there's a currently playing clip, output notes from it to buffer:
|
||||||
|
if let Some(ref clip) = clip {
|
||||||
|
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_pulse (
|
||||||
|
clip: &RwLock<MidiClip>,
|
||||||
|
pulse: usize,
|
||||||
|
sample: usize,
|
||||||
|
note_buf: &mut Vec<u8>,
|
||||||
|
out: &mut [Vec<Vec<u8>>],
|
||||||
|
notes: &mut [bool;128]
|
||||||
|
) {
|
||||||
|
// Source clip from which the MIDI events will be taken.
|
||||||
|
let clip = clip.read().unwrap();
|
||||||
|
// Clip with zero length is not processed
|
||||||
|
if clip.length > 0 {
|
||||||
|
// Current pulse index in source clip
|
||||||
|
let pulse = pulse % clip.length;
|
||||||
|
// Output each MIDI event from clip at appropriate frames of output buffer:
|
||||||
|
for message in clip.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.port_mut().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:?}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
crates/engine/Cargo.toml
Normal file
11
crates/engine/Cargo.toml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_engine"
|
||||||
|
edition = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tengri = { workspace = true }
|
||||||
|
jack = { workspace = true }
|
||||||
|
midly = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
atomic_float = { workspace = true }
|
||||||
18
crates/engine/src/jack.rs
Normal file
18
crates/engine/src/jack.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
use ::jack::{*, contrib::{*, ClosureProcessHandler}};
|
||||||
|
//contrib::ClosureProcessHandler,
|
||||||
|
//NotificationHandler,
|
||||||
|
//Client, AsyncClient, ClientOptions, ClientStatus,
|
||||||
|
//ProcessScope, Control, Frames,
|
||||||
|
//Port, PortId, PortSpec, PortFlags,
|
||||||
|
//Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
|
||||||
|
//};
|
||||||
|
|
||||||
|
pub(crate) use PortConnectName::*;
|
||||||
|
pub(crate) use PortConnectScope::*;
|
||||||
|
pub(crate) use PortConnectStatus::*;
|
||||||
|
pub(crate) use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
mod jack_client; pub use self::jack_client::*;
|
||||||
|
mod jack_event; pub use self::jack_event::*;
|
||||||
|
mod jack_port; pub use self::jack_port::*;
|
||||||
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use ::jack::contrib::*;
|
use super::*;
|
||||||
use self::JackState::*;
|
use self::JackState::*;
|
||||||
|
|
||||||
/// Things that can provide a [jack::Client] reference.
|
/// Things that can provide a [jack::Client] reference.
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
/// Event enum for JACK events.
|
/// Event enum for JACK events.
|
||||||
#[derive(Debug, Clone, PartialEq)] pub enum JackEvent {
|
#[derive(Debug, Clone, PartialEq)] pub enum JackEvent {
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
macro_rules! impl_port {
|
macro_rules! impl_port {
|
||||||
($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => {
|
($Name:ident : $Spec:ident -> $Pair:ident |$jack:ident, $name:ident|$port:expr) => {
|
||||||
|
|
@ -1,25 +1,28 @@
|
||||||
mod clock; pub use self::clock::*;
|
#![feature(type_alias_impl_trait)]
|
||||||
|
|
||||||
mod time_moment; pub use self::time_moment::*;
|
mod jack; pub use self::jack::*;
|
||||||
mod time_note; pub use self::time_note::*;
|
mod time; pub use self::time::*;
|
||||||
mod time_perf; pub use self::time_perf::*;
|
mod note; pub use self::note::*;
|
||||||
mod time_pulse; pub use self::time_pulse::*;
|
mod midi; pub use self::midi::*;
|
||||||
mod time_sample_count; pub use self::time_sample_count::*;
|
|
||||||
mod time_sample_rate; pub use self::time_sample_rate::*;
|
|
||||||
mod time_timebase; pub use self::time_timebase::*;
|
|
||||||
mod time_unit; pub use self::time_unit::*;
|
|
||||||
mod time_usec; pub use self::time_usec::*;
|
|
||||||
|
|
||||||
pub(crate) use ::tek_jack::{*, jack::{*, contrib::*}};
|
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}};
|
||||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicBool, AtomicUsize, Ordering::*}};
|
pub(crate) use std::path::PathBuf;
|
||||||
|
pub(crate) use std::fmt::Debug;
|
||||||
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
pub(crate) use std::ops::{Add, Sub, Mul, Div, Rem};
|
||||||
pub(crate) use ::tengri::{input::*, dsl::*};
|
|
||||||
|
pub(crate) use ::tengri::input::*;
|
||||||
|
pub(crate) use ::tengri::output::*;
|
||||||
|
pub(crate) use ::tengri::dsl::*;
|
||||||
|
pub(crate) use ::tengri::tui::*;
|
||||||
|
pub(crate) use ::tengri::tui::ratatui::style::{Style, Stylize, Color};
|
||||||
|
|
||||||
pub use ::atomic_float; pub(crate) use atomic_float::*;
|
pub use ::atomic_float; pub(crate) use atomic_float::*;
|
||||||
|
|
||||||
/// Standard result type.
|
/// Standard result type.
|
||||||
pub(crate) type Usually<T> = Result<T, Box<dyn std::error::Error>>;
|
pub(crate) type Usually<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
/// Standard optional result type.
|
/// Standard optional result type.
|
||||||
pub(crate) type Perhaps<T> = Result<Option<T>, Box<dyn std::error::Error>>;
|
pub(crate) type Perhaps<T> = std::result::Result<Option<T>, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
pub trait Gettable<T> {
|
pub trait Gettable<T> {
|
||||||
/// Returns current value
|
/// Returns current value
|
||||||
60
crates/engine/src/midi.rs
Normal file
60
crates/engine/src/midi.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub use ::midly;
|
||||||
|
pub(crate) use ::midly::{
|
||||||
|
MidiMessage,
|
||||||
|
num::*,
|
||||||
|
live::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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; },
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return boxed iterator of MIDI events
|
||||||
|
pub fn parse_midi_input <'a> (input: ::jack::MidiIter<'a>) -> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a> {
|
||||||
|
Box::new(input.map(|::jack::RawMidi { time, bytes }|(
|
||||||
|
time as usize,
|
||||||
|
LiveEvent::parse(bytes).unwrap(),
|
||||||
|
bytes
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for thing that may receive MIDI.
|
||||||
|
pub trait HasMidiIns {
|
||||||
|
fn midi_ins (&self) -> &Vec<JackMidiIn>;
|
||||||
|
|
||||||
|
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn>;
|
||||||
|
|
||||||
|
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<JackMidiOut>;
|
||||||
|
|
||||||
|
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut>;
|
||||||
|
|
||||||
|
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>;
|
||||||
|
}
|
||||||
9
crates/engine/src/time.rs
Normal file
9
crates/engine/src/time.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
mod time_moment; pub use self::time_moment::*;
|
||||||
|
mod time_note; pub use self::time_note::*;
|
||||||
|
mod time_perf; pub use self::time_perf::*;
|
||||||
|
mod time_pulse; pub use self::time_pulse::*;
|
||||||
|
mod time_sample_count; pub use self::time_sample_count::*;
|
||||||
|
mod time_sample_rate; pub use self::time_sample_rate::*;
|
||||||
|
mod time_timebase; pub use self::time_timebase::*;
|
||||||
|
mod time_unit; pub use self::time_unit::*;
|
||||||
|
mod time_usec; pub use self::time_usec::*;
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use tengri::tui::PerfModel;
|
use tengri::tui::PerfModel;
|
||||||
|
use ::jack::ProcessScope;
|
||||||
|
|
||||||
pub trait JackPerfModel {
|
pub trait JackPerfModel {
|
||||||
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
fn update_from_jack_scope (&self, t0: Option<u64>, scope: &ProcessScope);
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tek_jack"
|
|
||||||
edition = { workspace = true }
|
|
||||||
version = { workspace = true }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
jack = { workspace = true }
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
#![feature(type_alias_impl_trait)]
|
|
||||||
mod jack_client; pub use self::jack_client::*;
|
|
||||||
mod jack_event; pub use self::jack_event::*;
|
|
||||||
mod jack_port; pub use self::jack_port::*;
|
|
||||||
pub(crate) use PortConnectName::*;
|
|
||||||
pub(crate) use PortConnectScope::*;
|
|
||||||
pub(crate) use PortConnectStatus::*;
|
|
||||||
pub(crate) use std::sync::{Arc, RwLock};
|
|
||||||
pub use ::jack; pub(crate) use ::jack::{
|
|
||||||
//contrib::ClosureProcessHandler,
|
|
||||||
NotificationHandler,
|
|
||||||
Client, AsyncClient, ClientOptions, ClientStatus,
|
|
||||||
ProcessScope, Control, Frames,
|
|
||||||
Port, PortId, PortSpec, PortFlags,
|
|
||||||
Unowned, MidiIn, MidiOut, AudioIn, AudioOut,
|
|
||||||
};
|
|
||||||
pub(crate) type Usually<T> = Result<T, Box<dyn std::error::Error>>;
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tek_midi"
|
|
||||||
edition = { workspace = true }
|
|
||||||
version = { workspace = true }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tengri = { workspace = true }
|
|
||||||
|
|
||||||
tek_jack = { workspace = true }
|
|
||||||
tek_time = { workspace = true }
|
|
||||||
|
|
||||||
midly = { workspace = true }
|
|
||||||
uuid = { workspace = true }
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
//! MIDI player
|
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
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 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiPlayerApi: MidiRecordApi + MidiPlaybackApi + Send + Sync {}
|
|
||||||
|
|
||||||
impl MidiPlayerApi for MidiPlayer {}
|
|
||||||
|
|
||||||
/// Contains state for playing a clip
|
|
||||||
pub struct MidiPlayer {
|
|
||||||
/// State of clock and playhead
|
|
||||||
pub clock: Clock,
|
|
||||||
/// Start time and clip being played
|
|
||||||
pub play_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
|
||||||
/// Start time and next clip
|
|
||||||
pub next_clip: Option<(Moment, Option<Arc<RwLock<MidiClip>>>)>,
|
|
||||||
/// 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_ins: Vec<JackMidiIn>,
|
|
||||||
/// Play from current sequence to MIDI ports
|
|
||||||
pub midi_outs: Vec<JackMidiOut>,
|
|
||||||
/// Notes currently held at input
|
|
||||||
pub notes_in: Arc<RwLock<[bool; 128]>>,
|
|
||||||
/// Notes currently held at output
|
|
||||||
pub notes_out: Arc<RwLock<[bool; 128]>>,
|
|
||||||
/// MIDI output buffer
|
|
||||||
pub note_buf: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MidiPlayer {
|
|
||||||
fn default () -> Self {
|
|
||||||
Self {
|
|
||||||
play_clip: None,
|
|
||||||
next_clip: None,
|
|
||||||
recording: false,
|
|
||||||
monitoring: false,
|
|
||||||
overdub: false,
|
|
||||||
|
|
||||||
notes_in: RwLock::new([false;128]).into(),
|
|
||||||
notes_out: RwLock::new([false;128]).into(),
|
|
||||||
note_buf: vec![0;8],
|
|
||||||
reset: true,
|
|
||||||
|
|
||||||
midi_ins: vec![],
|
|
||||||
midi_outs: vec![],
|
|
||||||
clock: Clock::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MidiPlayer {
|
|
||||||
pub fn new (
|
|
||||||
name: impl AsRef<str>,
|
|
||||||
jack: &Jack,
|
|
||||||
clock: Option<&Clock>,
|
|
||||||
clip: Option<&Arc<RwLock<MidiClip>>>,
|
|
||||||
midi_from: &[PortConnect],
|
|
||||||
midi_to: &[PortConnect],
|
|
||||||
) -> Usually<Self> {
|
|
||||||
let _name = name.as_ref();
|
|
||||||
let clock = clock.cloned().unwrap_or_default();
|
|
||||||
Ok(Self {
|
|
||||||
midi_ins: vec![JackMidiIn::new(jack, format!("M/{}", name.as_ref()), midi_from)?,],
|
|
||||||
midi_outs: vec![JackMidiOut::new(jack, format!("{}/M", name.as_ref()), midi_to)?, ],
|
|
||||||
play_clip: clip.map(|clip|(Moment::zero(&clock.timebase), Some(clip.clone()))),
|
|
||||||
clock,
|
|
||||||
note_buf: vec![0;8],
|
|
||||||
reset: true,
|
|
||||||
recording: false,
|
|
||||||
monitoring: false,
|
|
||||||
overdub: false,
|
|
||||||
next_clip: None,
|
|
||||||
notes_in: RwLock::new([false;128]).into(),
|
|
||||||
notes_out: RwLock::new([false;128]).into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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_clip", &self.play_clip)
|
|
||||||
.field("next_clip", &self.next_clip)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
has_clock!(|self: MidiPlayer|self.clock);
|
|
||||||
|
|
||||||
impl HasMidiIns for MidiPlayer {
|
|
||||||
fn midi_ins (&self) -> &Vec<JackMidiIn> { &self.midi_ins }
|
|
||||||
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn> { &mut self.midi_ins }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HasMidiOuts for MidiPlayer {
|
|
||||||
fn midi_outs (&self) -> &Vec<JackMidiOut> { &self.midi_outs }
|
|
||||||
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut> { &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 clip 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 clip 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 HasPlayClip for MidiPlayer {
|
|
||||||
fn reset (&self) -> bool {
|
|
||||||
self.reset
|
|
||||||
}
|
|
||||||
fn reset_mut (&mut self) -> &mut bool {
|
|
||||||
&mut self.reset
|
|
||||||
}
|
|
||||||
fn play_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
|
||||||
&self.play_clip
|
|
||||||
}
|
|
||||||
fn play_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
|
||||||
&mut self.play_clip
|
|
||||||
}
|
|
||||||
fn next_clip (&self) -> &Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
|
||||||
&self.next_clip
|
|
||||||
}
|
|
||||||
fn next_clip_mut (&mut self) -> &mut Option<(Moment, Option<Arc<RwLock<MidiClip>>>)> {
|
|
||||||
&mut self.next_clip
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
pub(crate) use std::sync::{Arc, RwLock, atomic::{AtomicUsize, AtomicBool, Ordering::Relaxed}};
|
|
||||||
pub(crate) use std::path::PathBuf;
|
|
||||||
pub(crate) use std::fmt::Debug;
|
|
||||||
|
|
||||||
pub use ::midly;
|
|
||||||
pub(crate) use ::midly::{*, num::*, live::*};
|
|
||||||
|
|
||||||
pub(crate) use ::tek_time::*;
|
|
||||||
pub(crate) use ::tek_jack::{*, jack::*};
|
|
||||||
pub(crate) use ::tengri::input::*;
|
|
||||||
pub(crate) use ::tengri::output::*;
|
|
||||||
pub(crate) use ::tengri::dsl::*;
|
|
||||||
pub(crate) use ::tengri::tui::*;
|
|
||||||
pub(crate) use ::tengri::tui::ratatui::style::{Style, Stylize, Color};
|
|
||||||
|
|
||||||
mod clip; pub use self::clip::*;
|
|
||||||
mod mode; pub use self::mode::*;
|
|
||||||
mod note; pub use self::note::*;
|
|
||||||
mod piano; pub use self::piano::*;
|
|
||||||
mod pool; pub use self::pool::*;
|
|
||||||
mod port; pub use self::port::*;
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
mod piano_h; pub use self::piano_h::*;
|
|
||||||
mod piano_v; pub use self::piano_v::*;
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
use Color::*;
|
|
||||||
pub struct OctaveVertical {
|
|
||||||
on: [bool; 12],
|
|
||||||
colors: [Color; 3]
|
|
||||||
}
|
|
||||||
impl Default for OctaveVertical {
|
|
||||||
fn default () -> Self {
|
|
||||||
Self {
|
|
||||||
on: [false; 12],
|
|
||||||
colors: [Rgb(255,255,255), Rgb(0,0,0), Rgb(255,0,0)]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl OctaveVertical {
|
|
||||||
fn color (&self, pitch: usize) -> Color {
|
|
||||||
let pitch = pitch % 12;
|
|
||||||
self.colors[if self.on[pitch] { 2 } else {
|
|
||||||
match pitch { 0 | 2 | 4 | 5 | 6 | 8 | 10 => 0, _ => 1 }
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Content<TuiOut> for OctaveVertical {
|
|
||||||
fn content (&self) -> impl Render<TuiOut> {
|
|
||||||
row!(
|
|
||||||
Tui::fg_bg(self.color(0), self.color(1), "▙"),
|
|
||||||
Tui::fg_bg(self.color(2), self.color(3), "▙"),
|
|
||||||
Tui::fg_bg(self.color(4), self.color(5), "▌"),
|
|
||||||
Tui::fg_bg(self.color(6), self.color(7), "▟"),
|
|
||||||
Tui::fg_bg(self.color(8), self.color(9), "▟"),
|
|
||||||
Tui::fg_bg(self.color(10), self.color(11), "▟"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
mod port_in; pub use self::port_in::*;
|
|
||||||
mod port_out; pub use self::port_out::*;
|
|
||||||
|
|
||||||
/// 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; },
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return boxed iterator of MIDI events
|
|
||||||
pub fn parse_midi_input <'a> (input: MidiIter<'a>) -> Box<dyn Iterator<Item=(usize, LiveEvent<'a>, &'a [u8])> + 'a> {
|
|
||||||
Box::new(input.map(|RawMidi { time, bytes }|(
|
|
||||||
time as usize,
|
|
||||||
LiveEvent::parse(bytes).unwrap(),
|
|
||||||
bytes
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Trait for thing that may receive MIDI.
|
|
||||||
pub trait HasMidiIns {
|
|
||||||
fn midi_ins (&self) -> &Vec<JackMidiIn>;
|
|
||||||
|
|
||||||
fn midi_ins_mut (&mut self) -> &mut Vec<JackMidiIn>;
|
|
||||||
|
|
||||||
fn has_midi_ins (&self) -> bool {
|
|
||||||
!self.midi_ins().is_empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiRecordApi: HasClock + HasPlayClip + 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 overdub (&self) -> bool;
|
|
||||||
|
|
||||||
fn overdub_mut (&mut self) -> &mut bool;
|
|
||||||
|
|
||||||
fn toggle_overdub (&mut self) {
|
|
||||||
*self.overdub_mut() = !self.overdub();
|
|
||||||
}
|
|
||||||
|
|
||||||
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.port().iter(scope)) {
|
|
||||||
if let LiveEvent::Midi { message, .. } = event {
|
|
||||||
if monitoring {
|
|
||||||
midi_buf[sample].push(bytes.to_vec());
|
|
||||||
}
|
|
||||||
// FIXME: don't lock on every event!
|
|
||||||
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_clip().clone() {
|
|
||||||
self.record_clip(scope, started, clip, midi_buf);
|
|
||||||
}
|
|
||||||
if let Some((_start_at, _clip)) = &self.next_clip() {
|
|
||||||
self.record_next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_clip (
|
|
||||||
&mut self,
|
|
||||||
scope: &ProcessScope,
|
|
||||||
started: Moment,
|
|
||||||
clip: &Option<Arc<RwLock<MidiClip>>>,
|
|
||||||
_midi_buf: &mut Vec<Vec<Vec<u8>>>
|
|
||||||
) {
|
|
||||||
if let Some(clip) = clip {
|
|
||||||
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 clip = clip.write().unwrap();
|
|
||||||
let length = clip.length;
|
|
||||||
for input in self.midi_ins_mut().iter() {
|
|
||||||
for (sample, event, _bytes) in parse_midi_input(input.port().iter(scope)) {
|
|
||||||
if let LiveEvent::Midi { message, .. } = event {
|
|
||||||
clip.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
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
use crate::*;
|
|
||||||
|
|
||||||
/// Trait for thing that may output MIDI.
|
|
||||||
pub trait HasMidiOuts {
|
|
||||||
fn midi_outs (&self) -> &Vec<JackMidiOut>;
|
|
||||||
|
|
||||||
fn midi_outs_mut (&mut self) -> &mut Vec<JackMidiOut>;
|
|
||||||
|
|
||||||
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>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait MidiPlaybackApi: HasPlayClip + 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
|
|
||||||
) {
|
|
||||||
let n_frames = (scope.n_frames() as usize).min(out.len());
|
|
||||||
for frame in &mut out[0..n_frames] {
|
|
||||||
frame.clear();
|
|
||||||
}
|
|
||||||
if reset {
|
|
||||||
all_notes_off(out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Output notes from clip 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 clip is playing, write a chunk of MIDI events from it to the output buffer.
|
|
||||||
// If no clip is playing, prepare for switchover immediately.
|
|
||||||
self.play_clip().as_ref().map_or(true, |(started, clip)|{
|
|
||||||
self.play_chunk(scope, note_buf, out, started, clip)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle switchover from current to next playing clip.
|
|
||||||
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, clip)) = &self.next_clip() {
|
|
||||||
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 clip:
|
|
||||||
if start <= sample0.saturating_sub(sample) {
|
|
||||||
// Samples elapsed since clip was supposed to start
|
|
||||||
let _skipped = sample0 - start;
|
|
||||||
// Switch over to enqueued clip
|
|
||||||
let started = Moment::from_sample(self.clock().timebase(), start as f64);
|
|
||||||
// Launch enqueued clip
|
|
||||||
*self.play_clip_mut() = Some((started, clip.clone()));
|
|
||||||
// Unset enqueuement (TODO: where to implement looping?)
|
|
||||||
*self.next_clip_mut() = None;
|
|
||||||
// Fill in remaining ticks of chunk from next clip.
|
|
||||||
self.play(scope, note_buf, out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play_chunk (
|
|
||||||
&self,
|
|
||||||
scope: &ProcessScope,
|
|
||||||
note_buf: &mut Vec<u8>,
|
|
||||||
out: &mut [Vec<Vec<u8>>],
|
|
||||||
started: &Moment,
|
|
||||||
clip: &Option<Arc<RwLock<MidiClip>>>
|
|
||||||
) -> bool {
|
|
||||||
// First sample to populate. Greater than 0 means that the first
|
|
||||||
// pulse of the clip 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 clip 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 = clip.as_ref().map_or(0, |p|p.read().unwrap().length);
|
|
||||||
for (sample, pulse) in pulses {
|
|
||||||
// If a next clip is enqueued, and we're past the end of the current one,
|
|
||||||
// break the loop here (FIXME count pulse correctly)
|
|
||||||
let past_end = if clip.is_some() { pulse >= length } else { true };
|
|
||||||
if self.next_clip().is_some() && past_end {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
// If there's a currently playing clip, output notes from it to buffer:
|
|
||||||
if let Some(ref clip) = clip {
|
|
||||||
Self::play_pulse(clip, pulse, sample, note_buf, out, notes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn play_pulse (
|
|
||||||
clip: &RwLock<MidiClip>,
|
|
||||||
pulse: usize,
|
|
||||||
sample: usize,
|
|
||||||
note_buf: &mut Vec<u8>,
|
|
||||||
out: &mut [Vec<Vec<u8>>],
|
|
||||||
notes: &mut [bool;128]
|
|
||||||
) {
|
|
||||||
// Source clip from which the MIDI events will be taken.
|
|
||||||
let clip = clip.read().unwrap();
|
|
||||||
// Clip with zero length is not processed
|
|
||||||
if clip.length > 0 {
|
|
||||||
// Current pulse index in source clip
|
|
||||||
let pulse = pulse % clip.length;
|
|
||||||
// Output each MIDI event from clip at appropriate frames of output buffer:
|
|
||||||
for message in clip.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.port_mut().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:?}");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tek_plugin"
|
|
||||||
edition = { workspace = true }
|
|
||||||
version = { workspace = true }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tengri = { workspace = true }
|
|
||||||
|
|
||||||
tek_jack = { workspace = true }
|
|
||||||
tek_time = { workspace = true }
|
|
||||||
tek_midi = { workspace = true }
|
|
||||||
|
|
||||||
livi = { workspace = true, optional = true }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = [ "lv2" ]
|
|
||||||
lv2 = [ "livi" ]
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
mod plugin; pub use self::plugin::*;
|
|
||||||
mod lv2; pub use self::lv2::*;
|
|
||||||
pub(crate) use std::cmp::Ord;
|
|
||||||
pub(crate) use std::fmt::{Debug, Formatter};
|
|
||||||
pub(crate) use std::sync::{Arc, RwLock};
|
|
||||||
pub(crate) use std::thread::JoinHandle;
|
|
||||||
pub(crate) use ::tek_jack::{*, jack::*};
|
|
||||||
pub(crate) use ::tengri::{output::*, tui::{*, ratatui::prelude::*}};
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tek_sampler"
|
|
||||||
edition = { workspace = true }
|
|
||||||
version = { workspace = true }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tengri = { workspace = true }
|
|
||||||
|
|
||||||
tek_jack = { workspace = true }
|
|
||||||
tek_time = { workspace = true }
|
|
||||||
tek_midi = { workspace = true }
|
|
||||||
|
|
||||||
symphonia = { workspace = true }
|
|
||||||
wavers = { workspace = true }
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "tek_time"
|
|
||||||
edition = { workspace = true }
|
|
||||||
version = { workspace = true }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tengri = { workspace = true }
|
|
||||||
tek_jack = { workspace = true }
|
|
||||||
atomic_float = { workspace = true }
|
|
||||||
0
crates/plugin/vst/LICENSE → deps/vst/LICENSE
vendored
0
crates/plugin/vst/LICENSE → deps/vst/LICENSE
vendored
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