Compare commits

..

No commits in common. "4cf82af950954d56f13a5c1762b4c02534e423ab" and "7b432d12b4f054f777c5c184a8c8da4255dd9e0f" have entirely different histories.

115 changed files with 967 additions and 981 deletions

50
Cargo.lock generated
View file

@ -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]]

View file

@ -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 = { path = "./crates/app" }
tek_engine = { path = "./crates/engine" } tek_cli = { path = "./crates/cli" }
tek = { path = "./crates/app" } tek_jack = { path = "./crates/jack" }
tek_cli = { path = "./crates/cli" } 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" }

View file

@ -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"]

View file

@ -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())))

View file

@ -1,3 +0,0 @@
mod editor_api;
mod editor_model;
mod editor_view;

View file

@ -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)
}
}

View file

@ -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();

View file

@ -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),
}

View file

@ -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 { fn output_froms (&'a self) -> impl Content<TuiOut> + 'a {
let label = Align::ne("Next clip:"); Tryptich::top(2)
Tryptich::top(2).left(self.width_side, label).middle(self.width_mid, per_track_top( .left(self.width_side, Align::ne("Next:"))
self.width_mid, ||self.tracks_with_sizes_scrolled(), |t, track|{ .middle(self.width_mid, per_track_top(
let queued = track.player.next_clip.is_some(); self.width_mid,
let queued_blank = Thunk::new(||Tui::bg(Reset, " ------ ")); ||self.tracks_with_sizes_scrolled(),
let queued_clip = Thunk::new(||{ |t, track|Either(
let title = if let Some((_, clip)) = track.player.next_clip.as_ref() { track.player.next_clip.is_some(),
if let Some(clip) = clip { Thunk::new(||Tui::bg(Reset, format!("{:?}",
clip.read().unwrap().name.as_ref().clone() track.player.next_clip.as_ref()
} else { .map(|(moment, clip)|clip.as_ref()
"Stop" .map(|clip|clip.read().unwrap().name.clone()))
} .flatten().as_ref()))),
} else { Thunk::new(||Tui::bg(Reset, " ------ "))
"" )))
};
Tui::bg(Reset, title)
});
Either(queued, queued_clip, queued_blank)
}))
} }
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,26 +740,24 @@ 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, Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg,
Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, &connect.info)))))))))
&connect.info)))))))))
} }
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, Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x(""))))))))))
Fill::x(Align::w(Tui::bold(false, wrap(bg, fg, Fill::x(""))))))))))
} }
pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> ( pub(crate) fn per_track_top <'a, T: Content<TuiOut> + 'a, U: TracksSizes<'a>> (

View file

@ -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), &[], &[])?;
} }

View file

@ -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" ]

View file

@ -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),
}

View file

@ -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:?}");
});
}
}
}
}

View file

@ -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 }

View file

@ -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::*;

View file

@ -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>;
}

View file

@ -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
View file

@ -0,0 +1,7 @@
[package]
name = "tek_jack"
edition = { workspace = true }
version = { workspace = true }
[dependencies]
jack = { workspace = true }

View file

@ -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.

View file

@ -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 {

View file

@ -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
View 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
View 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 }

View file

@ -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>;

View file

@ -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)
}
}

View file

@ -87,3 +87,4 @@ pub trait HasPlayClip: HasClock {
FieldV(color, "Next:", format!("{} {}", time, name)) FieldV(color, "Next:", format!("{} {}", time, name))
} }
} }

View 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
View 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::*;

View file

@ -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
View file

@ -0,0 +1,2 @@
mod piano_h; pub use self::piano_h::*;
mod piano_v; pub use self::piano_v::*;

View file

@ -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), ""),
)
}
}

View 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
View 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);
}

View 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
}
}

View 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
View 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
View 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::*}};

View file

@ -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
View 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 }

View file

@ -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