mirror of
https://codeberg.org/unspeaker/tek.git
synced 2026-01-31 16:36:40 +01:00
Compare commits
No commits in common. "4cf82af950954d56f13a5c1762b4c02534e423ab" and "7b432d12b4f054f777c5c184a8c8da4255dd9e0f" have entirely different histories.
4cf82af950
...
7b432d12b4
115 changed files with 967 additions and 981 deletions
50
Cargo.lock
generated
50
Cargo.lock
generated
|
|
@ -1518,8 +1518,11 @@ dependencies = [
|
||||||
"proptest",
|
"proptest",
|
||||||
"proptest-derive",
|
"proptest-derive",
|
||||||
"rand",
|
"rand",
|
||||||
"tek_device",
|
"tek_jack",
|
||||||
"tek_engine",
|
"tek_midi",
|
||||||
|
"tek_plugin",
|
||||||
|
"tek_sampler",
|
||||||
|
"tek_time",
|
||||||
"tengri",
|
"tengri",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
@ -1533,26 +1536,53 @@ dependencies = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tek_device"
|
name = "tek_jack"
|
||||||
|
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",
|
||||||
"symphonia",
|
"tek_jack",
|
||||||
"tek_engine",
|
"tek_midi",
|
||||||
|
"tek_time",
|
||||||
|
"tengri",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tek_sampler"
|
||||||
|
version = "0.2.1"
|
||||||
|
dependencies = [
|
||||||
|
"symphonia",
|
||||||
|
"tek_jack",
|
||||||
|
"tek_midi",
|
||||||
|
"tek_time",
|
||||||
"tengri",
|
"tengri",
|
||||||
"uuid",
|
|
||||||
"wavers",
|
"wavers",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tek_engine"
|
name = "tek_time"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic_float",
|
"atomic_float",
|
||||||
"jack",
|
"tek_jack",
|
||||||
"midly",
|
|
||||||
"tengri",
|
"tengri",
|
||||||
"uuid",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
14
Cargo.toml
14
Cargo.toml
|
|
@ -5,10 +5,13 @@ 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"
|
||||||
|
|
@ -34,10 +37,13 @@ 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,8 +6,11 @@ version = { workspace = true }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tengri = { workspace = true }
|
tengri = { workspace = true }
|
||||||
|
|
||||||
tek_engine = { workspace = true }
|
tek_jack = { workspace = true }
|
||||||
tek_device = { workspace = true }
|
tek_time = { 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 }
|
||||||
|
|
@ -22,4 +25,4 @@ proptest-derive = { workspace = true }
|
||||||
[features]
|
[features]
|
||||||
default = ["cli"]
|
default = ["cli"]
|
||||||
cli = ["clap"]
|
cli = ["clap"]
|
||||||
host = ["tek_device/lv2"]
|
host = ["tek_plugin"]
|
||||||
|
|
|
||||||
|
|
@ -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::TrackClip { track, scene } => self.scenes[scene].clips[track].clone(),
|
Selection::Clip(t, s) => self.scenes[s].clips[t].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::TrackClip { track: t, scene: s }) })))
|
(t, s) => Self::Select(Selection::Clip(t, s)) })))
|
||||||
|
|
||||||
(ClipCommand:
|
(ClipCommand:
|
||||||
("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
("edit" [a: MaybeClip] Some(Self::Edit(a.unwrap())))
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
mod editor_api;
|
|
||||||
mod editor_model;
|
|
||||||
mod editor_view;
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -41,9 +41,6 @@ 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,11 +121,10 @@ 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 {
|
||||||
Track(_) => Track(index),
|
Selection::Track(t) => Selection::Track(index),
|
||||||
TrackClip { track, scene } => TrackClip { track: index, scene },
|
Selection::Clip(t, s) => Selection::Clip(index, s),
|
||||||
_ => self.selected
|
_ => self.selected
|
||||||
};
|
};
|
||||||
Ok(index)
|
Ok(index)
|
||||||
|
|
@ -178,11 +177,10 @@ 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 {
|
||||||
Scene(_) => Scene(index),
|
Selection::Scene(s) => Selection::Scene(index),
|
||||||
TrackClip { track, scene } => TrackClip { track, scene: index },
|
Selection::Clip(t, s) => Selection::Clip(t, index),
|
||||||
_ => self.selected
|
_ => self.selected
|
||||||
};
|
};
|
||||||
Ok(index)
|
Ok(index)
|
||||||
|
|
@ -217,15 +215,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::TrackClip { track, scene } = self.selected
|
&& let Selection::Clip(t, s) = self.selected
|
||||||
&& let Some(scene) = self.scenes.get_mut(scene)
|
&& let Some(scene) = self.scenes.get_mut(s)
|
||||||
&& let Some(slot) = scene.clips.get_mut(track)
|
&& let Some(slot) = scene.clips.get_mut(t)
|
||||||
&& 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[track].color.base.mix(
|
self.tracks[t].color.base.mix(
|
||||||
scene.color.base,
|
scene.color.base,
|
||||||
0.5
|
0.5
|
||||||
),
|
),
|
||||||
|
|
@ -241,9 +239,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::TrackClip { track, scene } = self.selected
|
&& let Selection::Clip(t, s) = self.selected
|
||||||
&& let Some(scene) = self.scenes.get_mut(scene)
|
&& let Some(scene) = self.scenes.get_mut(s)
|
||||||
&& let Some(slot) = scene.clips.get_mut(track)
|
&& let Some(slot) = scene.clips.get_mut(t)
|
||||||
&& let Some(clip) = slot.as_mut()
|
&& let Some(clip) = slot.as_mut()
|
||||||
{
|
{
|
||||||
let mut swapped = None;
|
let mut swapped = None;
|
||||||
|
|
@ -306,10 +304,8 @@ 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::TrackClip { track, scene }
|
Selection::Clip(t, s) if let Some(Some(Some(clip))) = self
|
||||||
if let Some(Some(Some(clip))) = self
|
.scenes.get(s).map(|s|s.clips.get(t)) => Some(clip),
|
||||||
.scenes.get(scene)
|
|
||||||
.map(|s|s.clips.get(track)) => Some(clip),
|
|
||||||
_ => None
|
_ => None
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -324,15 +320,14 @@ 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 {
|
||||||
Track(t) => {
|
Selection::Track(t) => {
|
||||||
self.tracks[t].player.enqueue_next(None)
|
self.tracks[t].player.enqueue_next(None)
|
||||||
},
|
},
|
||||||
TrackClip { track, scene } => {
|
Selection::Clip(t, s) => {
|
||||||
self.tracks[track].player.enqueue_next(self.scenes[scene].clips[track].as_ref())
|
self.tracks[t].player.enqueue_next(self.scenes[s].clips[t].as_ref())
|
||||||
},
|
},
|
||||||
Scene(s) => {
|
Selection::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())
|
||||||
}
|
}
|
||||||
|
|
@ -353,26 +348,25 @@ 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 {
|
||||||
Mix => {
|
Selection::Mix => {
|
||||||
let old = self.color;
|
let old = self.color;
|
||||||
self.color = palette;
|
self.color = palette;
|
||||||
old
|
old
|
||||||
},
|
},
|
||||||
Scene(s) => {
|
Selection::Track(t) => {
|
||||||
let old = self.scenes[s].color;
|
|
||||||
self.scenes[s].color = palette;
|
|
||||||
old
|
|
||||||
}
|
|
||||||
Track(t) => {
|
|
||||||
let old = self.tracks[t].color;
|
let old = self.tracks[t].color;
|
||||||
self.tracks[t].color = palette;
|
self.tracks[t].color = palette;
|
||||||
old
|
old
|
||||||
}
|
}
|
||||||
TrackClip { track, scene } => {
|
Selection::Scene(s) => {
|
||||||
if let Some(ref clip) = self.scenes[scene].clips[track] {
|
let old = self.scenes[s].color;
|
||||||
|
self.scenes[s].color = palette;
|
||||||
|
old
|
||||||
|
}
|
||||||
|
Selection::Clip(t, s) => {
|
||||||
|
if let Some(ref clip) = self.scenes[s].clips[t] {
|
||||||
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;
|
||||||
|
|
@ -380,8 +374,7 @@ impl Tek {
|
||||||
} else {
|
} else {
|
||||||
return None
|
return None
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
_ => todo!()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -433,22 +426,12 @@ pub enum Modal {
|
||||||
pub enum Selection {
|
pub enum Selection {
|
||||||
/// The whole mix is selected
|
/// The whole mix is selected
|
||||||
#[default] Mix,
|
#[default] Mix,
|
||||||
/// A MIDI input is selected.
|
|
||||||
Input(usize),
|
|
||||||
/// A MIDI output is selected.
|
|
||||||
Output(usize),
|
|
||||||
/// A scene is selected.
|
|
||||||
Scene(usize),
|
|
||||||
/// A track is selected.
|
/// A track is selected.
|
||||||
Track(usize),
|
Track(usize),
|
||||||
|
/// A scene is selected.
|
||||||
|
Scene(usize),
|
||||||
/// A clip (track × scene) is selected.
|
/// A clip (track × scene) is selected.
|
||||||
TrackClip { track: usize, scene: usize },
|
Clip(usize, 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
|
||||||
|
|
@ -463,101 +446,70 @@ 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::TrackClip {..})
|
matches!(self, Self::Clip(_, _))
|
||||||
}
|
}
|
||||||
pub fn track (&self) -> Option<usize> {
|
pub fn track (&self) -> Option<usize> {
|
||||||
use Selection::*;
|
use Selection::*;
|
||||||
match self {
|
match self { Clip(t, _) => Some(*t), Track(t) => Some(*t), _ => None }
|
||||||
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 {
|
||||||
Mix => Track(0),
|
Selection::Mix => Selection::Track(0),
|
||||||
Scene(s) => TrackClip { track: 0, scene: *s },
|
Selection::Track(t) if t + 1 < len => Selection::Track(t + 1),
|
||||||
Track(t) => if t + 1 < len {
|
Selection::Track(t) => Selection::Mix,
|
||||||
Track(t + 1)
|
Selection::Scene(s) => Selection::Clip(0, *s),
|
||||||
} else {
|
Selection::Clip(t, s) if t + 1 < len => Selection::Clip(t + 1, *s),
|
||||||
Mix
|
Selection::Clip(t, s) => Selection::Scene(*s),
|
||||||
},
|
|
||||||
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 {
|
||||||
Mix => Mix,
|
Selection::Mix => Selection::Mix,
|
||||||
Scene(s) => Scene(*s),
|
Selection::Scene(s) => Selection::Scene(*s),
|
||||||
Track(0) => Mix,
|
Selection::Track(0) => Selection::Mix,
|
||||||
Track(t) => Track(t - 1),
|
Selection::Track(t) => Selection::Track(t - 1),
|
||||||
TrackClip { track: 0, scene } => Scene(*scene),
|
Selection::Clip(0, s) => Selection::Scene(*s),
|
||||||
TrackClip { track: t, scene } => TrackClip { track: t - 1, scene: *scene },
|
Selection::Clip(t, s) => Selection::Clip(t - 1, *s),
|
||||||
_ => todo!()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn scene (&self) -> Option<usize> {
|
pub fn scene (&self) -> Option<usize> {
|
||||||
use Selection::*;
|
use Selection::*;
|
||||||
match self {
|
match self { Clip(_, s) => Some(*s), Scene(s) => Some(*s), _ => None }
|
||||||
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 {
|
||||||
Mix => Scene(0),
|
Selection::Mix => Selection::Scene(0),
|
||||||
Track(t) => TrackClip { track: *t, scene: 0 },
|
Selection::Track(t) => Selection::Clip(*t, 0),
|
||||||
Scene(s) => if s + 1 < len {
|
Selection::Scene(s) if s + 1 < len => Selection::Scene(s + 1),
|
||||||
Scene(s + 1)
|
Selection::Scene(s) => Selection::Mix,
|
||||||
} else {
|
Selection::Clip(t, s) if s + 1 < len => Selection::Clip(*t, s + 1),
|
||||||
Mix
|
Selection::Clip(t, s) => Selection::Track(*t),
|
||||||
},
|
|
||||||
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 {
|
||||||
Mix | Scene(0) => Mix,
|
Selection::Mix => Selection::Mix,
|
||||||
Scene(s) => Scene(s - 1),
|
Selection::Track(t) => Selection::Track(*t),
|
||||||
Track(t) => Track(*t),
|
Selection::Scene(0) => Selection::Mix,
|
||||||
TrackClip { track, scene: 0 } => Track(*track),
|
Selection::Scene(s) => Selection::Scene(s - 1),
|
||||||
TrackClip { track, scene } => TrackClip { track: *track, scene: scene - 1 },
|
Selection::Clip(t, 0) => Selection::Track(*t),
|
||||||
_ => todo!()
|
Selection::Clip(t, s) => Selection::Clip(*t, s - 1),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
||||||
Mix => "Everything".to_string(),
|
Self::Mix => "Everything".to_string(),
|
||||||
Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
Self::Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
||||||
.unwrap_or_else(||"S??".into()),
|
|
||||||
Track(t) => tracks.get(*t).map(|track|format!("T{t}: {}", &track.name))
|
|
||||||
.unwrap_or_else(||"T??".into()),
|
.unwrap_or_else(||"T??".into()),
|
||||||
TrackClip { track, scene } => match (tracks.get(*track), scenes.get(*scene)) {
|
Self::Scene(s) => scenes.get(*s).map(|scene|format!("S{s}: {}", &scene.name))
|
||||||
(Some(_), Some(s)) => match s.clip(*track) {
|
.unwrap_or_else(||"S??".into()),
|
||||||
Some(clip) => format!("T{track} S{scene} C{}", &clip.read().unwrap().name),
|
Self::Clip(t, s) => match (tracks.get(*t), scenes.get(*s)) {
|
||||||
None => format!("T{track} S{scene}: Empty")
|
(Some(_), Some(scene)) => match scene.clip(*t) {
|
||||||
|
Some(clip) => format!("T{t} S{s} C{}", &clip.read().unwrap().name),
|
||||||
|
None => format!("T{t} S{s}: Empty")
|
||||||
},
|
},
|
||||||
_ => format!("T{track} S{scene}: Empty"),
|
_ => format!("T{t} S{s}: Empty"),
|
||||||
},
|
}
|
||||||
_ => todo!()
|
|
||||||
}).into()
|
}).into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -787,3 +739,11 @@ 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),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,12 +181,11 @@ 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() {
|
||||||
Track(t) if editing => Some(t),
|
Selection::Track(t) if editing => Some(t),
|
||||||
TrackClip { track, .. } if editing => Some(track),
|
Selection::Clip(t, _) if editing => Some(t),
|
||||||
_ => None
|
_ => None
|
||||||
};
|
};
|
||||||
let bigger = self.editor_w();
|
let bigger = self.editor_w();
|
||||||
|
|
@ -201,11 +200,10 @@ 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() {
|
||||||
Track(t) => (Some(*t), None),
|
Selection::Track(t) => (Some(*t), None),
|
||||||
Scene(s) => (None, Some(*s)),
|
Selection::Scene(s) => (None, Some(*s)),
|
||||||
TrackClip { track, scene } => (Some(*track), Some(*scene)),
|
Selection::Clip(t, s) => (Some(*t), Some(*s)),
|
||||||
_ => (None, None)
|
_ => (None, None)
|
||||||
};
|
};
|
||||||
let mut y = 0;
|
let mut y = 0;
|
||||||
|
|
@ -306,10 +304,8 @@ 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, Bsp::s(
|
Tui::bg(Color::Reset,
|
||||||
Bsp::s(self.input_routes(), self.input_ports()),
|
Bsp::s(Bsp::s(self.input_routes(), self.input_ports()), self.input_intos()))
|
||||||
self.input_intos()
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_routes (&'a self) -> impl Content<TuiOut> + 'a {
|
fn input_routes (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
|
|
@ -317,12 +313,13 @@ 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(self.width_mid, ||self.tracks_with_sizes_scrolled(),
|
per_track_top(
|
||||||
move|_, &Track { color, .. }|io_conns(
|
self.width_mid,
|
||||||
color.dark.rgb,
|
||self.app.tracks_with_sizes(),
|
||||||
color.darker.rgb,
|
move|_, &Track { color, .. }|{
|
||||||
||self.app.inputs_with_sizes()
|
io_conns(color.dark.rgb, color.darker.rgb, ||self.app.inputs_with_sizes())
|
||||||
)))
|
}
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input_ports (&'a self) -> impl Content<TuiOut> + 'a {
|
fn input_ports (&'a self) -> impl Content<TuiOut> + 'a {
|
||||||
|
|
@ -334,7 +331,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.tracks_with_sizes_scrolled(),
|
||self.app.tracks_with_sizes(),
|
||||||
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;
|
||||||
|
|
@ -356,12 +353,14 @@ 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 clip:")))
|
Bsp::s(Align::e("Input:"), Align::e("Into:")))
|
||||||
.middle(self.width_mid,
|
.middle(self.width_mid,
|
||||||
per_track_top(
|
per_track_top(
|
||||||
self.width_mid,
|
self.width_mid,
|
||||||
||self.tracks_with_sizes_scrolled(),
|
||self.app.tracks_with_sizes(),
|
||||||
|_, _|Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))))
|
|_, _|{
|
||||||
|
Tui::bg(Reset, Align::c(Bsp::s(OctaveVertical::default(), " ------ ")))
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render output matrix.
|
/// Render output matrix.
|
||||||
|
|
@ -374,35 +373,30 @@ 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 clip:"))
|
.left(self.width_side, Align::ne("From:"))
|
||||||
.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,
|
||||||
|
|
@ -418,18 +412,13 @@ 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) {
|
let bg_1 = if self.track_selected == Some(i) { t.color.light.rgb } else { t.color.base.rgb };
|
||||||
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,
|
||||||
|
|
@ -751,9 +740,8 @@ 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, move|(
|
Map::new(iter,
|
||||||
index, name, connections, y, y2
|
move|(index, name, connections, y, y2): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
|
||||||
): (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,
|
||||||
|
|
@ -764,9 +752,8 @@ 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, move|(
|
Map::new(iter,
|
||||||
index, name, connections, y, y2
|
move|(index, name, connections, y, y2): (usize, &'a Arc<str>, &'a [PortConnect], usize, usize), _|
|
||||||
): (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::TrackClip { track: 0, scene: 0 },
|
selected: Selection::Clip(0, 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::TrackClip { track: 1, scene: 1 };
|
app.selected = Selection::Clip(1, 1);
|
||||||
app.scenes_add(scenes)?;
|
app.scenes_add(scenes)?;
|
||||||
app.tracks_add(tracks, Some(track_width), &[], &[])?;
|
app.tracks_add(tracks, Some(track_width), &[], &[])?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
[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" ]
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
#![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,452 +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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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:?}");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
[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 }
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
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,60 +0,0 @@
|
||||||
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>;
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
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::*;
|
|
||||||
7
crates/jack/Cargo.toml
Normal file
7
crates/jack/Cargo.toml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
[package]
|
||||||
|
name = "tek_jack"
|
||||||
|
edition = { workspace = true }
|
||||||
|
version = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
jack = { workspace = true }
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
use super::*;
|
use ::jack::contrib::*;
|
||||||
use self::JackState::*;
|
use self::JackState::*;
|
||||||
|
|
||||||
/// Things that can provide a [jack::Client] reference.
|
/// Things that can provide a [jack::Client] reference.
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
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,5 +1,4 @@
|
||||||
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) => {
|
||||||
17
crates/jack/src/lib.rs
Normal file
17
crates/jack/src/lib.rs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#![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>>;
|
||||||
13
crates/midi/Cargo.toml
Normal file
13
crates/midi/Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[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,9 +1,8 @@
|
||||||
use crate::*;
|
mod clip_editor; pub use self::clip_editor::*;
|
||||||
|
mod clip_launch; pub use self::clip_launch::*;
|
||||||
mod seq_clip; pub use self::seq_clip::*;
|
mod clip_model; pub use self::clip_model::*;
|
||||||
mod seq_launch; pub use self::seq_launch::*;
|
mod clip_play; pub use self::clip_play::*;
|
||||||
mod seq_model; pub use self::seq_model::*;
|
mod clip_view; pub use self::clip_view::*;
|
||||||
mod seq_view; pub use self::seq_view::*;
|
|
||||||
|
|
||||||
pub trait HasEditor {
|
pub trait HasEditor {
|
||||||
fn editor (&self) -> &Option<MidiEditor>;
|
fn editor (&self) -> &Option<MidiEditor>;
|
||||||
|
|
@ -51,6 +51,39 @@ 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
|
||||||
|
|
@ -148,3 +181,64 @@ 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -87,3 +87,4 @@ pub trait HasPlayClip: HasClock {
|
||||||
FieldV(color, "Next:", format!("{} {}", time, name))
|
FieldV(color, "Next:", format!("{} {}", time, name))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
208
crates/midi/src/clip/clip_play.rs
Normal file
208
crates/midi/src/clip/clip_play.rs
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
//! 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
|
||||||
|
}
|
||||||
|
}
|
||||||
21
crates/midi/src/lib.rs
Normal file
21
crates/midi/src/lib.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
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::*;
|
||||||
|
|
@ -16,4 +16,3 @@ pub enum PoolMode {
|
||||||
/// Save clip to disk
|
/// Save clip to disk
|
||||||
Export(usize, FileBrowser),
|
Export(usize, FileBrowser),
|
||||||
}
|
}
|
||||||
|
|
||||||
2
crates/midi/src/piano.rs
Normal file
2
crates/midi/src/piano.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod piano_h; pub use self::piano_h::*;
|
||||||
|
mod piano_v; pub use self::piano_v::*;
|
||||||
|
|
@ -325,39 +325,3 @@ 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), "▟"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
34
crates/midi/src/piano/piano_v.rs
Normal file
34
crates/midi/src/piano/piano_v.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
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), "▟"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
crates/midi/src/port.rs
Normal file
31
crates/midi/src/port.rs
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
107
crates/midi/src/port/port_in.rs
Normal file
107
crates/midi/src/port/port_in.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
164
crates/midi/src/port/port_out.rs
Normal file
164
crates/midi/src/port/port_out.rs
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
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:?}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
crates/plugin/Cargo.toml
Normal file
17
crates/plugin/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[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" ]
|
||||||
8
crates/plugin/src/lib.rs
Normal file
8
crates/plugin/src/lib.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
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,11 +1,5 @@
|
||||||
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 {
|
||||||
14
crates/sampler/Cargo.toml
Normal file
14
crates/sampler/Cargo.toml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[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,5 +1,13 @@
|
||||||
use crate::*;
|
#![feature(let_chains)]
|
||||||
|
|
||||||
|
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,
|
||||||
|
|
@ -11,6 +19,7 @@ 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::*;
|
||||||
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