finally, handle jack events

This commit is contained in:
🪞👃🪞 2025-01-21 23:08:04 +01:00
parent b2c9bfc0e2
commit 4028b3bb29
8 changed files with 168 additions and 121 deletions

View file

@ -30,8 +30,8 @@ debug := "reset && cargo run --"
release := "reset && cargo run --release --" release := "reset && cargo run --release --"
name := "-n tek" name := "-n tek"
bpm := "-b 174" bpm := "-b 174"
midi-in := "-i '.*nanoKey.*capture.*'" midi-in := "-i 'Midi-Bridge:.*nanoKEY.*:.*capture.*'"
midi-out := "-o '.*Komplete.*playback.*MIDI*'" midi-out := "-o 'Midi-Bridge:.*playback.*'"
# TODO: arranger track mappings # TODO: arranger track mappings
#-i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _" #-i "1=Midi-Bridge:nanoKEY Studio 2:(capture_0) nanoKEY Studio nanoKEY Studio _"

View file

@ -59,18 +59,17 @@ impl Jack {
// This is the misc notifications handler. It's a struct that wraps a [Box] // This is the misc notifications handler. It's a struct that wraps a [Box]
// which performs type erasure on a callback that takes [JackEvent], which is // which performs type erasure on a callback that takes [JackEvent], which is
// one of the available misc notifications. // one of the available misc notifications.
Notifications(Box::new(move|_|{/*TODO*/}) as BoxedJackEventHandler), Notifications(Box::new({
let app = app.clone();
move|event|app.write().unwrap().handle(event)
}) as BoxedJackEventHandler),
// This is the main processing handler. It's a struct that wraps a [Box] // This is the main processing handler. It's a struct that wraps a [Box]
// which performs type erasure on a callback that takes [Client] and [ProcessScope] // which performs type erasure on a callback that takes [Client] and [ProcessScope]
// and passes them down to the `app`'s `process` callback, which in turn // and passes them down to the `app`'s `process` callback, which in turn
// implements audio and MIDI input and output on a realtime basis. // implements audio and MIDI input and output on a realtime basis.
ClosureProcessHandler::new(Box::new({ ClosureProcessHandler::new(Box::new({
let app = app.clone(); let app = app.clone();
move|c: &_, s: &_|if let Ok(mut app) = app.write() { move|c: &_, s: &_|app.write().unwrap().process(c, s)
app.process(c, s)
} else {
Control::Quit
}
}) as BoxedAudioHandler<'j>), }) as BoxedAudioHandler<'j>),
)?; )?;
*self.state.write().unwrap() = Active(client); *self.state.write().unwrap() = Active(client);
@ -122,14 +121,19 @@ pub type DynamicAsyncClient<'j>
= AsyncClient<DynamicNotifications<'j>, DynamicAudioHandler<'j>>; = AsyncClient<DynamicNotifications<'j>, DynamicAudioHandler<'j>>;
/// Implement [Audio]: provide JACK callbacks. /// Implement [Audio]: provide JACK callbacks.
#[macro_export] macro_rules! audio { #[macro_export] macro_rules! audio {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => { (|
$self1:ident:
$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident
|$cb:expr$(;|$self2:ident,$e:ident|$cb2:expr)?) => {
impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? { impl $(<$($L),*$($T $(: $U)?),*>)? Audio for $Struct $(<$($L),*$($T),*>)? {
#[inline] fn process (&mut $self, $c: &Client, $s: &ProcessScope) -> Control { $cb } #[inline] fn process (&mut $self1, $c: &Client, $s: &ProcessScope) -> Control { $cb }
$(#[inline] fn handle (&mut $self2, $e: JackEvent) { $cb2 })?
} }
} }
} }
/// Trait for thing that has a JACK process callback. /// Trait for thing that has a JACK process callback.
pub trait Audio: Send + Sync { pub trait Audio: Send + Sync {
fn handle (&mut self, _event: JackEvent) {}
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control { fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
Control::Continue Control::Continue
} }

View file

@ -1,7 +1,6 @@
use crate::*; use crate::*;
#[derive(Debug, Clone, PartialEq)]
/// Event enum for JACK events. /// Event enum for JACK events.
pub enum JackEvent { #[derive(Debug, Clone, PartialEq)] pub enum JackEvent {
ThreadInit, ThreadInit,
Shutdown(ClientStatus, Arc<str>), Shutdown(ClientStatus, Arc<str>),
Freewheel(bool), Freewheel(bool),
@ -19,42 +18,33 @@ impl<T: Fn(JackEvent) + Send> NotificationHandler for Notifications<T> {
fn thread_init(&self, _: &Client) { fn thread_init(&self, _: &Client) {
self.0(JackEvent::ThreadInit); self.0(JackEvent::ThreadInit);
} }
unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) { unsafe fn shutdown(&mut self, status: ClientStatus, reason: &str) {
self.0(JackEvent::Shutdown(status, reason.into())); self.0(JackEvent::Shutdown(status, reason.into()));
} }
fn freewheel(&mut self, _: &Client, enabled: bool) { fn freewheel(&mut self, _: &Client, enabled: bool) {
self.0(JackEvent::Freewheel(enabled)); self.0(JackEvent::Freewheel(enabled));
} }
fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control { fn sample_rate(&mut self, _: &Client, frames: Frames) -> Control {
self.0(JackEvent::SampleRate(frames)); self.0(JackEvent::SampleRate(frames));
Control::Quit Control::Quit
} }
fn client_registration(&mut self, _: &Client, name: &str, reg: bool) { fn client_registration(&mut self, _: &Client, name: &str, reg: bool) {
self.0(JackEvent::ClientRegistration(name.into(), reg)); self.0(JackEvent::ClientRegistration(name.into(), reg));
} }
fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) { fn port_registration(&mut self, _: &Client, id: PortId, reg: bool) {
self.0(JackEvent::PortRegistration(id, reg)); self.0(JackEvent::PortRegistration(id, reg));
} }
fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control { fn port_rename(&mut self, _: &Client, id: PortId, old: &str, new: &str) -> Control {
self.0(JackEvent::PortRename(id, old.into(), new.into())); self.0(JackEvent::PortRename(id, old.into(), new.into()));
Control::Continue Control::Continue
} }
fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) { fn ports_connected(&mut self, _: &Client, a: PortId, b: PortId, are: bool) {
self.0(JackEvent::PortsConnected(a, b, are)); self.0(JackEvent::PortsConnected(a, b, are));
} }
fn graph_reorder(&mut self, _: &Client) -> Control { fn graph_reorder(&mut self, _: &Client) -> Control {
self.0(JackEvent::GraphReorder); self.0(JackEvent::GraphReorder);
Control::Continue Control::Continue
} }
fn xrun(&mut self, _: &Client) -> Control { fn xrun(&mut self, _: &Client) -> Control {
self.0(JackEvent::XRun); self.0(JackEvent::XRun);
Control::Continue Control::Continue

View file

@ -97,13 +97,15 @@ pub trait JackPortAutoconnect: JackPort + for<'a>JackPortConnect<&'a Port<Unowne
for connect in self.conn().iter() { for connect in self.conn().iter() {
let status = match &connect.name { let status = match &connect.name {
Exact(name) => self.connect_exact(name), Exact(name) => self.connect_exact(name),
RegExp(re) => self.connect_regexp(re), RegExp(re) => self.connect_regexp(re, connect.scope),
}?; }?;
*connect.status.write().unwrap() = status; *connect.status.write().unwrap() = status;
} }
Ok(()) Ok(())
} }
fn connect_exact (&self, name: &str) -> Usually<Vec<(Port<Unowned>, Arc<str>, PortConnectStatus)>> { fn connect_exact (
&self, name: &str
) -> Usually<Vec<(Port<Unowned>, Arc<str>, PortConnectStatus)>> {
self.with_client(|c|{ self.with_client(|c|{
let mut status = vec![]; let mut status = vec![];
for port in c.ports(None, None, PortFlags::empty()).iter() { for port in c.ports(None, None, PortFlags::empty()).iter() {
@ -121,18 +123,20 @@ pub trait JackPortAutoconnect: JackPort + for<'a>JackPortConnect<&'a Port<Unowne
Ok(status) Ok(status)
}) })
} }
fn connect_regexp (&self, re: &str) -> Usually<Vec<(Port<Unowned>, Arc<str>, PortConnectStatus)>> { fn connect_regexp (
&self, re: &str, scope: PortConnectScope
) -> Usually<Vec<(Port<Unowned>, Arc<str>, PortConnectStatus)>> {
self.with_client(|c|{ self.with_client(|c|{
let mut status = vec![]; let mut status = vec![];
for port in c.ports(Some(&re), None, PortFlags::empty()).iter() { let ports = c.ports(Some(&re), None, PortFlags::empty());
for port in ports.iter() {
if let Some(port) = c.port_by_name(port.as_str()) { if let Some(port) = c.port_by_name(port.as_str()) {
let port_status = self.connect_to(&port)?; let port_status = self.connect_to(&port)?;
let name = port.name()?.into(); let name = port.name()?.into();
status.push((port, name, port_status)); status.push((port, name, port_status));
// TODO if port_status == Connected && scope == One {
//if port_status == Connected && connect.scope == One { break
//break }
//}
} }
} }
Ok(status) Ok(status)
@ -185,12 +189,22 @@ impl PortConnect {
Self { name, scope: All, status: Arc::new(RwLock::new(vec![])) } Self { name, scope: All, status: Arc::new(RwLock::new(vec![])) }
} }
pub fn info (&self) -> Arc<str> { pub fn info (&self) -> Arc<str> {
format!("{} {} {}", match self.scope { let status = {
One => " ", let status = self.status.read().unwrap();
All => "*", let mut ok = 0;
}, match &self.name { for (_, _, state) in status.iter() {
Exact(name) => format!("= {name}"), if *state == Connected {
RegExp(name) => format!("~ {name}"), ok += 1
}, self.status.read().unwrap().len()).into() }
}
format!("{ok}/{}", status.len())
};
let scope = match self.scope {
One => " ", All => "*",
};
let name = match &self.name {
Exact(name) => format!("= {name}"), RegExp(name) => format!("~ {name}"),
};
format!("({}) {} {}", status, scope, name).into()
} }
} }

View file

@ -1,71 +1,99 @@
use crate::*; use crate::*;
impl HasJack for Tek { fn jack (&self) -> &Jack { &self.jack } } impl HasJack for Tek { fn jack (&self) -> &Jack { &self.jack } }
audio!(|self: Tek, client, scope|{ audio!(
// Start profiling cycle
let t0 = self.perf.get_t0(); |self: Tek, client, scope|{
// Update transport clock // Start profiling cycle
self.clock().update_from_scope(scope).unwrap(); let t0 = self.perf.get_t0();
// Collect MIDI input (TODO preallocate) // Update transport clock
let midi_in = self.midi_ins.iter() self.clock().update_from_scope(scope).unwrap();
.map(|port|port.port().iter(scope) // Collect MIDI input (TODO preallocate)
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes))) let midi_in = self.midi_ins.iter()
.collect::<Vec<_>>()) .map(|port|port.port().iter(scope)
.collect::<Vec<_>>(); .map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
// Update standalone MIDI sequencer .collect::<Vec<_>>())
if let Some(player) = self.player.as_mut() { .collect::<Vec<_>>();
if Control::Quit == PlayerAudio( // Update standalone MIDI sequencer
player, if let Some(player) = self.player.as_mut() {
&mut self.note_buf, if Control::Quit == PlayerAudio(
&mut self.midi_buf, player,
).process(client, scope) { &mut self.note_buf,
return Control::Quit &mut self.midi_buf,
} ).process(client, scope) {
} return Control::Quit
// Update standalone sampler
if let Some(sampler) = self.sampler.as_mut() {
if Control::Quit == SamplerAudio(sampler).process(client, scope) {
return Control::Quit
}
//for port in midi_in.iter() {
//for message in port.iter() {
//match message {
//Ok(M
//}
//}
//}
}
// TODO move these to editor and sampler?:
for port in midi_in.iter() {
for event in port.iter() {
match event {
(time, Ok(LiveEvent::Midi {message, ..})) => match message {
MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => {
editor.set_note_pos(key.as_int() as usize);
},
MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = (
self.editor.as_ref(),
self.sampler.as_ref(),
) => {
// TODO: give sampler its own cursor
if let Some(sample) = &sampler.mapped[editor.note_pos()] {
sample.write().unwrap().handle_cc(*controller, *value)
}
}
_ =>{}
},
_ =>{}
} }
} }
} // Update standalone sampler
// Update track sequencers if let Some(sampler) = self.sampler.as_mut() {
for track in self.tracks.iter_mut() { if Control::Quit == SamplerAudio(sampler).process(client, scope) {
if PlayerAudio( return Control::Quit
track.player_mut(), &mut self.note_buf, &mut self.midi_buf }
).process(client, scope) == Control::Quit { //for port in midi_in.iter() {
return Control::Quit //for message in port.iter() {
//match message {
//Ok(M
//}
//}
//}
}
// TODO move these to editor and sampler?:
for port in midi_in.iter() {
for event in port.iter() {
match event {
(time, Ok(LiveEvent::Midi {message, ..})) => match message {
MidiMessage::NoteOn {ref key, ..} if let Some(editor) = self.editor.as_ref() => {
editor.set_note_pos(key.as_int() as usize);
},
MidiMessage::Controller {controller, value} if let (Some(editor), Some(sampler)) = (
self.editor.as_ref(),
self.sampler.as_ref(),
) => {
// TODO: give sampler its own cursor
if let Some(sample) = &sampler.mapped[editor.note_pos()] {
sample.write().unwrap().handle_cc(*controller, *value)
}
}
_ =>{}
},
_ =>{}
}
}
}
// Update track sequencers
for track in self.tracks.iter_mut() {
if PlayerAudio(
track.player_mut(), &mut self.note_buf, &mut self.midi_buf
).process(client, scope) == Control::Quit {
return Control::Quit
}
}
// End profiling cycle
self.perf.update(t0, scope);
Control::Continue
};
|self, event|{
use JackEvent::*;
match event {
SampleRate(sr) =>
{ self.clock.timebase.sr.set(sr as f64); },
PortRegistration(id, true) =>
{},
PortRegistration(id, false) =>
{},
PortsConnected(a, b, true) =>
{},
PortsConnected(a, b, false) =>
{},
ClientRegistration(id, true) =>
{},
ClientRegistration(id, false) =>
{},
ThreadInit =>
{},
XRun =>
{},
_ => { panic!("{event:?}"); }
} }
} }
// End profiling cycle );
self.perf.update(t0, scope);
Control::Continue
});

View file

@ -64,8 +64,8 @@ impl TekCli {
let jack = Jack::new(name)?; let jack = Jack::new(name)?;
let engine = Tui::new()?; let engine = Tui::new()?;
let empty = &[] as &[&str]; let empty = &[] as &[&str];
let midi_froms = PortConnect::collect(&self.midi_from, &self.midi_from_re, empty); let midi_froms = PortConnect::collect(&self.midi_from, empty, &self.midi_from_re);
let midi_tos = PortConnect::collect(&self.midi_to, &self.midi_to_re, empty); let midi_tos = PortConnect::collect(&self.midi_to, empty, &self.midi_to_re);
let left_froms = PortConnect::collect(&self.left_from, empty, empty); let left_froms = PortConnect::collect(&self.left_from, empty, empty);
let left_tos = PortConnect::collect(&self.left_to, empty, empty); let left_tos = PortConnect::collect(&self.left_to, empty, empty);
let right_froms = PortConnect::collect(&self.right_from, empty, empty); let right_froms = PortConnect::collect(&self.right_from, empty, empty);

View file

@ -118,12 +118,14 @@ impl Tek {
name, name,
..Default::default() ..Default::default()
}; };
track.player.midi_ins.push(JackMidiIn::new(
&self.jack, &format!("{}I", &track.name), midi_from let midi_in = JackMidiIn::new(&self.jack, &format!("{}I", &track.name), midi_from)?;
)?); midi_in.connect_to_matching()?;
track.player.midi_outs.push(JackMidiOut::new( track.player.midi_ins.push(midi_in);
&self.jack, &format!("{}O", &track.name), midi_to
)?); let midi_out = JackMidiOut::new(&self.jack, &format!("{}O", &track.name), midi_to)?;
midi_out.connect_to_matching()?;
track.player.midi_outs.push(midi_out);
self.tracks_mut().push(track); self.tracks_mut().push(track);
let len = self.tracks().len(); let len = self.tracks().len();
let index = len - 1; let index = len - 1;

View file

@ -198,19 +198,23 @@ impl Tek {
)) ))
} }
fn view_inputs (&self) -> impl Content<TuiOut> + use<'_> { fn view_inputs (&self) -> impl Content<TuiOut> + use<'_> {
let w = self.w();
let fg = Tui::g(224); let fg = Tui::g(224);
let bg = Tui::g(64); let bg = Tui::g(64);
let h = 1 + self.midi_ins.len() as u16; let mut h = 1 + self.midi_ins.len();
for midi_in in self.midi_ins.iter() { h += midi_in.conn().len() }
let conn = move|conn: &PortConnect|{
Fill::x(Align::w(Tui::bold(false, Tui::fg_bg(fg, bg, conn.info()))))
};
let header: ThunkBox<_> = io_header!( let header: ThunkBox<_> = io_header!(
self, self,
" I ", " I ",
" midi ins", " midi ins",
self.midi_ins.len(), self.midi_ins.len(),
self.midi_ins().get(0).map( self.midi_ins().get(0).map(move|input: &JackMidiIn|Bsp::s(
move|input: &JackMidiIn|Bsp::s( Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(input.name())))),
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(input.name().clone())))), input.conn().get(0).map(conn)
input.conn().get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, )));
Tui::fg_bg(fg, bg, connect.info()))))))));
let rec = false; let rec = false;
let mon = false; let mon = false;
let cells: ThunkBox<_> = per_track!(self.size.w();|self, track, _t|Bsp::s(Tui::bold(true, row!( let cells: ThunkBox<_> = per_track!(self.size.w();|self, track, _t|Bsp::s(Tui::bold(true, row!(
@ -222,15 +226,20 @@ impl Tek {
Tui::fg_bg(if rec { White } else { track.color.dark.rgb }, track.color.darker.rgb, ""), Tui::fg_bg(if rec { White } else { track.color.dark.rgb }, track.color.darker.rgb, ""),
Tui::fg_bg(if mon { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"), Tui::fg_bg(if mon { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"),
))); )));
self.view_row(self.w(), h, header, cells) self.view_row(w, h as u16, header, cells)
} }
fn view_outputs (&self) -> impl Content<TuiOut> + use<'_> { fn view_outputs (&self) -> impl Content<TuiOut> + use<'_> {
let fg = Tui::g(224); let fg = Tui::g(224);
let bg = Tui::g(64); let bg = Tui::g(64);
let h = 1 + self.midi_outs.len() as u16; let mut h = 1 + self.midi_outs.len();
let header: ThunkBox<_> = io_header!(self, " O ", " midi outs", self.midi_outs.len(), self.midi_outs().get(0).map( for midi_out in self.midi_outs.iter() { h += midi_out.conn().len() }
move|output: &JackMidiOut|Bsp::s( let header: ThunkBox<_> = io_header!(
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(output.name().clone())))), self,
" O ",
" midi outs",
self.midi_outs.len(),
self.midi_outs().get(0).map(move|output: &JackMidiOut|Bsp::s(
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(output.name())))),
output.conn().get(0).map(|connect|Fill::x(Align::w(Tui::bold(false, output.conn().get(0).map(|connect|Fill::x(Align::w(Tui::bold(false,
Tui::fg_bg(fg, bg, connect.info())))))))); Tui::fg_bg(fg, bg, connect.info()))))))));
let mute = false; let mute = false;
@ -244,7 +253,7 @@ impl Tek {
Tui::fg_bg(if mute { White } else { track.color.darker.rgb }, track.color.darker.rgb, ""), Tui::fg_bg(if mute { White } else { track.color.darker.rgb }, track.color.darker.rgb, ""),
Tui::fg_bg(if solo { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"), Tui::fg_bg(if solo { White } else { track.color.light.rgb }, track.color.darker.rgb, "CH**"),
))); )));
self.view_row(self.w(), h, header, cells) self.view_row(self.w(), h as u16, header, cells)
} }
fn view_tracks (&self) -> impl Content<TuiOut> + use<'_> { fn view_tracks (&self) -> impl Content<TuiOut> + use<'_> {
let h = 1; let h = 1;