mirror of
https://github.com/robbert-vdh/nih-plug.git
synced 2026-07-01 02:36:54 +00:00
256 lines
10 KiB
Rust
256 lines
10 KiB
Rust
use std::sync::Arc;
|
|
|
|
use anyhow::{Context, Result};
|
|
use atomic_refcell::AtomicRefCell;
|
|
use crossbeam::channel;
|
|
use jack::{
|
|
AudioIn, AudioOut, Client, ClientOptions, ClosureProcessHandler, Control, MidiIn, MidiOut, Port,
|
|
};
|
|
|
|
use super::super::config::WrapperConfig;
|
|
use super::Backend;
|
|
use crate::buffer::Buffer;
|
|
use crate::context::Transport;
|
|
use crate::midi::{MidiConfig, NoteEvent};
|
|
use crate::plugin::Plugin;
|
|
|
|
/// Uses JACK audio and MIDI.
|
|
pub struct Jack {
|
|
config: WrapperConfig,
|
|
/// The JACK client, wrapped in an option since it needs to be transformed into an `AsyncClient`
|
|
/// and then back into a regular `Client`.
|
|
client: Option<Client>,
|
|
|
|
inputs: Arc<Vec<Port<AudioIn>>>,
|
|
outputs: Arc<AtomicRefCell<Vec<Port<AudioOut>>>>,
|
|
midi_input: Option<Arc<Port<MidiIn>>>,
|
|
midi_output: Option<Arc<AtomicRefCell<Port<MidiOut>>>>,
|
|
}
|
|
|
|
/// A simple message to tell the audio thread to shut down, since the actual processing happens in
|
|
/// these callbacks.
|
|
enum Task {
|
|
Shutdown,
|
|
}
|
|
|
|
impl Backend for Jack {
|
|
fn run(
|
|
&mut self,
|
|
mut cb: impl FnMut(&mut Buffer, Transport, &[NoteEvent], &mut Vec<NoteEvent>) -> bool
|
|
+ 'static
|
|
+ Send,
|
|
) {
|
|
let client = self.client.take().unwrap();
|
|
let buffer_size = client.buffer_size();
|
|
|
|
let mut buffer = Buffer::default();
|
|
unsafe {
|
|
buffer.with_raw_vec(|output_slices| {
|
|
output_slices.resize_with(self.outputs.borrow().len(), || &mut []);
|
|
})
|
|
}
|
|
|
|
let mut input_events = Vec::with_capacity(2048);
|
|
let mut output_events = Vec::with_capacity(2048);
|
|
|
|
let (control_sender, control_receiver) = channel::bounded(32);
|
|
let config = self.config.clone();
|
|
let inputs = self.inputs.clone();
|
|
let outputs = self.outputs.clone();
|
|
let midi_input = self.midi_input.clone();
|
|
let midi_output = self.midi_output.clone();
|
|
let process_handler = ClosureProcessHandler::new(move |client, ps| {
|
|
// In theory we could handle `num_frames <= buffer_size`, but JACK will never chop up
|
|
// buffers like that so we'll just make it easier for ourselves by not supporting that
|
|
let num_frames = ps.n_frames();
|
|
if num_frames != buffer_size {
|
|
nih_error!(
|
|
"Buffer size changed from {buffer_size} to {num_frames}. Buffer size changes \
|
|
are currently not supported, aborting..."
|
|
);
|
|
control_sender.send(Task::Shutdown).unwrap();
|
|
return Control::Quit;
|
|
}
|
|
|
|
let mut transport = Transport::new(client.sample_rate() as f32);
|
|
transport.tempo = Some(config.tempo as f64);
|
|
transport.time_sig_numerator = Some(config.timesig_num as i32);
|
|
transport.time_sig_denominator = Some(config.timesig_denom as i32);
|
|
|
|
if let Ok(jack_transport) = client.transport().query() {
|
|
transport.pos_samples = Some(jack_transport.pos.frame() as i64);
|
|
transport.playing = jack_transport.state == jack::TransportState::Rolling;
|
|
|
|
if let Some(bbt) = jack_transport.pos.bbt() {
|
|
transport.tempo = Some(bbt.bpm);
|
|
transport.time_sig_numerator = Some(bbt.sig_num as i32);
|
|
transport.time_sig_denominator = Some(bbt.sig_denom as i32);
|
|
|
|
transport.pos_beats = Some(
|
|
(bbt.bar as f64 * 4.0)
|
|
+ (bbt.beat as f64 / bbt.sig_denom as f64 * 4.0)
|
|
+ (bbt.tick as f64 / bbt.ticks_per_beat),
|
|
);
|
|
transport.bar_number = Some(bbt.bar as i32);
|
|
}
|
|
}
|
|
|
|
// Just like all of the plugin backends, we need to grab the output slices and copy the
|
|
// inputs to the outputs
|
|
let mut outputs = outputs.borrow_mut();
|
|
for (input, output) in inputs.iter().zip(outputs.iter_mut()) {
|
|
// XXX: Since the JACK bindings let us do this, presumably these two can't alias,
|
|
// right?
|
|
output.as_mut_slice(ps).copy_from_slice(input.as_slice(ps));
|
|
}
|
|
|
|
// And the buffer's slices need to point to the JACK output ports
|
|
unsafe {
|
|
buffer.with_raw_vec(|output_slices| {
|
|
for (output_slice, output) in output_slices.iter_mut().zip(outputs.iter_mut()) {
|
|
// SAFETY: This buffer is only read from after in this callback, and the
|
|
// reference passed to `cb` cannot outlive that function call
|
|
*output_slice = &mut *(output.as_mut_slice(ps) as *mut _);
|
|
}
|
|
})
|
|
}
|
|
|
|
input_events.clear();
|
|
if let Some(midi_input) = &midi_input {
|
|
input_events.extend(midi_input.iter(ps).filter_map(|midi| {
|
|
// This should always be three bytes, but we get an unsized slice
|
|
let midi_data: [u8; 3] = midi.bytes.try_into().ok()?;
|
|
NoteEvent::from_midi(midi.time, midi_data).ok()
|
|
}));
|
|
}
|
|
|
|
output_events.clear();
|
|
if cb(&mut buffer, transport, &input_events, &mut output_events) {
|
|
if let Some(midi_output) = &midi_output {
|
|
let mut midi_output = midi_output.borrow_mut();
|
|
let mut midi_writer = midi_output.writer(ps);
|
|
for event in output_events.drain(..) {
|
|
let timing = event.timing();
|
|
if let Some(midi_data) = event.as_midi() {
|
|
let write_result = midi_writer.write(&jack::RawMidi {
|
|
time: timing,
|
|
bytes: &midi_data,
|
|
});
|
|
nih_debug_assert!(write_result.is_ok(), "The MIDI buffer is full");
|
|
}
|
|
}
|
|
}
|
|
|
|
Control::Continue
|
|
} else {
|
|
control_sender.send(Task::Shutdown).unwrap();
|
|
Control::Quit
|
|
}
|
|
});
|
|
// TODO: What can go wrong here that would cause an error?
|
|
let async_client = client.activate_async((), process_handler).unwrap();
|
|
|
|
// The process callback happens on another thread, so we need to block this thread until we
|
|
// get the request to shut down or until the process callback runs into an error
|
|
#[allow(clippy::never_loop)]
|
|
loop {
|
|
match control_receiver.recv() {
|
|
Ok(Task::Shutdown) => break,
|
|
Err(err) => {
|
|
nih_debug_assert_failure!("Error reading from channel: {}", err);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// And put the client back where it belongs in case this function is called a second time
|
|
let (client, _, _) = async_client.deactivate().unwrap();
|
|
self.client = Some(client);
|
|
}
|
|
}
|
|
|
|
impl Jack {
|
|
/// Initialize the JACK backend. Returns an error if this failed for whatever reason. The plugin
|
|
/// generic argument is to get the name for the client, and to know whether or not the
|
|
/// standalone should expose JACK MIDI ports.
|
|
pub fn new<P: Plugin>(config: WrapperConfig) -> Result<Self> {
|
|
let (client, status) = Client::new(P::NAME, ClientOptions::NO_START_SERVER)
|
|
.context("Error while initializing the JACK client")?;
|
|
if !status.is_empty() {
|
|
anyhow::bail!("The JACK server returned an error: {status:?}");
|
|
}
|
|
|
|
let mut inputs = Vec::new();
|
|
for port_no in 1..config.input_channels + 1 {
|
|
inputs.push(client.register_port(&format!("input_{port_no}"), AudioIn)?);
|
|
}
|
|
|
|
let mut outputs = Vec::new();
|
|
for port_no in 1..config.output_channels + 1 {
|
|
let port = client.register_port(&format!("output_{port_no}"), AudioOut)?;
|
|
|
|
// We don't connect the inputs automatically to avoid feedback loops, but this should be
|
|
// safe. And if this fails, then that's fine.
|
|
let system_playback_port_name = &format!("system:playback_{port_no}");
|
|
let _ = client.connect_ports_by_name(&port.name()?, system_playback_port_name);
|
|
|
|
outputs.push(port);
|
|
}
|
|
|
|
let midi_input = if P::MIDI_INPUT >= MidiConfig::Basic {
|
|
Some(Arc::new(client.register_port("midi_input", MidiIn)?))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
let midi_output = if P::MIDI_OUTPUT >= MidiConfig::Basic {
|
|
Some(Arc::new(AtomicRefCell::new(
|
|
client.register_port("midi_output", MidiOut)?,
|
|
)))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// This option can either be set to a single port all inputs should be connected to, or a
|
|
// comma separated list of ports
|
|
if let Some(port_name) = &config.connect_jack_inputs {
|
|
if port_name.contains(',') {
|
|
for (port_name, input) in port_name.split(',').zip(&inputs) {
|
|
if let Err(err) = client.connect_ports_by_name(port_name, &input.name()?) {
|
|
nih_error!("Could not connect to '{port_name}': {err}");
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
for input in &inputs {
|
|
if let Err(err) = client.connect_ports_by_name(port_name, &input.name()?) {
|
|
nih_error!("Could not connect to '{port_name}': {err}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let (Some(port), Some(port_name)) = (&midi_input, &config.connect_jack_midi_input) {
|
|
if let Err(err) = client.connect_ports_by_name(port_name, &port.name()?) {
|
|
nih_error!("Could not connect to '{port_name}': {err}");
|
|
}
|
|
}
|
|
if let (Some(port), Some(port_name)) = (&midi_output, &config.connect_jack_midi_output) {
|
|
if let Err(err) = client.connect_ports_by_name(&port.borrow().name()?, port_name) {
|
|
nih_error!("Could not connect to '{port_name}': {err}");
|
|
}
|
|
}
|
|
|
|
Ok(Self {
|
|
config,
|
|
client: Some(client),
|
|
|
|
inputs: Arc::new(inputs),
|
|
outputs: Arc::new(AtomicRefCell::new(outputs)),
|
|
midi_input,
|
|
midi_output,
|
|
})
|
|
}
|
|
}
|