From 7e0caa4c8dc66afd9fb9aea1908e227a99ddb928 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Tue, 14 Jun 2022 18:59:54 +0200 Subject: [PATCH] Add audio support to the JACK backend --- src/wrapper/standalone/backend.rs | 2 +- src/wrapper/standalone/backend/dummy.rs | 2 +- src/wrapper/standalone/backend/jack.rs | 101 +++++++++++++++++++++--- 3 files changed, 91 insertions(+), 14 deletions(-) diff --git a/src/wrapper/standalone/backend.rs b/src/wrapper/standalone/backend.rs index 3fb4bed2..e100286f 100644 --- a/src/wrapper/standalone/backend.rs +++ b/src/wrapper/standalone/backend.rs @@ -14,5 +14,5 @@ pub trait Backend: 'static + Send + Sync { /// /// TODO: MIDI /// TODO: Auxiliary inputs and outputs - fn run(&mut self, cb: impl FnMut(&mut Buffer) -> bool); + fn run(&mut self, cb: impl FnMut(&mut Buffer) -> bool + 'static + Send); } diff --git a/src/wrapper/standalone/backend/dummy.rs b/src/wrapper/standalone/backend/dummy.rs index 89ff9b0a..f248a24a 100644 --- a/src/wrapper/standalone/backend/dummy.rs +++ b/src/wrapper/standalone/backend/dummy.rs @@ -12,7 +12,7 @@ pub struct Dummy { } impl Backend for Dummy { - fn run(&mut self, mut cb: impl FnMut(&mut Buffer) -> bool) { + fn run(&mut self, mut cb: impl FnMut(&mut Buffer) -> bool + 'static + Send) { // We can't really do anything meaningful here, so we'll simply periodically call the // callback with empty buffers. let interval = diff --git a/src/wrapper/standalone/backend/jack.rs b/src/wrapper/standalone/backend/jack.rs index b159e710..c730c3f6 100644 --- a/src/wrapper/standalone/backend/jack.rs +++ b/src/wrapper/standalone/backend/jack.rs @@ -1,5 +1,9 @@ +use std::sync::Arc; + use anyhow::{Context, Result}; -use jack::{AudioIn, AudioOut, Client, ClientOptions, Port}; +use atomic_refcell::AtomicRefCell; +use crossbeam::channel; +use jack::{AudioIn, AudioOut, Client, ClientOptions, ClosureProcessHandler, Control, Port}; use super::super::config::WrapperConfig; use super::Backend; @@ -7,17 +11,91 @@ use crate::buffer::Buffer; /// Uses JACK audio and MIDI. pub struct Jack { - config: WrapperConfig, - client: Client, + /// 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, - inputs: Vec>, - outputs: Vec>, + inputs: Arc>>, + outputs: Arc>>>, +} + +/// 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, cb: impl FnMut(&mut Buffer) -> bool) { - // TODO: Create an async client and do The Thing (tm) - todo!() + fn run(&mut self, mut cb: impl FnMut(&mut Buffer) -> 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 (control_sender, control_receiver) = channel::bounded(32); + let inputs = self.inputs.clone(); + let outputs = self.outputs.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; + } + + // 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 _); + } + }) + } + + if cb(&mut buffer) { + 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); } } @@ -50,11 +128,10 @@ impl Jack { // TODO: Command line argument to connect the inputs? Ok(Self { - config, - client, + client: Some(client), - inputs, - outputs, + inputs: Arc::new(inputs), + outputs: Arc::new(AtomicRefCell::new(outputs)), }) } }