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 --"
name := "-n tek"
bpm := "-b 174"
midi-in := "-i '.*nanoKey.*capture.*'"
midi-out := "-o '.*Komplete.*playback.*MIDI*'"
midi-in := "-i 'Midi-Bridge:.*nanoKEY.*:.*capture.*'"
midi-out := "-o 'Midi-Bridge:.*playback.*'"
# TODO: arranger track mappings
#-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]
// which performs type erasure on a callback that takes [JackEvent], which is
// 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]
// 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
// implements audio and MIDI input and output on a realtime basis.
ClosureProcessHandler::new(Box::new({
let app = app.clone();
move|c: &_, s: &_|if let Ok(mut app) = app.write() {
app.process(c, s)
} else {
Control::Quit
}
move|c: &_, s: &_|app.write().unwrap().process(c, s)
}) as BoxedAudioHandler<'j>),
)?;
*self.state.write().unwrap() = Active(client);
@ -122,14 +121,19 @@ pub type DynamicAsyncClient<'j>
= AsyncClient<DynamicNotifications<'j>, DynamicAudioHandler<'j>>;
/// Implement [Audio]: provide JACK callbacks.
#[macro_export] macro_rules! audio {
(|$self:ident:$Struct:ident$(<$($L:lifetime),*$($T:ident$(:$U:path)?),*>)?,$c:ident,$s:ident|$cb:expr) => {
(|
$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),*>)? {
#[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.
pub trait Audio: Send + Sync {
fn handle (&mut self, _event: JackEvent) {}
fn process (&mut self, _: &Client, _: &ProcessScope) -> Control {
Control::Continue
}

View file

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

View file

@ -97,13 +97,15 @@ pub trait JackPortAutoconnect: JackPort + for<'a>JackPortConnect<&'a Port<Unowne
for connect in self.conn().iter() {
let status = match &connect.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;
}
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|{
let mut status = vec![];
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)
})
}
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|{
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()) {
let port_status = self.connect_to(&port)?;
let name = port.name()?.into();
status.push((port, name, port_status));
// TODO
//if port_status == Connected && connect.scope == One {
//break
//}
if port_status == Connected && scope == One {
break
}
}
}
Ok(status)
@ -185,12 +189,22 @@ impl PortConnect {
Self { name, scope: All, status: Arc::new(RwLock::new(vec![])) }
}
pub fn info (&self) -> Arc<str> {
format!("{} {} {}", match self.scope {
One => " ",
All => "*",
}, match &self.name {
Exact(name) => format!("= {name}"),
RegExp(name) => format!("~ {name}"),
}, self.status.read().unwrap().len()).into()
let status = {
let status = self.status.read().unwrap();
let mut ok = 0;
for (_, _, state) in status.iter() {
if *state == Connected {
ok += 1
}
}
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::*;
impl HasJack for Tek { fn jack (&self) -> &Jack { &self.jack } }
audio!(|self: Tek, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
self.clock().update_from_scope(scope).unwrap();
// Collect MIDI input (TODO preallocate)
let midi_in = self.midi_ins.iter()
.map(|port|port.port().iter(scope)
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
.collect::<Vec<_>>())
.collect::<Vec<_>>();
// Update standalone MIDI sequencer
if let Some(player) = self.player.as_mut() {
if Control::Quit == PlayerAudio(
player,
&mut self.note_buf,
&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)
}
}
_ =>{}
},
_ =>{}
audio!(
|self: Tek, client, scope|{
// Start profiling cycle
let t0 = self.perf.get_t0();
// Update transport clock
self.clock().update_from_scope(scope).unwrap();
// Collect MIDI input (TODO preallocate)
let midi_in = self.midi_ins.iter()
.map(|port|port.port().iter(scope)
.map(|RawMidi { time, bytes }|(time, LiveEvent::parse(bytes)))
.collect::<Vec<_>>())
.collect::<Vec<_>>();
// Update standalone MIDI sequencer
if let Some(player) = self.player.as_mut() {
if Control::Quit == PlayerAudio(
player,
&mut self.note_buf,
&mut self.midi_buf,
).process(client, scope) {
return Control::Quit
}
}
}
// 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
// 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 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 engine = Tui::new()?;
let empty = &[] as &[&str];
let midi_froms = PortConnect::collect(&self.midi_from, &self.midi_from_re, empty);
let midi_tos = PortConnect::collect(&self.midi_to, &self.midi_to_re, empty);
let midi_froms = PortConnect::collect(&self.midi_from, empty, &self.midi_from_re);
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_tos = PortConnect::collect(&self.left_to, empty, empty);
let right_froms = PortConnect::collect(&self.right_from, empty, empty);

View file

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

View file

@ -198,19 +198,23 @@ impl Tek {
))
}
fn view_inputs (&self) -> impl Content<TuiOut> + use<'_> {
let w = self.w();
let fg = Tui::g(224);
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!(
self,
" I ",
" midi ins",
self.midi_ins.len(),
self.midi_ins().get(0).map(
move|input: &JackMidiIn|Bsp::s(
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(input.name().clone())))),
input.conn().get(0).map(|connect|Fill::x(Align::w(Tui::bold(false,
Tui::fg_bg(fg, bg, connect.info()))))))));
self.midi_ins().get(0).map(move|input: &JackMidiIn|Bsp::s(
Fill::x(Tui::bold(true, Tui::fg_bg(fg, bg, Align::w(input.name())))),
input.conn().get(0).map(conn)
)));
let rec = false;
let mon = false;
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 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<'_> {
let fg = Tui::g(224);
let bg = Tui::g(64);
let h = 1 + self.midi_outs.len() as u16;
let header: ThunkBox<_> = io_header!(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().clone())))),
let mut h = 1 + self.midi_outs.len();
for midi_out in self.midi_outs.iter() { h += midi_out.conn().len() }
let header: ThunkBox<_> = io_header!(
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,
Tui::fg_bg(fg, bg, connect.info()))))))));
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 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<'_> {
let h = 1;