From 9560ebf8931e06fc4533f0adf65d89a3fc108a6e Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Fri, 10 Apr 2020 11:36:24 +0200 Subject: [PATCH 01/27] Adds jack dependency --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 97375d123..df6fe1f86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["audio", "sound"] [features] asio = ["asio-sys"] # Only available on Windows. See README for setup instructions. + [dependencies] thiserror = "1.0.2" lazy_static = "1.3" @@ -29,6 +30,7 @@ parking_lot = "0.9" [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))'.dependencies] alsa-sys = { version = "0.1", path = "alsa-sys" } libc = "0.2" +jack = { version = "0.6.2", optional = true } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] coreaudio-rs = { version = "0.9.1", default-features = false, features = ["audio_unit", "core_audio"] } From b3d6a20b956643959953940ab3fb73fea4b61ee0 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Mon, 13 Apr 2020 14:18:01 +0200 Subject: [PATCH 02/27] First almost compiling commit of JACK implementation --- src/host/jack/device.rs | 231 +++++++++++++++++++++++++++++++ src/host/jack/mod.rs | 151 +++++++++++++++++++++ src/host/jack/stream.rs | 292 ++++++++++++++++++++++++++++++++++++++++ src/host/mod.rs | 2 + 4 files changed, 676 insertions(+) create mode 100644 src/host/jack/device.rs create mode 100644 src/host/jack/mod.rs create mode 100644 src/host/jack/stream.rs diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs new file mode 100644 index 000000000..e4eb79525 --- /dev/null +++ b/src/host/jack/device.rs @@ -0,0 +1,231 @@ +use crate::{ + BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, + PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, BackendSpecificError +}; +use std::cell::RefCell; +use std::hash::{Hash, Hasher}; +use traits::{DeviceTrait, HostTrait, StreamTrait}; + +use super::stream::Stream; +use super::JACK_SAMPLE_FORMAT; + +pub type SupportedInputConfigs = std::vec::IntoIter; +pub type SupportedOutputConfigs = std::vec::IntoIter; + +const DEFAULT_NUM_CHANNELS: u16 = 2; +const DEFAULT_SUPPORTED_CHANNELS: [u16; 10] = [1, 2, 4, 6, 8, 16, 24, 32, 48, 64]; + + +/// If a device is for input or output. +/// Until we have duplex stream support JACK clients and CPAL devices for JACK will be either input or output. +#[derive(Clone, Debug)] +pub enum DeviceType { + InputDevice, + OutputDevice, +} +#[derive(Clone, Debug)] +pub struct Device { + name: String, + sample_rate: SampleRate, + device_type: DeviceType, + start_server_automatically: bool, + connect_ports_automatically: bool, +} + +impl Device { + fn new_device( + name: &str, + connect_ports_automatically: bool, + start_server_automatically: bool, + device_type: DeviceType, + ) -> Result { + // ClientOptions are bit flags that you can set with the constants provided + let client_options = super::get_client_options(start_server_automatically); + + // Create a dummy client to find out the sample rate of the server to be able to provide it as a possible config. + // This client will be dropped and a new one will be created when making the stream. + // This is a hack due to the fact that the Client must be moved to create the AsyncClient. + match super::get_client(name, client_options) { + Ok(client) => Ok(Device { + name: client.name().to_string(), + sample_rate: SampleRate(client.sample_rate() as u32), + device_type, + start_server_automatically, + connect_ports_automatically, + }), + Err(e) => Err(e), + } + } + + pub fn default_output_device( + name: &str, + connect_ports_automatically: bool, + start_server_automatically: bool, + ) -> Result { + let output_client_name = format!("{}_out", name); + Device::new_device( + &output_client_name, + connect_ports_automatically, + start_server_automatically, + DeviceType::OutputDevice, + ) + } + + pub fn default_input_device( + name: &str, + connect_ports_automatically: bool, + start_server_automatically: bool, + ) -> Result { + let input_client_name = format!("{}_in", name); + Device::new_device( + &input_client_name, + connect_ports_automatically, + start_server_automatically, + DeviceType::InputDevice, + ) + } + + pub fn default_config(&self) -> Result { + let channels = DEFAULT_NUM_CHANNELS; + let sample_rate = self.sample_rate; + // The sample format for JACK audio ports is always "32 bit float mono audio" in the current implementation. + // Custom formats are allowed within JACK, but this is of niche interest. + // The format can be found programmatically by calling jack::PortSpec::port_type() on a created port. + let sample_format = JACK_SAMPLE_FORMAT; + Ok(SupportedStreamConfig { + channels, + sample_rate, + sample_format, + }) + } + + pub fn supported_configs(&self) -> Vec { + let mut f = match self.default_config() { + Err(_) => return vec![], + Ok(f) => f, + }; + + let mut supported_configs = vec![]; + + for &channels in DEFAULT_SUPPORTED_CHANNELS.iter() { + f.channels = channels; + supported_configs.push(SupportedStreamConfigRange::from(f.clone())); + } + supported_configs + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn name(&self) -> Result { + Ok(self.name) + } + + fn supported_input_configs( + &self, + ) -> Result { + Ok(self.supported_configs().into_iter()) + } + + fn supported_output_configs( + &self, + ) -> Result { + Ok(self.supported_configs().into_iter()) + } + + /// Returns the default input config + /// The sample format for JACK audio ports is always "32 bit float mono audio" unless using a custom type. + /// The sample rate is set by the JACK server. + fn default_input_config(&self) -> Result { + self.default_config() + } + + /// Returns the default output config + /// The sample format for JACK audio ports is always "32 bit float mono audio" unless using a custom type. + /// The sample rate is set by the JACK server. + fn default_output_config(&self) -> Result { + self.default_config() + } + + fn build_input_stream_raw( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + ) -> Result + where + D: FnMut(&Data) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + if let DeviceType::OutputDevice = &self.device_type { + // Trying to create an input stream from an output device + return Err(BuildStreamError::StreamConfigNotSupported); + } + if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { + return Err(BuildStreamError::StreamConfigNotSupported); + } + // The settings should be fine, create a Client + let client_options = super::get_client_options(self.start_server_automatically); + let client; + match super::get_client(&self.name, client_options) { + Ok(c) => client = c, + Err(e) => { + return Err(BuildStreamError::BackendSpecific{err: BackendSpecificError{ description: e.to_string()}}) + } + }; + let stream = Stream::new_input(client, conf.channels, data_callback, error_callback); + Ok(stream) + } + + fn build_output_stream_raw( + &self, + conf: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + ) -> Result + where + D: FnMut(&mut Data) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + if let DeviceType::InputDevice = &self.device_type { + // Trying to create an output stream from an input device + return Err(BuildStreamError::StreamConfigNotSupported); + } + if conf.sample_rate != self.sample_rate || sample_format != JACK_SAMPLE_FORMAT { + return Err(BuildStreamError::StreamConfigNotSupported); + } + + // The settings should be fine, create a Client + let client_options = super::get_client_options(self.start_server_automatically); + let client; + match super::get_client(&self.name, client_options) { + Ok(c) => client = c, + Err(e) => { + return Err(BuildStreamError::BackendSpecific{err: BackendSpecificError{ description: e.to_string()}}) + } + }; + let stream = Stream::new_output(client, conf.channels, data_callback, error_callback); + Ok(stream) + } +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + // Device::name() can never fail in this implementation + self.name().unwrap() == other.name().unwrap() + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash(&self, state: &mut H) { + self.name().unwrap().hash(state); + } +} diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs new file mode 100644 index 000000000..ca1142080 --- /dev/null +++ b/src/host/jack/mod.rs @@ -0,0 +1,151 @@ +extern crate jack; + +use crate::{ + BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, + PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, + SupportedStreamConfig, SupportedStreamConfigsError, SupportedStreamConfigRange, +}; +use traits::{DeviceTrait, HostTrait, StreamTrait}; + +use std::cell::RefCell; + +mod device; +use self::device::Device; +mod stream; + +const JACK_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; + +pub type SupportedInputConfigs = std::vec::IntoIter; +pub type SupportedOutputConfigs = std::vec::IntoIter; + +/// The JACK Host type +#[derive(Debug)] +pub struct Host { + /// The name that the client will have in JACK. + /// Until we have duplex streams two clients will be created adding "out" or "in" to the name + /// since names have to be unique. + name: String, + /// If ports are to be connected to the system (soundcard) ports automatically (default is true). + connect_ports_automatically: bool, + /// If the JACK server should be started automatically if it isn't already when creating a Client (default is false). + start_server_automatically: bool, + /// A list of the devices that have been created from this Host. + devices_created: RefCell>, +} + +impl Host { + pub fn new() -> Result { + Ok(Host { + name: "cpal_client".to_owned(), + connect_ports_automatically: true, + start_server_automatically: false, + devices_created: RefCell::new(vec![]), + }) + } + /// Set whether the ports should automatically be connected to system + /// (default is true) + pub fn set_connect_automatically(&mut self, do_connect: bool) { + self.connect_ports_automatically = do_connect; + } + /// Set whether a JACK server should be automatically started if it isn't already. + /// (default is false) + pub fn set_start_server_automatically(&mut self, do_start_server: bool) { + self.start_server_automatically = do_start_server; + } + + pub fn input_device_with_name(&mut self, name: &str) -> Option { + self.name = name.to_owned(); + self.default_input_device() + } + + pub fn output_device_with_name(&mut self, name: &str) -> Option { + self.name = name.to_owned(); + self.default_output_device() + } +} + +impl HostTrait for Host { + + type Device = Device; + type Devices = std::vec::IntoIter; + + fn is_available() -> bool { + // TODO: Determine if JACK is available. What does that mean? That the server is started? + // To properly check if the server is started we need to create a Client, but we cannot do this + // until we know what name to give the client. + true + } + + fn devices(&self) -> Result { + Ok(self.devices_created.borrow().clone().into_iter()) + } + + fn default_input_device(&self) -> Option { + // TODO: Check if a device with that name was already created and add it to the list when created if it wasn't + let device_res = Device::default_input_device(&self.name, self. connect_ports_automatically,self.start_server_automatically); + match device_res { + Ok(device) => Some(device), + Err(err) => { + println!("{}", err); + None + } + } + } + + fn default_output_device(&self) -> Option { + // TODO: Check if a device with that name was already created and add it to the list when created if it wasn't + let device_res = Device::default_output_device(&self.name, self.connect_ports_automatically, self.start_server_automatically); + match device_res { + Ok(device) => Some(device), + Err(err) => { + println!("{}", err); + None + } + } + } +} + +fn get_client_options(start_server_automatically: bool) -> jack::ClientOptions { + let mut client_options = jack::ClientOptions::empty(); + client_options.set( + jack::ClientOptions::NO_START_SERVER, + !start_server_automatically, + ); + client_options +} + +fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { + let c_res = jack::Client::new(name, client_options); + match c_res { + Ok((client, status)) => { + // The ClientStatus can tell us many things + println!( + "Managed to open client {}, with status {:?}!", + client.name(), + status + ); + if status.intersects(jack::ClientStatus::SERVER_ERROR) { + return Err("There was an error communicating with the JACK server!"); + } else if status.intersects(jack::ClientStatus::SERVER_FAILED) { + return Err("Could not connect to the JACK server!"); + } else if status.intersects(jack::ClientStatus::VERSION_ERROR) { + return Err("Error connecting to JACK server: Client's protocol version does not match!"); + } else if status.intersects(jack::ClientStatus::INIT_FAILURE) { + return Err("Error connecting to JACK server: Unable to initialize client!"); + } else if status.intersects(jack::ClientStatus::SHM_FAILURE) { + return Err("Error connecting to JACK server: Unable to access shared memory!"); + } else if status.intersects(jack::ClientStatus::NO_SUCH_CLIENT) { + return Err( + "Error connecting to JACK server: Requested client does not exist!", + ); + } else if status.intersects(jack::ClientStatus::INVALID_OPTION) { + return Err("Error connecting to JACK server: The operation contained an invalid or unsupported option!"); + } + + return Ok(client); + } + Err(e) => { + return Err(&format!("Failed to open client because of error: {:?}", e)); + } + } +} \ No newline at end of file diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs new file mode 100644 index 000000000..9a400409a --- /dev/null +++ b/src/host/jack/stream.rs @@ -0,0 +1,292 @@ +use crate::{ChannelCount}; +use traits::{DeviceTrait, HostTrait, StreamTrait}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use crate::{ + BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, + PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, +}; + +const TEMP_BUFFER_SIZE: usize = 16; +use super::JACK_SAMPLE_FORMAT; +pub struct Stream { + // TODO: It might be faster to send a message when playing/pausing than to check this every iteration + playing: Arc, + async_client: jack::AsyncClient<(), LocalProcessHandler>, + // Port names are stored in order to connect them to other ports in jack automatically + input_port_names: Vec, + output_port_names: Vec, +} + +impl Stream { + // TODO: Return error messages + pub fn new_input( + client: jack::Client, + channels: ChannelCount, + mut data_callback: D, + mut error_callback: E, + ) -> Stream + where + D: FnMut(&Data) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let ports = vec![]; + let port_names: Vec = vec![]; + // Create ports + for i in 0..channels { + let mut port = client + .register_port(&format!("in_{}", i), jack::AudioIn::default()) + .expect("Failed to create JACK port."); + ports.push(port); + if let Ok(port_name) = port.name() { + port_names.push(port_name); + } + } + + let playing = Arc::new(AtomicBool::new(true)); + + let input_process_handler = LocalProcessHandler::new( + vec![], + ports, + SampleRate(client.sample_rate() as u32), + Some(Box::new(data_callback)), + None, + playing.clone(), + client.buffer_size() as usize, + ); + + // TODO: Add notification handler, using the error callback? + let async_client = client.activate_async((), input_process_handler).unwrap(); + + Stream { + playing, + async_client, + input_port_names: port_names, + output_port_names: vec![], + } + } + + pub fn new_output( + client: jack::Client, + channels: ChannelCount, + mut data_callback: D, + mut error_callback: E, + ) -> Stream + where + D: FnMut(&mut Data) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let ports = vec![]; + let port_names: Vec = vec![]; + // Create ports + for i in 0..channels { + let mut port = client + .register_port(&format!("out_{}", i), jack::AudioOut::default()) + .expect("Failed to create JACK port."); + ports.push(port); + if let Ok(port_name) = port.name() { + port_names.push(port_name); + } + } + + let playing = Arc::new(AtomicBool::new(true)); + + let output_process_handler = LocalProcessHandler::new( + ports, + vec![], + SampleRate(client.sample_rate() as u32), + None, + Some(Box::new(data_callback)), + playing.clone(), + client.buffer_size() as usize, + ); + + // TODO: Add notification handler, using the error callback? + let async_client = client.activate_async((), output_process_handler).unwrap(); + + Stream { + playing, + async_client, + input_port_names: vec![], + output_port_names: port_names, + } + } + + /// Connect to the standard system outputs in jack, system:playback_1 and system:playback_2 + /// This has to be done after the client is activated, doing it just after creating the ports doesn't work. + pub fn connect_to_system_outputs(&mut self) { + // Get the system ports + let system_ports = self.async_client.as_client().ports( + Some("system:playback_.*"), + None, + jack::PortFlags::empty(), + ); + + // Connect outputs from this client to the system playback inputs + for i in 0..self.output_port_names.len() { + if i >= system_ports.len() { + break; + } + match self.async_client + .as_client() + .connect_ports_by_name(&self.output_port_names[i], &system_ports[i]) + { + Ok(_) => (), + Err(e) => println!("Unable to connect to port with error {}", e), + } + } + } + + /// Connect to the standard system outputs in jack, system:playback_1 and system:playback_2 + /// This has to be done after the client is activated, doing it just after creating the ports doesn't work. + pub fn connect_to_system_inputs(&mut self) { + // Get the system ports + let system_ports = self.async_client.as_client().ports( + Some("system:capture_.*"), + None, + jack::PortFlags::empty(), + ); + + // Connect outputs from this client to the system playback inputs + for i in 0..self.input_port_names.len() { + if i >= system_ports.len() { + break; + } + match self.async_client + .as_client() + .connect_ports_by_name(&self.input_port_names[i], &system_ports[i]) + { + Ok(_) => (), + Err(e) => println!("Unable to connect to port with error {}", e), + } + } + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + self.playing.store(true, Ordering::SeqCst); + Ok(()) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + self.playing.store(false, Ordering::SeqCst); + Ok(()) + } +} + +struct LocalProcessHandler { + /// No new ports are allowed to be created after the creation of the LocalProcessHandler as that would invalidate the buffer sizes + out_ports: Vec>, + in_ports: Vec>, + // out_port_buffers: Vec<&mut [f32]>, + // in_port_buffers: Vec<&[f32]>, + sample_rate: SampleRate, + input_data_callback: Option>, + output_data_callback: Option>, + // JACK audio samples are 32 bit float (unless you do some custom dark magic) + temp_output_buffer: Vec, + /// The number of frames in the temp_output_buffer + temp_output_buffer_size: usize, + temp_output_buffer_index: usize, + playing: Arc, +} + +impl LocalProcessHandler { + fn new( + out_ports: Vec>, + in_ports: Vec>, + sample_rate: SampleRate, + input_data_callback: Option>, + output_data_callback: Option>, + playing: Arc, + buffer_size: usize, + ) -> Self { + + // TODO: Is there a better way than to allocate the temporary buffer on the heap? + // Allocation happens before any audio callbacks so it should be fine. + let mut temp_output_buffer = Vec::with_capacity(out_ports.len() * buffer_size); + let mut temp_output_buffer_index: usize = 0; + + // let out_port_buffers = Vec::with_capacity(out_ports.len()); + // let in_port_buffers = Vec::with_capacity(in_ports.len()); + + LocalProcessHandler { + out_ports, + in_ports, + // out_port_buffers, + // in_port_buffers, + sample_rate, + input_data_callback, + output_data_callback, + temp_output_buffer, + temp_output_buffer_size: buffer_size, + temp_output_buffer_index: 0, + playing, + } + } + + fn temp_output_buffer_to_data(&mut self) -> Data { + let data = self.temp_output_buffer.as_mut_ptr() as *mut (); + let len = self.temp_output_buffer.len(); + let data = unsafe { Data::from_parts(data, len, JACK_SAMPLE_FORMAT) }; + data + } +} + +impl jack::ProcessHandler for LocalProcessHandler { + fn process(&mut self, _: &jack::Client, process_scope: &jack::ProcessScope) -> jack::Control { + + if !self.playing.load(Ordering::SeqCst) { + return jack::Control::Continue; + } + + let current_buffer_size = process_scope.n_frames() as usize; + + if let Some(input_callback) = &mut self.input_data_callback { + // There is an input callback + // Let's get the data from the input ports and run the callback + + // Get the mutable slices for each input port buffer + // for i in 0..self.in_ports.len() { + // self.in_port_buffers[i] = self.in_ports[i].as_slice(process_scope); + // } + } + + if let Some(output_callback) = &mut self.output_data_callback { + // There is an output callback. + + // Get the mutable slices for each output port buffer + // for i in 0..self.out_ports.len() { + // self.out_port_buffers[i] = self.out_ports[i].as_mut_slice(process_scope); + // } + + // Create a buffer to store the audio data for this tick + let num_out_channels = self.out_ports.len(); + + // Run the output callback on the temporary output buffer until we have filled the output ports + for i in 0..current_buffer_size { + if self.temp_output_buffer_index == self.temp_output_buffer_size { + // Get new samples if the temporary buffer is depleted + let data = self.temp_output_buffer_to_data(); + output_callback(&mut data); + self.temp_output_buffer_index = 0; + } + // Write the interleaved samples e.g. [l0, r0, l1, r1, ..] to each output buffer + for ch_ix in 0..num_out_channels { + // TODO: This could be very slow, it would be faster to store pointers to these slices, but I don't know how + // to avoid lifetime issues and allocation + let output_channel = &mut self.out_ports[ch_ix].as_mut_slice(process_scope); + output_channel[i] = self.temp_output_buffer[ch_ix + self.temp_output_buffer_index*num_out_channels]; + } + // Increase the index into the temporary buffer + self.temp_output_buffer_index += 1; + } + } + + // Continue as normal + jack::Control::Continue + } +} \ No newline at end of file diff --git a/src/host/mod.rs b/src/host/mod.rs index b64fa1217..47caa6729 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -1,5 +1,7 @@ #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))] pub(crate) mod alsa; +#[cfg(all(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), feature = "jack"))] +pub(crate) mod jack; #[cfg(all(windows, feature = "asio"))] pub(crate) mod asio; #[cfg(any(target_os = "macos", target_os = "ios"))] From d6531c9ad871b648575443da66f5116fc825593f Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 14 Apr 2020 09:20:45 +0200 Subject: [PATCH 03/27] First JACK Host version that compiles --- src/host/jack/device.rs | 16 ++++++------- src/host/jack/mod.rs | 45 ++++++++++++++++++++++++------------- src/host/jack/stream.rs | 50 +++++++++++++++++++++-------------------- 3 files changed, 64 insertions(+), 47 deletions(-) diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index e4eb79525..7391aa3d7 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -35,18 +35,18 @@ pub struct Device { impl Device { fn new_device( - name: &str, + name: String, connect_ports_automatically: bool, start_server_automatically: bool, device_type: DeviceType, - ) -> Result { + ) -> Result { // ClientOptions are bit flags that you can set with the constants provided let client_options = super::get_client_options(start_server_automatically); // Create a dummy client to find out the sample rate of the server to be able to provide it as a possible config. // This client will be dropped and a new one will be created when making the stream. // This is a hack due to the fact that the Client must be moved to create the AsyncClient. - match super::get_client(name, client_options) { + match super::get_client(&name, client_options) { Ok(client) => Ok(Device { name: client.name().to_string(), sample_rate: SampleRate(client.sample_rate() as u32), @@ -62,10 +62,10 @@ impl Device { name: &str, connect_ports_automatically: bool, start_server_automatically: bool, - ) -> Result { + ) -> Result { let output_client_name = format!("{}_out", name); Device::new_device( - &output_client_name, + output_client_name, connect_ports_automatically, start_server_automatically, DeviceType::OutputDevice, @@ -76,10 +76,10 @@ impl Device { name: &str, connect_ports_automatically: bool, start_server_automatically: bool, - ) -> Result { + ) -> Result { let input_client_name = format!("{}_in", name); Device::new_device( - &input_client_name, + input_client_name, connect_ports_automatically, start_server_automatically, DeviceType::InputDevice, @@ -122,7 +122,7 @@ impl DeviceTrait for Device { type Stream = Stream; fn name(&self) -> Result { - Ok(self.name) + Ok(self.name.clone()) } fn supported_input_configs( diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index ca1142080..8cac2517c 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -3,7 +3,7 @@ extern crate jack; use crate::{ BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, - SupportedStreamConfig, SupportedStreamConfigsError, SupportedStreamConfigRange, + SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; use traits::{DeviceTrait, HostTrait, StreamTrait}; @@ -65,7 +65,6 @@ impl Host { } impl HostTrait for Host { - type Device = Device; type Devices = std::vec::IntoIter; @@ -82,7 +81,11 @@ impl HostTrait for Host { fn default_input_device(&self) -> Option { // TODO: Check if a device with that name was already created and add it to the list when created if it wasn't - let device_res = Device::default_input_device(&self.name, self. connect_ports_automatically,self.start_server_automatically); + let device_res = Device::default_input_device( + &self.name, + self.connect_ports_automatically, + self.start_server_automatically, + ); match device_res { Ok(device) => Some(device), Err(err) => { @@ -94,7 +97,11 @@ impl HostTrait for Host { fn default_output_device(&self) -> Option { // TODO: Check if a device with that name was already created and add it to the list when created if it wasn't - let device_res = Device::default_output_device(&self.name, self.connect_ports_automatically, self.start_server_automatically); + let device_res = Device::default_output_device( + &self.name, + self.connect_ports_automatically, + self.start_server_automatically, + ); match device_res { Ok(device) => Some(device), Err(err) => { @@ -114,7 +121,7 @@ fn get_client_options(start_server_automatically: bool) -> jack::ClientOptions { client_options } -fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { +fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { let c_res = jack::Client::new(name, client_options); match c_res { Ok((client, status)) => { @@ -125,27 +132,35 @@ fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { - return Err(&format!("Failed to open client because of error: {:?}", e)); + return Err(format!("Failed to open client because of error: {:?}", e)); } } -} \ No newline at end of file +} diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 9a400409a..4495105bf 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -1,6 +1,6 @@ use crate::{ChannelCount}; use traits::{DeviceTrait, HostTrait, StreamTrait}; -use std::sync::Arc; +use std::sync::{Arc, Mutex}; use std::sync::atomic::{AtomicBool, Ordering}; use crate::{ @@ -32,17 +32,17 @@ impl Stream { D: FnMut(&Data) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - let ports = vec![]; - let port_names: Vec = vec![]; + let mut ports = vec![]; + let mut port_names: Vec = vec![]; // Create ports for i in 0..channels { let mut port = client .register_port(&format!("in_{}", i), jack::AudioIn::default()) .expect("Failed to create JACK port."); - ports.push(port); if let Ok(port_name) = port.name() { port_names.push(port_name); - } + } + ports.push(port); } let playing = Arc::new(AtomicBool::new(true)); @@ -51,7 +51,7 @@ impl Stream { vec![], ports, SampleRate(client.sample_rate() as u32), - Some(Box::new(data_callback)), + Some(Arc::new(Mutex::new(Box::new(data_callback)))), None, playing.clone(), client.buffer_size() as usize, @@ -78,17 +78,17 @@ impl Stream { D: FnMut(&mut Data) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { - let ports = vec![]; - let port_names: Vec = vec![]; + let mut ports = vec![]; + let mut port_names: Vec = vec![]; // Create ports for i in 0..channels { let mut port = client .register_port(&format!("out_{}", i), jack::AudioOut::default()) .expect("Failed to create JACK port."); - ports.push(port); if let Ok(port_name) = port.name() { port_names.push(port_name); - } + } + ports.push(port); } let playing = Arc::new(AtomicBool::new(true)); @@ -98,7 +98,7 @@ impl Stream { vec![], SampleRate(client.sample_rate() as u32), None, - Some(Box::new(data_callback)), + Some(Arc::new(Mutex::new(Box::new(data_callback)))), playing.clone(), client.buffer_size() as usize, ); @@ -139,7 +139,7 @@ impl Stream { } } - /// Connect to the standard system outputs in jack, system:playback_1 and system:playback_2 + /// Connect to the standard system outputs in jack, system:capture_1 and system:capture_2 /// This has to be done after the client is activated, doing it just after creating the ports doesn't work. pub fn connect_to_system_inputs(&mut self) { // Get the system ports @@ -184,8 +184,8 @@ struct LocalProcessHandler { // out_port_buffers: Vec<&mut [f32]>, // in_port_buffers: Vec<&[f32]>, sample_rate: SampleRate, - input_data_callback: Option>, - output_data_callback: Option>, + input_data_callback: Option>>>, + output_data_callback: Option>>>, // JACK audio samples are 32 bit float (unless you do some custom dark magic) temp_output_buffer: Vec, /// The number of frames in the temp_output_buffer @@ -199,8 +199,8 @@ impl LocalProcessHandler { out_ports: Vec>, in_ports: Vec>, sample_rate: SampleRate, - input_data_callback: Option>, - output_data_callback: Option>, + input_data_callback: Option>>>, + output_data_callback: Option>>>, playing: Arc, buffer_size: usize, ) -> Self { @@ -227,13 +227,13 @@ impl LocalProcessHandler { playing, } } +} - fn temp_output_buffer_to_data(&mut self) -> Data { - let data = self.temp_output_buffer.as_mut_ptr() as *mut (); - let len = self.temp_output_buffer.len(); - let data = unsafe { Data::from_parts(data, len, JACK_SAMPLE_FORMAT) }; - data - } +fn temp_output_buffer_to_data(temp_output_buffer: &mut Vec) -> Data { + let data = temp_output_buffer.as_mut_ptr() as *mut (); + let len = temp_output_buffer.len(); + let data = unsafe { Data::from_parts(data, len, JACK_SAMPLE_FORMAT) }; + data } impl jack::ProcessHandler for LocalProcessHandler { @@ -255,7 +255,9 @@ impl jack::ProcessHandler for LocalProcessHandler { // } } - if let Some(output_callback) = &mut self.output_data_callback { + if let Some(output_callback_mutex) = &mut self.output_data_callback { + // Nothing else should ever lock this Mutex + let output_callback = &mut *output_callback_mutex.lock().unwrap(); // There is an output callback. // Get the mutable slices for each output port buffer @@ -270,7 +272,7 @@ impl jack::ProcessHandler for LocalProcessHandler { for i in 0..current_buffer_size { if self.temp_output_buffer_index == self.temp_output_buffer_size { // Get new samples if the temporary buffer is depleted - let data = self.temp_output_buffer_to_data(); + let mut data = temp_output_buffer_to_data(&mut self.temp_output_buffer); output_callback(&mut data); self.temp_output_buffer_index = 0; } From e80087d94e777b2316c5ac563b3ce00a2e11d949 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 14 Apr 2020 09:35:19 +0200 Subject: [PATCH 04/27] Add JACK as a Host if the feature is enabled --- src/host/jack/mod.rs | 6 ++++-- src/platform/mod.rs | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index 8cac2517c..a48bc6b30 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -10,13 +10,15 @@ use traits::{DeviceTrait, HostTrait, StreamTrait}; use std::cell::RefCell; mod device; -use self::device::Device; +pub use self::device::{Device}; +pub use self::stream::Stream; mod stream; const JACK_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32; pub type SupportedInputConfigs = std::vec::IntoIter; pub type SupportedOutputConfigs = std::vec::IntoIter; +pub type Devices = std::vec::IntoIter; /// The JACK Host type #[derive(Debug)] @@ -66,7 +68,7 @@ impl Host { impl HostTrait for Host { type Device = Device; - type Devices = std::vec::IntoIter; + type Devices = Devices; fn is_available() -> bool { // TODO: Determine if JACK is available. What does that mean? That the server is started? diff --git a/src/platform/mod.rs b/src/platform/mod.rs index cd5f5290e..594a6b1e1 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -448,12 +448,22 @@ macro_rules! impl_platform_host { // TODO: Add pulseaudio and jack here eventually. #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))] mod platform_impl { + #[cfg(feature = "jack")] + pub use crate::host::jack::{ + Device as JackDevice, Devices as JackDevices, Host as JackHost, Stream as JackStream, + SupportedInputConfigs as JackSupportedInputConfigs, + SupportedOutputConfigs as JackSupportedOutputConfigs, + }; pub use crate::host::alsa::{ Device as AlsaDevice, Devices as AlsaDevices, Host as AlsaHost, Stream as AlsaStream, SupportedInputConfigs as AlsaSupportedInputConfigs, SupportedOutputConfigs as AlsaSupportedOutputConfigs, }; + #[cfg(feature = "jack")] + impl_platform_host!(Jack jack "JACK", Alsa alsa "ALSA"); + + #[cfg(not(feature = "jack"))] impl_platform_host!(Alsa alsa "ALSA"); /// The default host for the current compilation target platform. From 1cf6fff1c4df25531e8cbf4ddea6e2584a350230 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 14 Apr 2020 21:07:09 +0200 Subject: [PATCH 05/27] Zero temp_output_buffer before use --- src/host/jack/stream.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 4495105bf..7e2ad26ca 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -205,9 +205,7 @@ impl LocalProcessHandler { buffer_size: usize, ) -> Self { - // TODO: Is there a better way than to allocate the temporary buffer on the heap? - // Allocation happens before any audio callbacks so it should be fine. - let mut temp_output_buffer = Vec::with_capacity(out_ports.len() * buffer_size); + let mut temp_output_buffer = vec![0.0; out_ports.len() * buffer_size]; let mut temp_output_buffer_index: usize = 0; // let out_port_buffers = Vec::with_capacity(out_ports.len()); From 20464a3446e2ad66826cf040774e57a360d86da6 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 14 Apr 2020 21:11:14 +0200 Subject: [PATCH 06/27] Automatically connect to system ports when creating a stream --- src/host/jack/device.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 7391aa3d7..341e0501f 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -178,7 +178,11 @@ impl DeviceTrait for Device { return Err(BuildStreamError::BackendSpecific{err: BackendSpecificError{ description: e.to_string()}}) } }; - let stream = Stream::new_input(client, conf.channels, data_callback, error_callback); + let mut stream = Stream::new_input(client, conf.channels, data_callback, error_callback); + + if(self.connect_ports_automatically) { + stream.connect_to_system_inputs(); + } Ok(stream) } @@ -210,7 +214,11 @@ impl DeviceTrait for Device { return Err(BuildStreamError::BackendSpecific{err: BackendSpecificError{ description: e.to_string()}}) } }; - let stream = Stream::new_output(client, conf.channels, data_callback, error_callback); + let mut stream = Stream::new_output(client, conf.channels, data_callback, error_callback); + + if(self.connect_ports_automatically) { + stream.connect_to_system_outputs(); + } Ok(stream) } } From fab47f2dcca7b9286dd9c4b70375b7da3d6f30db Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 14 Apr 2020 21:14:06 +0200 Subject: [PATCH 07/27] Formatting --- src/host/jack/device.rs | 25 +++++++++++++++++-------- src/host/jack/mod.rs | 2 +- src/host/jack/stream.rs | 25 +++++++++++++------------ 3 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 341e0501f..cc8fc8ba3 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -1,7 +1,7 @@ use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, - PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, BackendSpecificError + BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, + DevicesError, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, + StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; use std::cell::RefCell; use std::hash::{Hash, Hasher}; @@ -16,7 +16,6 @@ pub type SupportedOutputConfigs = std::vec::IntoIter const DEFAULT_NUM_CHANNELS: u16 = 2; const DEFAULT_SUPPORTED_CHANNELS: [u16; 10] = [1, 2, 4, 6, 8, 16, 24, 32, 48, 64]; - /// If a device is for input or output. /// Until we have duplex stream support JACK clients and CPAL devices for JACK will be either input or output. #[derive(Clone, Debug)] @@ -175,14 +174,19 @@ impl DeviceTrait for Device { match super::get_client(&self.name, client_options) { Ok(c) => client = c, Err(e) => { - return Err(BuildStreamError::BackendSpecific{err: BackendSpecificError{ description: e.to_string()}}) + return Err(BuildStreamError::BackendSpecific { + err: BackendSpecificError { + description: e.to_string(), + }, + }) } }; let mut stream = Stream::new_input(client, conf.channels, data_callback, error_callback); - if(self.connect_ports_automatically) { + if self.connect_ports_automatically { stream.connect_to_system_inputs(); } + Ok(stream) } @@ -211,14 +215,19 @@ impl DeviceTrait for Device { match super::get_client(&self.name, client_options) { Ok(c) => client = c, Err(e) => { - return Err(BuildStreamError::BackendSpecific{err: BackendSpecificError{ description: e.to_string()}}) + return Err(BuildStreamError::BackendSpecific { + err: BackendSpecificError { + description: e.to_string(), + }, + }) } }; let mut stream = Stream::new_output(client, conf.channels, data_callback, error_callback); - if(self.connect_ports_automatically) { + if self.connect_ports_automatically { stream.connect_to_system_outputs(); } + Ok(stream) } } diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index a48bc6b30..412589042 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -10,7 +10,7 @@ use traits::{DeviceTrait, HostTrait, StreamTrait}; use std::cell::RefCell; mod device; -pub use self::device::{Device}; +pub use self::device::Device; pub use self::stream::Stream; mod stream; diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 7e2ad26ca..b191b526a 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -1,7 +1,7 @@ -use crate::{ChannelCount}; -use traits::{DeviceTrait, HostTrait, StreamTrait}; -use std::sync::{Arc, Mutex}; +use crate::ChannelCount; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, @@ -59,7 +59,7 @@ impl Stream { // TODO: Add notification handler, using the error callback? let async_client = client.activate_async((), input_process_handler).unwrap(); - + Stream { playing, async_client, @@ -87,7 +87,7 @@ impl Stream { .expect("Failed to create JACK port."); if let Ok(port_name) = port.name() { port_names.push(port_name); - } + } ports.push(port); } @@ -105,7 +105,7 @@ impl Stream { // TODO: Add notification handler, using the error callback? let async_client = client.activate_async((), output_process_handler).unwrap(); - + Stream { playing, async_client, @@ -129,7 +129,8 @@ impl Stream { if i >= system_ports.len() { break; } - match self.async_client + match self + .async_client .as_client() .connect_ports_by_name(&self.output_port_names[i], &system_ports[i]) { @@ -154,7 +155,8 @@ impl Stream { if i >= system_ports.len() { break; } - match self.async_client + match self + .async_client .as_client() .connect_ports_by_name(&self.input_port_names[i], &system_ports[i]) { @@ -204,7 +206,6 @@ impl LocalProcessHandler { playing: Arc, buffer_size: usize, ) -> Self { - let mut temp_output_buffer = vec![0.0; out_ports.len() * buffer_size]; let mut temp_output_buffer_index: usize = 0; @@ -236,7 +237,6 @@ fn temp_output_buffer_to_data(temp_output_buffer: &mut Vec) -> Data { impl jack::ProcessHandler for LocalProcessHandler { fn process(&mut self, _: &jack::Client, process_scope: &jack::ProcessScope) -> jack::Control { - if !self.playing.load(Ordering::SeqCst) { return jack::Control::Continue; } @@ -279,7 +279,8 @@ impl jack::ProcessHandler for LocalProcessHandler { // TODO: This could be very slow, it would be faster to store pointers to these slices, but I don't know how // to avoid lifetime issues and allocation let output_channel = &mut self.out_ports[ch_ix].as_mut_slice(process_scope); - output_channel[i] = self.temp_output_buffer[ch_ix + self.temp_output_buffer_index*num_out_channels]; + output_channel[i] = self.temp_output_buffer + [ch_ix + self.temp_output_buffer_index * num_out_channels]; } // Increase the index into the temporary buffer self.temp_output_buffer_index += 1; @@ -289,4 +290,4 @@ impl jack::ProcessHandler for LocalProcessHandler { // Continue as normal jack::Control::Continue } -} \ No newline at end of file +} From b29926f3583699ca4b3a04af1a26f4d78541894d Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 14 Apr 2020 21:54:59 +0200 Subject: [PATCH 08/27] Input streams now work and connect correctly to the system capture ports --- src/host/jack/stream.rs | 48 +++++++++++++++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index b191b526a..4c265cf20 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -158,7 +158,7 @@ impl Stream { match self .async_client .as_client() - .connect_ports_by_name(&self.input_port_names[i], &system_ports[i]) + .connect_ports_by_name(&system_ports[i], &self.input_port_names[i]) { Ok(_) => (), Err(e) => println!("Unable to connect to port with error {}", e), @@ -188,6 +188,12 @@ struct LocalProcessHandler { sample_rate: SampleRate, input_data_callback: Option>>>, output_data_callback: Option>>>, + + temp_input_buffer: Vec, + /// The number of frames in the temp_input_buffer i.e. temp_input_buffer.len() / in_ports.len() + temp_input_buffer_size: usize, + temp_input_buffer_index: usize, + // JACK audio samples are 32 bit float (unless you do some custom dark magic) temp_output_buffer: Vec, /// The number of frames in the temp_output_buffer @@ -206,8 +212,11 @@ impl LocalProcessHandler { playing: Arc, buffer_size: usize, ) -> Self { + // buffer_size is the maximum number of samples per port JACK can request/provide in a single call + // If it can be fewer than that per call the temp_input_buffer needs to be the smallest multiple of that. + let mut temp_input_buffer = vec![0.0; in_ports.len() * buffer_size]; + let mut temp_output_buffer = vec![0.0; out_ports.len() * buffer_size]; - let mut temp_output_buffer_index: usize = 0; // let out_port_buffers = Vec::with_capacity(out_ports.len()); // let in_port_buffers = Vec::with_capacity(in_ports.len()); @@ -220,6 +229,9 @@ impl LocalProcessHandler { sample_rate, input_data_callback, output_data_callback, + temp_input_buffer, + temp_input_buffer_size: buffer_size, + temp_input_buffer_index: 0, temp_output_buffer, temp_output_buffer_size: buffer_size, temp_output_buffer_index: 0, @@ -235,6 +247,14 @@ fn temp_output_buffer_to_data(temp_output_buffer: &mut Vec) -> Data { data } +fn temp_input_buffer_to_data(temp_input_buffer: &mut Vec, total_buffer_size: usize) -> Data { + let slice = &temp_input_buffer[0..total_buffer_size]; + let data = slice.as_ptr() as *mut (); + let len = temp_input_buffer.len(); + let data = unsafe { Data::from_parts(data, len, JACK_SAMPLE_FORMAT) }; + data +} + impl jack::ProcessHandler for LocalProcessHandler { fn process(&mut self, _: &jack::Client, process_scope: &jack::ProcessScope) -> jack::Control { if !self.playing.load(Ordering::SeqCst) { @@ -243,30 +263,40 @@ impl jack::ProcessHandler for LocalProcessHandler { let current_buffer_size = process_scope.n_frames() as usize; - if let Some(input_callback) = &mut self.input_data_callback { + if let Some(input_callback_mutex) = &mut self.input_data_callback { // There is an input callback + let input_callback = &mut *input_callback_mutex.lock().unwrap(); // Let's get the data from the input ports and run the callback - // Get the mutable slices for each input port buffer - // for i in 0..self.in_ports.len() { - // self.in_port_buffers[i] = self.in_ports[i].as_slice(process_scope); - // } + let num_in_channels = self.in_ports.len(); + + // Read the data from the input ports into the temporary buffer + // Go through every channel and store its data in the temporary input buffer + for ch_ix in 0..num_in_channels { + let input_channel = &self.in_ports[ch_ix].as_slice(process_scope); + for i in 0..current_buffer_size { + self.temp_input_buffer[ch_ix + i * num_in_channels] = input_channel[i]; + } + } + // Create a slice of exactly current_buffer_size frames + let data = temp_input_buffer_to_data(&mut self.temp_input_buffer, current_buffer_size*num_in_channels); + input_callback(&data); } if let Some(output_callback_mutex) = &mut self.output_data_callback { // Nothing else should ever lock this Mutex let output_callback = &mut *output_callback_mutex.lock().unwrap(); - // There is an output callback. // Get the mutable slices for each output port buffer // for i in 0..self.out_ports.len() { // self.out_port_buffers[i] = self.out_ports[i].as_mut_slice(process_scope); // } - // Create a buffer to store the audio data for this tick let num_out_channels = self.out_ports.len(); // Run the output callback on the temporary output buffer until we have filled the output ports + // JACK ports each provide a mutable slice to be filled with samples whereas CPAL uses interleaved + // channels. The formats therefore have to be bridged. for i in 0..current_buffer_size { if self.temp_output_buffer_index == self.temp_output_buffer_size { // Get new samples if the temporary buffer is depleted From 634d1e15d98b87587ec8208f3c3f07a617ff96ea Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 14 Apr 2020 21:57:17 +0200 Subject: [PATCH 09/27] Fix input data buffer size --- src/host/jack/stream.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 4c265cf20..d8e0bed2a 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -250,7 +250,7 @@ fn temp_output_buffer_to_data(temp_output_buffer: &mut Vec) -> Data { fn temp_input_buffer_to_data(temp_input_buffer: &mut Vec, total_buffer_size: usize) -> Data { let slice = &temp_input_buffer[0..total_buffer_size]; let data = slice.as_ptr() as *mut (); - let len = temp_input_buffer.len(); + let len = total_buffer_size; let data = unsafe { Data::from_parts(data, len, JACK_SAMPLE_FORMAT) }; data } From 820d153ddbdcbefce566a4c6db8ef3b6b4e6745c Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Thu, 16 Apr 2020 21:07:53 +0200 Subject: [PATCH 10/27] Adds device enumeration (Devices) --- src/host/jack/device.rs | 14 +++++++ src/host/jack/mod.rs | 81 ++++++++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index cc8fc8ba3..b5ce8b088 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -113,6 +113,20 @@ impl Device { } supported_configs } + + pub fn is_input(&self) -> bool { + match self.device_type { + DeviceType::InputDevice => true, + _ => false + } + } + + pub fn is_output(&self) -> bool { + match self.device_type { + DeviceType::OutputDevice => true, + _ => false + } + } } impl DeviceTrait for Device { diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index 412589042..4faafccfe 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -32,17 +32,20 @@ pub struct Host { /// If the JACK server should be started automatically if it isn't already when creating a Client (default is false). start_server_automatically: bool, /// A list of the devices that have been created from this Host. - devices_created: RefCell>, + devices_created: Vec, } impl Host { pub fn new() -> Result { - Ok(Host { + let mut host = Host { name: "cpal_client".to_owned(), connect_ports_automatically: true, start_server_automatically: false, - devices_created: RefCell::new(vec![]), - }) + devices_created: vec![], + }; + // Devices don't exist for JACK, they have to be created + host.initialize_default_devices(); + Ok(host) } /// Set whether the ports should automatically be connected to system /// (default is true) @@ -64,6 +67,33 @@ impl Host { self.name = name.to_owned(); self.default_output_device() } + + fn initialize_default_devices(&mut self) { + let in_device_res = Device::default_input_device( + &self.name, + self.connect_ports_automatically, + self.start_server_automatically, + ); + + match in_device_res { + Ok(device) => self.devices_created.push(device), + Err(err) => { + println!("{}", err); + } + } + + let out_device_res = Device::default_output_device( + &self.name, + self.connect_ports_automatically, + self.start_server_automatically, + ); + match out_device_res { + Ok(device) => self.devices_created.push(device), + Err(err) => { + println!("{}", err); + } + } + } } impl HostTrait for Host { @@ -71,46 +101,37 @@ impl HostTrait for Host { type Devices = Devices; fn is_available() -> bool { - // TODO: Determine if JACK is available. What does that mean? That the server is started? - // To properly check if the server is started we need to create a Client, but we cannot do this - // until we know what name to give the client. + // JACK is available if + // - the jack feature flag is set + // - libjack is installed (wouldn't compile without it) + // - the JACK server can be started + // + // There is no way to know if the user has set up a correct JACK configuration e.g. with qjackctl + // Users can choose to automatically start the server if it isn't already started when creating a client + // so this should always return true. true } fn devices(&self) -> Result { - Ok(self.devices_created.borrow().clone().into_iter()) + Ok(self.devices_created.clone().into_iter()) } fn default_input_device(&self) -> Option { - // TODO: Check if a device with that name was already created and add it to the list when created if it wasn't - let device_res = Device::default_input_device( - &self.name, - self.connect_ports_automatically, - self.start_server_automatically, - ); - match device_res { - Ok(device) => Some(device), - Err(err) => { - println!("{}", err); - None + for device in &self.devices_created { + if device.is_input() { + return Some(device.clone()); } } + None } fn default_output_device(&self) -> Option { - // TODO: Check if a device with that name was already created and add it to the list when created if it wasn't - let device_res = Device::default_output_device( - &self.name, - self.connect_ports_automatically, - self.start_server_automatically, - ); - match device_res { - Ok(device) => Some(device), - Err(err) => { - println!("{}", err); - None + for device in &self.devices_created { + if device.is_output() { + return Some(device.clone()); } } + None } } From bebea2d919c80068ebec0469f4060e676522c80a Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Sun, 5 Jul 2020 10:54:58 +0200 Subject: [PATCH 11/27] Clarify motivation for the Host::is_available() function always returning true --- src/host/jack/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index 4faafccfe..edb420d7c 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -106,7 +106,8 @@ impl HostTrait for Host { // - libjack is installed (wouldn't compile without it) // - the JACK server can be started // - // There is no way to know if the user has set up a correct JACK configuration e.g. with qjackctl + // If the code compiles the necessary jack libraries are installed. + // There is no way to know if the user has set up a correct JACK configuration e.g. with qjackctl. // Users can choose to automatically start the server if it isn't already started when creating a client // so this should always return true. true From d42dc3effca38b85601f024fc674dd545f495ef9 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Sun, 5 Jul 2020 10:55:14 +0200 Subject: [PATCH 12/27] Clarify motivation for the Host::is_available() function always returning true --- src/host/jack/mod.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index edb420d7c..dbbcb21f3 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -100,16 +100,17 @@ impl HostTrait for Host { type Device = Device; type Devices = Devices; + /// JACK is available if + /// - the jack feature flag is set + /// - libjack is installed (wouldn't compile without it) + /// - the JACK server can be started + /// + /// If the code compiles the necessary jack libraries are installed. + /// There is no way to know if the user has set up a correct JACK configuration e.g. with qjackctl. + /// Users can choose to automatically start the server if it isn't already started when creating a client + /// so checking if the server is running could give a false negative in some use cases. + /// For these reasons this function should always return true. fn is_available() -> bool { - // JACK is available if - // - the jack feature flag is set - // - libjack is installed (wouldn't compile without it) - // - the JACK server can be started - // - // If the code compiles the necessary jack libraries are installed. - // There is no way to know if the user has set up a correct JACK configuration e.g. with qjackctl. - // Users can choose to automatically start the server if it isn't already started when creating a client - // so this should always return true. true } From e77125e4c2cba31e697bfc84abef71519ba0e95f Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Sun, 5 Jul 2020 11:58:46 +0200 Subject: [PATCH 13/27] Removes .expect() on Stream creation, instead sending the error through the error_callback --- src/host/jack/stream.rs | 44 +++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index d8e0bed2a..ffd2bcbe9 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -36,13 +36,22 @@ impl Stream { let mut port_names: Vec = vec![]; // Create ports for i in 0..channels { - let mut port = client - .register_port(&format!("in_{}", i), jack::AudioIn::default()) - .expect("Failed to create JACK port."); - if let Ok(port_name) = port.name() { - port_names.push(port_name); + let mut port_try = client + .register_port(&format!("out_{}", i), jack::AudioOut::default()); + match(port_try) { + Ok(port) => { + // Get the port name in order to later connect it automatically + if let Ok(port_name) = port.name() { + port_names.push(port_name); + } + // Store the port into a Vec to move to the ProcessHandler + ports.push(port); + }, + Err(e) => { + // If port creation failed, send the error back via the error_callback + error_callback(BackendSpecificError { e }.into()); + } } - ports.push(port); } let playing = Arc::new(AtomicBool::new(true)); @@ -82,13 +91,22 @@ impl Stream { let mut port_names: Vec = vec![]; // Create ports for i in 0..channels { - let mut port = client - .register_port(&format!("out_{}", i), jack::AudioOut::default()) - .expect("Failed to create JACK port."); - if let Ok(port_name) = port.name() { - port_names.push(port_name); + let mut port_try = client + .register_port(&format!("out_{}", i), jack::AudioOut::default()); + match(port_try) { + Ok(port) => { + // Get the port name in order to later connect it automatically + if let Ok(port_name) = port.name() { + port_names.push(port_name); + } + // Store the port into a Vec to move to the ProcessHandler + ports.push(port); + }, + Err(e) => { + // If port creation failed, send the error back via the error_callback + error_callback(BackendSpecificError { e }.into()); + } } - ports.push(port); } let playing = Arc::new(AtomicBool::new(true)); @@ -320,4 +338,4 @@ impl jack::ProcessHandler for LocalProcessHandler { // Continue as normal jack::Control::Continue } -} +} \ No newline at end of file From 60f3697f688c32b637156182b1ad2df20a78270a Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Sun, 5 Jul 2020 13:00:36 +0200 Subject: [PATCH 14/27] Adds a notification handler for error reporting --- src/host/jack/stream.rs | 63 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index ffd2bcbe9..97949e758 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use traits::{DeviceTrait, HostTrait, StreamTrait}; use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, + BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, }; @@ -37,8 +37,8 @@ impl Stream { // Create ports for i in 0..channels { let mut port_try = client - .register_port(&format!("out_{}", i), jack::AudioOut::default()); - match(port_try) { + .register_port(&format!("in_{}", i), jack::AudioIn::default()); + match port_try { Ok(port) => { // Get the port name in order to later connect it automatically if let Ok(port_name) = port.name() { @@ -49,7 +49,7 @@ impl Stream { }, Err(e) => { // If port creation failed, send the error back via the error_callback - error_callback(BackendSpecificError { e }.into()); + error_callback(BackendSpecificError { description: e.to_string() }.into()); } } } @@ -93,7 +93,7 @@ impl Stream { for i in 0..channels { let mut port_try = client .register_port(&format!("out_{}", i), jack::AudioOut::default()); - match(port_try) { + match port_try { Ok(port) => { // Get the port name in order to later connect it automatically if let Ok(port_name) = port.name() { @@ -104,7 +104,7 @@ impl Stream { }, Err(e) => { // If port creation failed, send the error back via the error_callback - error_callback(BackendSpecificError { e }.into()); + error_callback(BackendSpecificError { description: e.to_string() }.into()); } } } @@ -338,4 +338,55 @@ impl jack::ProcessHandler for LocalProcessHandler { // Continue as normal jack::Control::Continue } +} + +/// Receives notifications from the JACK server. It is unclear if this may be run concurrent with itself under JACK2 specs +/// so it needs to be Sync. +struct JackNotificationHandler { + error_callback_ptr: Arc>>, +} + +impl JackNotificationHandler { + pub fn new(mut error_callback: E) -> Self + where + E: FnMut(StreamError) + Send + 'static, + { + JackNotificationHandler { + error_callback_ptr: Arc::new(Mutex::new(Box::new(error_callback))) + } + } + + fn send_error(&mut self, description: String) { + // This thread isn't the audio thread, it's fine to block + if let Ok(mut mutex_guard) = self.error_callback_ptr.lock() { + let err = &mut *mutex_guard; + err(BackendSpecificError { description }.into()); + } + } +} + +impl jack::NotificationHandler for JackNotificationHandler { + fn shutdown(&mut self, _status: jack::ClientStatus, reason: &str) { + self.send_error(format!("JACK was shut down for reason: {}", reason)); + } + + fn sample_rate(&mut self, _: &jack::Client, srate: jack::Frames) -> jack::Control { + self.send_error(format!("sample rate changed to: {}", srate)); + // Since CPAL currently has no way of signaling a sample rate change in order to make + // all necessary changes that would bring we choose to quit. + jack::Control::Quit + } + + fn buffer_size(&mut self, _: &jack::Client, size: jack::Frames) -> jack::Control { + self.send_error(format!("buffer size changed to: {}", size)); + // The current implementation should work even if the buffer size changes, although + // potentially with poorer performance. However, reallocating the temporary processing + // buffers would be expensive so we choose to just continue in this case. + jack::Control::Continue + } + + fn xrun(&mut self, _: &jack::Client) -> jack::Control { + self.send_error(String::from("xrun (buffer over or under run)")); + jack::Control::Continue + } } \ No newline at end of file From 3f13df0aaf24d43b772e235ad27ab2cbf04327e7 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Sun, 5 Jul 2020 13:53:17 +0200 Subject: [PATCH 15/27] Adds a notification handler for error reporting --- src/host/jack/mod.rs | 2 +- src/host/jack/stream.rs | 50 +++++++++++++++++++++++++++++++---------- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index dbbcb21f3..d271e5c8e 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -152,7 +152,7 @@ fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { // The ClientStatus can tell us many things println!( - "Managed to open client {}, with status {:?}!", + "Succeeded to open client {}, with status {:?}!", client.name(), status ); diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 97949e758..c4a75ff98 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -14,7 +14,7 @@ use super::JACK_SAMPLE_FORMAT; pub struct Stream { // TODO: It might be faster to send a message when playing/pausing than to check this every iteration playing: Arc, - async_client: jack::AsyncClient<(), LocalProcessHandler>, + async_client: jack::AsyncClient, // Port names are stored in order to connect them to other ports in jack automatically input_port_names: Vec, output_port_names: Vec, @@ -66,8 +66,9 @@ impl Stream { client.buffer_size() as usize, ); - // TODO: Add notification handler, using the error callback? - let async_client = client.activate_async((), input_process_handler).unwrap(); + let notification_handler = JackNotificationHandler::new(error_callback); + + let async_client = client.activate_async(notification_handler, input_process_handler).unwrap(); Stream { playing, @@ -121,8 +122,9 @@ impl Stream { client.buffer_size() as usize, ); - // TODO: Add notification handler, using the error callback? - let async_client = client.activate_async((), output_process_handler).unwrap(); + let notification_handler = JackNotificationHandler::new(error_callback); + + let async_client = client.activate_async(notification_handler, output_process_handler).unwrap(); Stream { playing, @@ -344,15 +346,19 @@ impl jack::ProcessHandler for LocalProcessHandler { /// so it needs to be Sync. struct JackNotificationHandler { error_callback_ptr: Arc>>, + init_block_size_flag: Arc, + init_sample_rate_flag: Arc, } impl JackNotificationHandler { pub fn new(mut error_callback: E) -> Self where E: FnMut(StreamError) + Send + 'static, - { + { JackNotificationHandler { - error_callback_ptr: Arc::new(Mutex::new(Box::new(error_callback))) + error_callback_ptr: Arc::new(Mutex::new(Box::new(error_callback))), + init_block_size_flag: Arc::new(AtomicBool::new(false)), + init_sample_rate_flag: Arc::new(AtomicBool::new(false)), } } @@ -371,14 +377,34 @@ impl jack::NotificationHandler for JackNotificationHandler { } fn sample_rate(&mut self, _: &jack::Client, srate: jack::Frames) -> jack::Control { - self.send_error(format!("sample rate changed to: {}", srate)); - // Since CPAL currently has no way of signaling a sample rate change in order to make - // all necessary changes that would bring we choose to quit. - jack::Control::Quit + + match self.init_sample_rate_flag.load(Ordering::SeqCst) { + false => { + // One of these notifications is sent every time a client is started. + self.init_sample_rate_flag.store(true, Ordering::SeqCst); + jack::Control::Continue + }, + true => { + self.send_error(format!("sample rate changed to: {}", srate)); + // Since CPAL currently has no way of signaling a sample rate change in order to make + // all necessary changes that would bring we choose to quit. + jack::Control::Quit + } + } } fn buffer_size(&mut self, _: &jack::Client, size: jack::Frames) -> jack::Control { - self.send_error(format!("buffer size changed to: {}", size)); + + match self.init_block_size_flag.load(Ordering::SeqCst) { + false => { + // One of these notifications is sent every time a client is started. + self.init_block_size_flag.store(true, Ordering::SeqCst) + }, + true => { + self.send_error(format!("buffer size changed to: {}", size)); + } + } + // The current implementation should work even if the buffer size changes, although // potentially with poorer performance. However, reallocating the temporary processing // buffers would be expensive so we choose to just continue in this case. From 64b9b4e564161d11dbe587e7cd287bc67f67534e Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Sun, 5 Jul 2020 21:20:51 +0200 Subject: [PATCH 16/27] Remove confusing success message --- src/host/jack/device.rs | 3 ++- src/host/jack/mod.rs | 5 ----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index b5ce8b088..6d4d182ff 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -47,7 +47,8 @@ impl Device { // This is a hack due to the fact that the Client must be moved to create the AsyncClient. match super::get_client(&name, client_options) { Ok(client) => Ok(Device { - name: client.name().to_string(), + // The name given to the client by JACK, could potentially be different from the name supplied e.g.if there is a name collision + name: client.name().to_string(), sample_rate: SampleRate(client.sample_rate() as u32), device_type, start_server_automatically, diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index d271e5c8e..7658c2f98 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -151,11 +151,6 @@ fn get_client(name: &str, client_options: jack::ClientOptions) -> Result { // The ClientStatus can tell us many things - println!( - "Succeeded to open client {}, with status {:?}!", - client.name(), - status - ); if status.intersects(jack::ClientStatus::SERVER_ERROR) { return Err(String::from( "There was an error communicating with the JACK server!", From 43b0b6c2e58657274b844cfaa842ba5e0618fb5e Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 15 Sep 2020 11:48:36 +0200 Subject: [PATCH 17/27] Bump jack dependency to 0.6.5 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index df6fe1f86..1e6ad5dca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ parking_lot = "0.9" [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))'.dependencies] alsa-sys = { version = "0.1", path = "alsa-sys" } libc = "0.2" -jack = { version = "0.6.2", optional = true } +jack = { version = "0.6.5", optional = true } [target.'cfg(any(target_os = "macos", target_os = "ios"))'.dependencies] coreaudio-rs = { version = "0.9.1", default-features = false, features = ["audio_unit", "core_audio"] } From 891d6eb32ddc0d61d2b7203d164330627dee6c04 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 15 Sep 2020 11:53:55 +0200 Subject: [PATCH 18/27] Remove ArcMutex wrapper around callbacks --- src/host/jack/stream.rs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index c4a75ff98..da75a74d8 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -60,7 +60,7 @@ impl Stream { vec![], ports, SampleRate(client.sample_rate() as u32), - Some(Arc::new(Mutex::new(Box::new(data_callback)))), + Some(Box::new(data_callback)), None, playing.clone(), client.buffer_size() as usize, @@ -117,7 +117,7 @@ impl Stream { vec![], SampleRate(client.sample_rate() as u32), None, - Some(Arc::new(Mutex::new(Box::new(data_callback)))), + Some(Box::new(data_callback)), playing.clone(), client.buffer_size() as usize, ); @@ -206,8 +206,8 @@ struct LocalProcessHandler { // out_port_buffers: Vec<&mut [f32]>, // in_port_buffers: Vec<&[f32]>, sample_rate: SampleRate, - input_data_callback: Option>>>, - output_data_callback: Option>>>, + input_data_callback: Option>, + output_data_callback: Option>, temp_input_buffer: Vec, /// The number of frames in the temp_input_buffer i.e. temp_input_buffer.len() / in_ports.len() @@ -227,8 +227,8 @@ impl LocalProcessHandler { out_ports: Vec>, in_ports: Vec>, sample_rate: SampleRate, - input_data_callback: Option>>>, - output_data_callback: Option>>>, + input_data_callback: Option>, + output_data_callback: Option>, playing: Arc, buffer_size: usize, ) -> Self { @@ -283,9 +283,7 @@ impl jack::ProcessHandler for LocalProcessHandler { let current_buffer_size = process_scope.n_frames() as usize; - if let Some(input_callback_mutex) = &mut self.input_data_callback { - // There is an input callback - let input_callback = &mut *input_callback_mutex.lock().unwrap(); + if let Some(input_callback) = &mut self.input_data_callback { // Let's get the data from the input ports and run the callback let num_in_channels = self.in_ports.len(); @@ -303,9 +301,7 @@ impl jack::ProcessHandler for LocalProcessHandler { input_callback(&data); } - if let Some(output_callback_mutex) = &mut self.output_data_callback { - // Nothing else should ever lock this Mutex - let output_callback = &mut *output_callback_mutex.lock().unwrap(); + if let Some(output_callback) = &mut self.output_data_callback { // Get the mutable slices for each output port buffer // for i in 0..self.out_ports.len() { From 67d635b2b1f0d2718744eeb216307f17bbc51e41 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 15 Sep 2020 14:50:03 +0200 Subject: [PATCH 19/27] Bring up to date with master branch, implement timestamps, run rustfmt --- src/host/jack/device.rs | 34 ++++++--- src/host/jack/mod.rs | 12 +-- src/host/jack/stream.rs | 163 +++++++++++++++++++++++++++++----------- src/host/mod.rs | 7 +- src/platform/mod.rs | 10 +-- 5 files changed, 157 insertions(+), 69 deletions(-) diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 6d4d182ff..5e7cad44b 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -1,11 +1,11 @@ use crate::{ BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, - DevicesError, PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, - StreamError, SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, + SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, }; -use std::cell::RefCell; use std::hash::{Hash, Hasher}; -use traits::{DeviceTrait, HostTrait, StreamTrait}; +use traits::DeviceTrait; use super::stream::Stream; use super::JACK_SAMPLE_FORMAT; @@ -27,6 +27,7 @@ pub enum DeviceType { pub struct Device { name: String, sample_rate: SampleRate, + buffer_size: SupportedBufferSize, device_type: DeviceType, start_server_automatically: bool, connect_ports_automatically: bool, @@ -48,8 +49,12 @@ impl Device { match super::get_client(&name, client_options) { Ok(client) => Ok(Device { // The name given to the client by JACK, could potentially be different from the name supplied e.g.if there is a name collision - name: client.name().to_string(), + name: client.name().to_string(), sample_rate: SampleRate(client.sample_rate() as u32), + buffer_size: SupportedBufferSize::Range { + min: client.buffer_size(), + max: client.buffer_size(), + }, device_type, start_server_automatically, connect_ports_automatically, @@ -89,6 +94,7 @@ impl Device { pub fn default_config(&self) -> Result { let channels = DEFAULT_NUM_CHANNELS; let sample_rate = self.sample_rate; + let buffer_size = self.buffer_size.clone(); // The sample format for JACK audio ports is always "32 bit float mono audio" in the current implementation. // Custom formats are allowed within JACK, but this is of niche interest. // The format can be found programmatically by calling jack::PortSpec::port_type() on a created port. @@ -96,6 +102,7 @@ impl Device { Ok(SupportedStreamConfig { channels, sample_rate, + buffer_size, sample_format, }) } @@ -109,8 +116,13 @@ impl Device { let mut supported_configs = vec![]; for &channels in DEFAULT_SUPPORTED_CHANNELS.iter() { - f.channels = channels; - supported_configs.push(SupportedStreamConfigRange::from(f.clone())); + supported_configs.push(SupportedStreamConfigRange { + channels, + min_sample_rate: f.sample_rate, + max_sample_rate: f.sample_rate, + buffer_size: f.buffer_size.clone(), + sample_format: f.sample_format.clone(), + }); } supported_configs } @@ -118,14 +130,14 @@ impl Device { pub fn is_input(&self) -> bool { match self.device_type { DeviceType::InputDevice => true, - _ => false + _ => false, } } pub fn is_output(&self) -> bool { match self.device_type { DeviceType::OutputDevice => true, - _ => false + _ => false, } } } @@ -173,7 +185,7 @@ impl DeviceTrait for Device { error_callback: E, ) -> Result where - D: FnMut(&Data) + Send + 'static, + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { if let DeviceType::OutputDevice = &self.device_type { @@ -213,7 +225,7 @@ impl DeviceTrait for Device { error_callback: E, ) -> Result where - D: FnMut(&mut Data) + Send + 'static, + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { if let DeviceType::InputDevice = &self.device_type { diff --git a/src/host/jack/mod.rs b/src/host/jack/mod.rs index 7658c2f98..d52624f83 100644 --- a/src/host/jack/mod.rs +++ b/src/host/jack/mod.rs @@ -1,13 +1,7 @@ extern crate jack; -use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, - PauseStreamError, PlayStreamError, SampleFormat, StreamConfig, StreamError, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, -}; -use traits::{DeviceTrait, HostTrait, StreamTrait}; - -use std::cell::RefCell; +use crate::{DevicesError, SampleFormat, SupportedStreamConfigRange}; +use traits::HostTrait; mod device; pub use self::device::Device; @@ -104,7 +98,7 @@ impl HostTrait for Host { /// - the jack feature flag is set /// - libjack is installed (wouldn't compile without it) /// - the JACK server can be started - /// + /// /// If the code compiles the necessary jack libraries are installed. /// There is no way to know if the user has set up a correct JACK configuration e.g. with qjackctl. /// Users can choose to automatically start the server if it isn't already started when creating a client diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index da75a74d8..b2c0b9eab 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -1,12 +1,11 @@ use crate::ChannelCount; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use traits::{DeviceTrait, HostTrait, StreamTrait}; +use traits::StreamTrait; use crate::{ - BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, - PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, - SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, + BackendSpecificError, Data, InputCallbackInfo, OutputCallbackInfo, PauseStreamError, + PlayStreamError, SampleRate, StreamError, }; const TEMP_BUFFER_SIZE: usize = 16; @@ -29,15 +28,14 @@ impl Stream { mut error_callback: E, ) -> Stream where - D: FnMut(&Data) + Send + 'static, + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { let mut ports = vec![]; let mut port_names: Vec = vec![]; // Create ports for i in 0..channels { - let mut port_try = client - .register_port(&format!("in_{}", i), jack::AudioIn::default()); + let mut port_try = client.register_port(&format!("in_{}", i), jack::AudioIn::default()); match port_try { Ok(port) => { // Get the port name in order to later connect it automatically @@ -46,10 +44,15 @@ impl Stream { } // Store the port into a Vec to move to the ProcessHandler ports.push(port); - }, + } Err(e) => { // If port creation failed, send the error back via the error_callback - error_callback(BackendSpecificError { description: e.to_string() }.into()); + error_callback( + BackendSpecificError { + description: e.to_string(), + } + .into(), + ); } } } @@ -68,7 +71,9 @@ impl Stream { let notification_handler = JackNotificationHandler::new(error_callback); - let async_client = client.activate_async(notification_handler, input_process_handler).unwrap(); + let async_client = client + .activate_async(notification_handler, input_process_handler) + .unwrap(); Stream { playing, @@ -85,15 +90,15 @@ impl Stream { mut error_callback: E, ) -> Stream where - D: FnMut(&mut Data) + Send + 'static, + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, E: FnMut(StreamError) + Send + 'static, { let mut ports = vec![]; let mut port_names: Vec = vec![]; // Create ports for i in 0..channels { - let mut port_try = client - .register_port(&format!("out_{}", i), jack::AudioOut::default()); + let mut port_try = + client.register_port(&format!("out_{}", i), jack::AudioOut::default()); match port_try { Ok(port) => { // Get the port name in order to later connect it automatically @@ -102,10 +107,15 @@ impl Stream { } // Store the port into a Vec to move to the ProcessHandler ports.push(port); - }, + } Err(e) => { // If port creation failed, send the error back via the error_callback - error_callback(BackendSpecificError { description: e.to_string() }.into()); + error_callback( + BackendSpecificError { + description: e.to_string(), + } + .into(), + ); } } } @@ -124,7 +134,9 @@ impl Stream { let notification_handler = JackNotificationHandler::new(error_callback); - let async_client = client.activate_async(notification_handler, output_process_handler).unwrap(); + let async_client = client + .activate_async(notification_handler, output_process_handler) + .unwrap(); Stream { playing, @@ -206,8 +218,8 @@ struct LocalProcessHandler { // out_port_buffers: Vec<&mut [f32]>, // in_port_buffers: Vec<&[f32]>, sample_rate: SampleRate, - input_data_callback: Option>, - output_data_callback: Option>, + input_data_callback: Option>, + output_data_callback: Option>, temp_input_buffer: Vec, /// The number of frames in the temp_input_buffer i.e. temp_input_buffer.len() / in_ports.len() @@ -220,6 +232,7 @@ struct LocalProcessHandler { temp_output_buffer_size: usize, temp_output_buffer_index: usize, playing: Arc, + creation_timestamp: std::time::Instant, } impl LocalProcessHandler { @@ -227,8 +240,10 @@ impl LocalProcessHandler { out_ports: Vec>, in_ports: Vec>, sample_rate: SampleRate, - input_data_callback: Option>, - output_data_callback: Option>, + input_data_callback: Option>, + output_data_callback: Option< + Box, + >, playing: Arc, buffer_size: usize, ) -> Self { @@ -256,6 +271,7 @@ impl LocalProcessHandler { temp_output_buffer_size: buffer_size, temp_output_buffer_index: 0, playing, + creation_timestamp: std::time::Instant::now(), } } } @@ -281,7 +297,27 @@ impl jack::ProcessHandler for LocalProcessHandler { return jack::Control::Continue; } - let current_buffer_size = process_scope.n_frames() as usize; + let current_frame_count = process_scope.n_frames() as usize; + + // Get timestamp data + let cycle_times = process_scope.cycle_times(); + let current_start_usecs = match cycle_times { + Ok(times) => times.current_usecs, + Err(_) => { + // jack was unable to get the current time information + // Fall back to using Instants + let now = std::time::Instant::now(); + let duration = now.duration_since(self.creation_timestamp); + duration.as_micros() as u64 + } + }; + let start_cycle_instant = micros_to_stream_instant(current_start_usecs); + let start_callback_instant = start_cycle_instant + .add(frames_to_duration( + process_scope.frames_since_cycle_start() as usize, + self.sample_rate, + )) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); if let Some(input_callback) = &mut self.input_data_callback { // Let's get the data from the input ports and run the callback @@ -292,17 +328,29 @@ impl jack::ProcessHandler for LocalProcessHandler { // Go through every channel and store its data in the temporary input buffer for ch_ix in 0..num_in_channels { let input_channel = &self.in_ports[ch_ix].as_slice(process_scope); - for i in 0..current_buffer_size { + for i in 0..current_frame_count { self.temp_input_buffer[ch_ix + i * num_in_channels] = input_channel[i]; } } - // Create a slice of exactly current_buffer_size frames - let data = temp_input_buffer_to_data(&mut self.temp_input_buffer, current_buffer_size*num_in_channels); - input_callback(&data); + // Create a slice of exactly current_frame_count frames + let data = temp_input_buffer_to_data( + &mut self.temp_input_buffer, + current_frame_count * num_in_channels, + ); + // Create timestamp + let frames_since_cycle_start = process_scope.frames_since_cycle_start() as usize; + let duration_since_cycle_start = + frames_to_duration(frames_since_cycle_start, self.sample_rate); + let callback = start_callback_instant + .add(duration_since_cycle_start) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let capture = start_callback_instant; + let timestamp = crate::InputStreamTimestamp { callback, capture }; + let info = crate::InputCallbackInfo { timestamp }; + input_callback(&data, &info); } if let Some(output_callback) = &mut self.output_data_callback { - // Get the mutable slices for each output port buffer // for i in 0..self.out_ports.len() { // self.out_port_buffers[i] = self.out_ports[i].as_mut_slice(process_scope); @@ -311,18 +359,36 @@ impl jack::ProcessHandler for LocalProcessHandler { let num_out_channels = self.out_ports.len(); // Run the output callback on the temporary output buffer until we have filled the output ports - // JACK ports each provide a mutable slice to be filled with samples whereas CPAL uses interleaved + // JACK ports each provide a mutable slice to be filled with samples whereas CPAL uses interleaved // channels. The formats therefore have to be bridged. - for i in 0..current_buffer_size { + for i in 0..current_frame_count { if self.temp_output_buffer_index == self.temp_output_buffer_size { - // Get new samples if the temporary buffer is depleted + // Get new samples if the temporary buffer is depleted. This can theoretically happen + // several times per cycle or once every few cycles if the buffer size changes, but in practice + // it should generally happen once per cycle if the buffer size is not changed. let mut data = temp_output_buffer_to_data(&mut self.temp_output_buffer); - output_callback(&mut data); + // Create timestamp + let frames_since_cycle_start = + process_scope.frames_since_cycle_start() as usize; + let duration_since_cycle_start = + frames_to_duration(frames_since_cycle_start, self.sample_rate); + let callback = start_callback_instant + .add(duration_since_cycle_start) + .expect( + "`playback` occurs beyond representation supported by `StreamInstant`", + ); + let buffer_duration = frames_to_duration(current_frame_count, self.sample_rate); + let playback = start_cycle_instant.add(buffer_duration).expect( + "`playback` occurs beyond representation supported by `StreamInstant`", + ); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + let info = crate::OutputCallbackInfo { timestamp }; + output_callback(&mut data, &info); self.temp_output_buffer_index = 0; } // Write the interleaved samples e.g. [l0, r0, l1, r1, ..] to each output buffer for ch_ix in 0..num_out_channels { - // TODO: This could be very slow, it would be faster to store pointers to these slices, but I don't know how + // TODO: It should be marginally faster to store pointers to these slices, but I don't know how // to avoid lifetime issues and allocation let output_channel = &mut self.out_ports[ch_ix].as_mut_slice(process_scope); output_channel[i] = self.temp_output_buffer @@ -338,6 +404,21 @@ impl jack::ProcessHandler for LocalProcessHandler { } } +fn micros_to_stream_instant(micros: u64) -> crate::StreamInstant { + let nanos = micros * 1000; + let secs = nanos / 1_000_000; + let subsec_nanos = nanos - secs * 1_000_000_000; + crate::StreamInstant::new(secs as i64, subsec_nanos as u32) +} + +// Convert the given duration in frames at the given sample rate to a `std::time::Duration`. +fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { + let secsf = frames as f64 / rate.0 as f64; + let secs = secsf as u64; + let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; + std::time::Duration::new(secs, nanos) +} + /// Receives notifications from the JACK server. It is unclear if this may be run concurrent with itself under JACK2 specs /// so it needs to be Sync. struct JackNotificationHandler { @@ -348,9 +429,9 @@ struct JackNotificationHandler { impl JackNotificationHandler { pub fn new(mut error_callback: E) -> Self - where + where E: FnMut(StreamError) + Send + 'static, - { + { JackNotificationHandler { error_callback_ptr: Arc::new(Mutex::new(Box::new(error_callback))), init_block_size_flag: Arc::new(AtomicBool::new(false)), @@ -373,13 +454,12 @@ impl jack::NotificationHandler for JackNotificationHandler { } fn sample_rate(&mut self, _: &jack::Client, srate: jack::Frames) -> jack::Control { - match self.init_sample_rate_flag.load(Ordering::SeqCst) { false => { // One of these notifications is sent every time a client is started. self.init_sample_rate_flag.store(true, Ordering::SeqCst); jack::Control::Continue - }, + } true => { self.send_error(format!("sample rate changed to: {}", srate)); // Since CPAL currently has no way of signaling a sample rate change in order to make @@ -389,20 +469,19 @@ impl jack::NotificationHandler for JackNotificationHandler { } } - fn buffer_size(&mut self, _: &jack::Client, size: jack::Frames) -> jack::Control { - + fn buffer_size(&mut self, _: &jack::Client, size: jack::Frames) -> jack::Control { match self.init_block_size_flag.load(Ordering::SeqCst) { false => { // One of these notifications is sent every time a client is started. self.init_block_size_flag.store(true, Ordering::SeqCst) - }, + } true => { self.send_error(format!("buffer size changed to: {}", size)); } } - - // The current implementation should work even if the buffer size changes, although - // potentially with poorer performance. However, reallocating the temporary processing + + // The current implementation should work even if the buffer size changes, although + // potentially with poorer performance. However, reallocating the temporary processing // buffers would be expensive so we choose to just continue in this case. jack::Control::Continue } @@ -411,4 +490,4 @@ impl jack::NotificationHandler for JackNotificationHandler { self.send_error(String::from("xrun (buffer over or under run)")); jack::Control::Continue } -} \ No newline at end of file +} diff --git a/src/host/mod.rs b/src/host/mod.rs index 6caf46516..534939a7d 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -1,13 +1,16 @@ #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))] pub(crate) mod alsa; -#[cfg(all(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), feature = "jack"))] -pub(crate) mod jack; #[cfg(all(windows, feature = "asio"))] pub(crate) mod asio; #[cfg(any(target_os = "macos", target_os = "ios"))] pub(crate) mod coreaudio; #[cfg(target_os = "emscripten")] pub(crate) mod emscripten; +#[cfg(all( + any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), + feature = "jack" +))] +pub(crate) mod jack; pub(crate) mod null; #[cfg(windows)] pub(crate) mod wasapi; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 5731dc5b9..311a9029e 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -448,17 +448,17 @@ macro_rules! impl_platform_host { // TODO: Add pulseaudio and jack here eventually. #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))] mod platform_impl { + pub use crate::host::alsa::{ + Device as AlsaDevice, Devices as AlsaDevices, Host as AlsaHost, Stream as AlsaStream, + SupportedInputConfigs as AlsaSupportedInputConfigs, + SupportedOutputConfigs as AlsaSupportedOutputConfigs, + }; #[cfg(feature = "jack")] pub use crate::host::jack::{ Device as JackDevice, Devices as JackDevices, Host as JackHost, Stream as JackStream, SupportedInputConfigs as JackSupportedInputConfigs, SupportedOutputConfigs as JackSupportedOutputConfigs, }; - pub use crate::host::alsa::{ - Device as AlsaDevice, Devices as AlsaDevices, Host as AlsaHost, Stream as AlsaStream, - SupportedInputConfigs as AlsaSupportedInputConfigs, - SupportedOutputConfigs as AlsaSupportedOutputConfigs, - }; #[cfg(feature = "jack")] impl_platform_host!(Jack jack "JACK", Alsa alsa "ALSA"); From 18bd609cca690a617522549dc31926ffc1883411 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 15 Sep 2020 14:51:32 +0200 Subject: [PATCH 20/27] Add jack examples to make it easy to test --- examples/jack-beep.rs | 69 ++++++++++++++++++++++++ examples/jack-feedback.rs | 110 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) create mode 100644 examples/jack-beep.rs create mode 100644 examples/jack-feedback.rs diff --git a/examples/jack-beep.rs b/examples/jack-beep.rs new file mode 100644 index 000000000..8fac6dc8a --- /dev/null +++ b/examples/jack-beep.rs @@ -0,0 +1,69 @@ +extern crate cpal; + +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + HostId, +}; + +fn main() -> Result<(), anyhow::Error> { + let host = cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == HostId::Jack) + .expect( + "make sure --features jack is specified. only works on OSes where jack is available", + )).expect("jack host unavailable"); + let device = host + .default_output_device() + .expect("failed to find a default output device"); + let config = device.default_output_config()?; + + match config.sample_format() { + cpal::SampleFormat::F32 => run::(&device, &config.into())?, + _ => panic!("only F32 supported on jack"), + } + + Ok(()) +} + +fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error> +where + T: cpal::Sample, +{ + let sample_rate = config.sample_rate.0 as f32; + let channels = config.channels as usize; + + // Produce a sinusoid of maximum amplitude. + let mut sample_clock = 0f32; + let mut next_value = move || { + sample_clock = (sample_clock + 1.0) % sample_rate; + (sample_clock * 440.0 * 2.0 * 3.141592 / sample_rate).sin() + }; + + let err_fn = |err| eprintln!("an error occurred on stream: {}", err); + + let stream = device.build_output_stream( + config, + move |data: &mut [T], _: &cpal::OutputCallbackInfo| { + write_data(data, channels, &mut next_value) + }, + err_fn, + )?; + stream.play()?; + + std::thread::sleep(std::time::Duration::from_millis(1_000_000)); + + Ok(()) +} + +fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) +where + T: cpal::Sample, +{ + //println!(); + for frame in output.chunks_mut(channels) { + let value: T = cpal::Sample::from::(&next_sample()); + for sample in frame.iter_mut() { + *sample = value; + } + } +} diff --git a/examples/jack-feedback.rs b/examples/jack-feedback.rs new file mode 100644 index 000000000..16ce74e68 --- /dev/null +++ b/examples/jack-feedback.rs @@ -0,0 +1,110 @@ +//! Feeds back the input stream directly into the output stream. +//! +//! Assumes that the input and output devices can use the same stream configuration and that they +//! support the f32 sample format. +//! +//! Uses a delay of `LATENCY_MS` milliseconds in case the default input and output streams are not +//! precisely synchronised. + +extern crate anyhow; +extern crate cpal; +extern crate ringbuf; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use ringbuf::RingBuffer; + +const LATENCY_MS: f32 = 10.0; + +fn main() -> Result<(), anyhow::Error> { + // let host = cpal::default_host(); + let host = cpal::host_from_id(cpal::HostId::Jack).unwrap(); + + // Default devices. + let input_device = host + .default_input_device() + .expect("failed to get default input device"); + let output_device = host + .default_output_device() + .expect("failed to get default output device"); + println!("Using default input device: \"{}\"", input_device.name()?); + println!("Using default output device: \"{}\"", output_device.name()?); + + // We'll try and use the same configuration between streams to keep it simple. + let config: cpal::StreamConfig = input_device.default_input_config()?.into(); + + // Create a delay in case the input and output devices aren't synced. + let latency_frames = (LATENCY_MS / 1_000.0) * config.sample_rate.0 as f32; + let latency_samples = latency_frames as usize * config.channels as usize; + + // The buffer to share samples + let ring = RingBuffer::new(latency_samples * 2); + let (mut producer, mut consumer) = ring.split(); + + // Fill the samples with 0.0 equal to the length of the delay. + for _ in 0..latency_samples { + // The ring buffer has twice as much space as necessary to add latency here, + // so this should never fail + producer.push(0.0).unwrap(); + } + + let input_data_fn = move |data: &[f32], _: &cpal::InputCallbackInfo| { + let mut output_fell_behind = false; + for &sample in data { + if producer.push(sample).is_err() { + output_fell_behind = true; + } + } + if output_fell_behind { + eprintln!("output stream fell behind: try increasing latency"); + } + }; + + let output_data_fn = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let mut input_fell_behind = None; + for sample in data { + *sample = match consumer.pop() { + Ok(s) => s, + Err(err) => { + input_fell_behind = Some(err); + 0.0 + } + }; + } + if let Some(err) = input_fell_behind { + eprintln!( + "input stream fell behind: {:?}: try increasing latency", + err + ); + } + }; + + // Build streams. + println!( + "Attempting to build both streams with f32 samples and `{:?}`.", + config + ); + let input_stream = input_device.build_input_stream(&config, input_data_fn, err_fn)?; + let output_stream = output_device.build_output_stream(&config, output_data_fn, err_fn)?; + // input_stream.link_to_output_stream(&mut output_stream); + println!("Successfully built streams."); + + // Play the streams. + println!( + "Starting the input and output streams with `{}` milliseconds of latency.", + LATENCY_MS + ); + input_stream.play()?; + output_stream.play()?; + + // Run for 3 seconds before closing. + println!("Playing for 3 seconds... "); + std::thread::sleep(std::time::Duration::from_secs(3)); + drop(input_stream); + drop(output_stream); + println!("Done!"); + Ok(()) +} + +fn err_fn(err: cpal::StreamError) { + eprintln!("an error occurred on stream: {}", err); +} From a5eb8be8db18f1abc7dfab6b46dde0d93f767afa Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 15 Sep 2020 14:51:32 +0200 Subject: [PATCH 21/27] Add jack examples to make it easy to test --- examples/jack-beep.rs | 77 +++++++++++++++++++++++++ examples/jack-feedback.rs | 116 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 examples/jack-beep.rs create mode 100644 examples/jack-feedback.rs diff --git a/examples/jack-beep.rs b/examples/jack-beep.rs new file mode 100644 index 000000000..4198264f5 --- /dev/null +++ b/examples/jack-beep.rs @@ -0,0 +1,77 @@ +extern crate cpal; + +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + HostId, +}; + +fn main() -> Result<(), anyhow::Error> { + // Condintionally compile with jack if the feature is specified. + #[cfg(all( + any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), + feature = "jack" + ))] + let host = cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == HostId::Jack) + .expect( + "make sure --features jack is specified. only works on OSes where jack is available", + )).expect("jack host unavailable"); + #[cfg(not(feature = "jack"))] + let host = cpal::default_host(); + + let device = host + .default_output_device() + .expect("failed to find a default output device"); + let config = device.default_output_config()?; + + match config.sample_format() { + cpal::SampleFormat::F32 => run::(&device, &config.into())?, + _ => panic!("only F32 supported on jack"), + } + + Ok(()) +} + +fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error> +where + T: cpal::Sample, +{ + let sample_rate = config.sample_rate.0 as f32; + let channels = config.channels as usize; + + // Produce a sinusoid of maximum amplitude. + let mut sample_clock = 0f32; + let mut next_value = move || { + sample_clock = (sample_clock + 1.0) % sample_rate; + (sample_clock * 440.0 * 2.0 * 3.141592 / sample_rate).sin() + }; + + let err_fn = |err| eprintln!("an error occurred on stream: {}", err); + + let stream = device.build_output_stream( + config, + move |data: &mut [T], _: &cpal::OutputCallbackInfo| { + write_data(data, channels, &mut next_value) + }, + err_fn, + )?; + stream.play()?; + + std::thread::sleep(std::time::Duration::from_millis(1_000_000)); + + Ok(()) +} + +fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) +where + T: cpal::Sample, +{ + //println!(); + for frame in output.chunks_mut(channels) { + let value: T = cpal::Sample::from::(&next_sample()); + for sample in frame.iter_mut() { + *sample = value; + } + } +} diff --git a/examples/jack-feedback.rs b/examples/jack-feedback.rs new file mode 100644 index 000000000..a79e7294a --- /dev/null +++ b/examples/jack-feedback.rs @@ -0,0 +1,116 @@ +//! Feeds back the input stream directly into the output stream. +//! +//! Assumes that the input and output devices can use the same stream configuration and that they +//! support the f32 sample format. +//! +//! Uses a delay of `LATENCY_MS` milliseconds in case the default input and output streams are not +//! precisely synchronised. + +extern crate anyhow; +extern crate cpal; +extern crate ringbuf; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use ringbuf::RingBuffer; + +const LATENCY_MS: f32 = 10.0; + +fn main() -> Result<(), anyhow::Error> { + // Condintionally compile with jack if the feature is specified. + #[cfg(all( + any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), + feature = "jack" + ))] + let host = cpal::host_from_id(cpal::HostId::Jack).unwrap(); + #[cfg(not(feature = "jack"))] + let host = cpal::default_host(); + + // Default devices. + let input_device = host + .default_input_device() + .expect("failed to get default input device"); + let output_device = host + .default_output_device() + .expect("failed to get default output device"); + println!("Using default input device: \"{}\"", input_device.name()?); + println!("Using default output device: \"{}\"", output_device.name()?); + + // We'll try and use the same configuration between streams to keep it simple. + let config: cpal::StreamConfig = input_device.default_input_config()?.into(); + + // Create a delay in case the input and output devices aren't synced. + let latency_frames = (LATENCY_MS / 1_000.0) * config.sample_rate.0 as f32; + let latency_samples = latency_frames as usize * config.channels as usize; + + // The buffer to share samples + let ring = RingBuffer::new(latency_samples * 2); + let (mut producer, mut consumer) = ring.split(); + + // Fill the samples with 0.0 equal to the length of the delay. + for _ in 0..latency_samples { + // The ring buffer has twice as much space as necessary to add latency here, + // so this should never fail + producer.push(0.0).unwrap(); + } + + let input_data_fn = move |data: &[f32], _: &cpal::InputCallbackInfo| { + let mut output_fell_behind = false; + for &sample in data { + if producer.push(sample).is_err() { + output_fell_behind = true; + } + } + if output_fell_behind { + eprintln!("output stream fell behind: try increasing latency"); + } + }; + + let output_data_fn = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { + let mut input_fell_behind = None; + for sample in data { + *sample = match consumer.pop() { + Ok(s) => s, + Err(err) => { + input_fell_behind = Some(err); + 0.0 + } + }; + } + if let Some(err) = input_fell_behind { + eprintln!( + "input stream fell behind: {:?}: try increasing latency", + err + ); + } + }; + + // Build streams. + println!( + "Attempting to build both streams with f32 samples and `{:?}`.", + config + ); + let input_stream = input_device.build_input_stream(&config, input_data_fn, err_fn)?; + let output_stream = output_device.build_output_stream(&config, output_data_fn, err_fn)?; + // input_stream.link_to_output_stream(&mut output_stream); + println!("Successfully built streams."); + + // Play the streams. + println!( + "Starting the input and output streams with `{}` milliseconds of latency.", + LATENCY_MS + ); + input_stream.play()?; + output_stream.play()?; + + // Run for 3 seconds before closing. + println!("Playing for 3 seconds... "); + std::thread::sleep(std::time::Duration::from_secs(3)); + drop(input_stream); + drop(output_stream); + println!("Done!"); + Ok(()) +} + +fn err_fn(err: cpal::StreamError) { + eprintln!("an error occurred on stream: {}", err); +} From ac7f9d9aa7f4c220eb323cf93c2d6eba008433f7 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Tue, 15 Sep 2020 15:46:49 +0200 Subject: [PATCH 22/27] Fix conditional compilation to not try to use jack on Win or Mac --- examples/jack-beep.rs | 5 ++++- examples/jack-feedback.rs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/jack-beep.rs b/examples/jack-beep.rs index 4198264f5..170fbe36d 100644 --- a/examples/jack-beep.rs +++ b/examples/jack-beep.rs @@ -17,7 +17,10 @@ fn main() -> Result<(), anyhow::Error> { .expect( "make sure --features jack is specified. only works on OSes where jack is available", )).expect("jack host unavailable"); - #[cfg(not(feature = "jack"))] + #[cfg(any( + not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")), + not(feature = "jack") + ))] let host = cpal::default_host(); let device = host diff --git a/examples/jack-feedback.rs b/examples/jack-feedback.rs index a79e7294a..7be7a4e46 100644 --- a/examples/jack-feedback.rs +++ b/examples/jack-feedback.rs @@ -22,7 +22,10 @@ fn main() -> Result<(), anyhow::Error> { feature = "jack" ))] let host = cpal::host_from_id(cpal::HostId::Jack).unwrap(); - #[cfg(not(feature = "jack"))] + #[cfg(any( + not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")), + not(feature = "jack") + ))] let host = cpal::default_host(); // Default devices. From 2ce3961504011c19dc63f35ba83f475bff5924bf Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Thu, 17 Sep 2020 19:22:27 +0200 Subject: [PATCH 23/27] Merge jack examples into ordinary examples --- examples/beep.rs | 35 ++++++++++- examples/feedback.rs | 35 ++++++++++- examples/jack-beep.rs | 80 ------------------------- examples/jack-feedback.rs | 119 -------------------------------------- 4 files changed, 68 insertions(+), 201 deletions(-) delete mode 100644 examples/jack-beep.rs delete mode 100644 examples/jack-feedback.rs diff --git a/examples/beep.rs b/examples/beep.rs index dec1fdfee..c7ab1f05c 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -1,9 +1,42 @@ extern crate anyhow; extern crate cpal; +use std::env; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + HostId, +}; fn main() -> Result<(), anyhow::Error> { + // Manually check for flags. Can be passed through cargo with -- e.g. + // cargo run --release --example beep --features jack -- --jack + let args: Vec = env::args().collect(); + let mut jack_flag = false; + for arg in args { + if arg == "--jack" { + jack_flag = true; + } + } + + // Condintionally compile with jack if the feature is specified. + #[cfg(all( + any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), + feature = "jack" + ))] + let host = if jack_flag { + cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == HostId::Jack) + .expect( + "make sure --features jack is specified. only works on OSes where jack is available", + )).expect("jack host unavailable") + } else { + cpal::default_host() + }; + #[cfg(any( + not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")), + not(feature = "jack") + ))] let host = cpal::default_host(); let device = host .default_output_device() diff --git a/examples/feedback.rs b/examples/feedback.rs index e44f2d038..7847b1283 100644 --- a/examples/feedback.rs +++ b/examples/feedback.rs @@ -10,12 +10,45 @@ extern crate anyhow; extern crate cpal; extern crate ringbuf; -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{ + traits::{DeviceTrait, HostTrait, StreamTrait}, + HostId, +}; use ringbuf::RingBuffer; +use std::env; const LATENCY_MS: f32 = 150.0; fn main() -> Result<(), anyhow::Error> { + // Manually check for flags. Can be passed through cargo with -- e.g. + // cargo run --release --example beep --features jack -- --jack + let args: Vec = env::args().collect(); + let mut jack_flag = false; + for arg in args { + if arg == "--jack" { + jack_flag = true; + } + } + + // Condintionally compile with jack if the feature is specified. + #[cfg(all( + any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), + feature = "jack" + ))] + let host = if jack_flag { + cpal::host_from_id(cpal::available_hosts() + .into_iter() + .find(|id| *id == HostId::Jack) + .expect( + "make sure --features jack is specified. only works on OSes where jack is available", + )).expect("jack host unavailable") + } else { + cpal::default_host() + }; + #[cfg(any( + not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")), + not(feature = "jack") + ))] let host = cpal::default_host(); // Default devices. diff --git a/examples/jack-beep.rs b/examples/jack-beep.rs deleted file mode 100644 index 170fbe36d..000000000 --- a/examples/jack-beep.rs +++ /dev/null @@ -1,80 +0,0 @@ -extern crate cpal; - -use cpal::{ - traits::{DeviceTrait, HostTrait, StreamTrait}, - HostId, -}; - -fn main() -> Result<(), anyhow::Error> { - // Condintionally compile with jack if the feature is specified. - #[cfg(all( - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), - feature = "jack" - ))] - let host = cpal::host_from_id(cpal::available_hosts() - .into_iter() - .find(|id| *id == HostId::Jack) - .expect( - "make sure --features jack is specified. only works on OSes where jack is available", - )).expect("jack host unavailable"); - #[cfg(any( - not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")), - not(feature = "jack") - ))] - let host = cpal::default_host(); - - let device = host - .default_output_device() - .expect("failed to find a default output device"); - let config = device.default_output_config()?; - - match config.sample_format() { - cpal::SampleFormat::F32 => run::(&device, &config.into())?, - _ => panic!("only F32 supported on jack"), - } - - Ok(()) -} - -fn run(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error> -where - T: cpal::Sample, -{ - let sample_rate = config.sample_rate.0 as f32; - let channels = config.channels as usize; - - // Produce a sinusoid of maximum amplitude. - let mut sample_clock = 0f32; - let mut next_value = move || { - sample_clock = (sample_clock + 1.0) % sample_rate; - (sample_clock * 440.0 * 2.0 * 3.141592 / sample_rate).sin() - }; - - let err_fn = |err| eprintln!("an error occurred on stream: {}", err); - - let stream = device.build_output_stream( - config, - move |data: &mut [T], _: &cpal::OutputCallbackInfo| { - write_data(data, channels, &mut next_value) - }, - err_fn, - )?; - stream.play()?; - - std::thread::sleep(std::time::Duration::from_millis(1_000_000)); - - Ok(()) -} - -fn write_data(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32) -where - T: cpal::Sample, -{ - //println!(); - for frame in output.chunks_mut(channels) { - let value: T = cpal::Sample::from::(&next_sample()); - for sample in frame.iter_mut() { - *sample = value; - } - } -} diff --git a/examples/jack-feedback.rs b/examples/jack-feedback.rs deleted file mode 100644 index 7be7a4e46..000000000 --- a/examples/jack-feedback.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Feeds back the input stream directly into the output stream. -//! -//! Assumes that the input and output devices can use the same stream configuration and that they -//! support the f32 sample format. -//! -//! Uses a delay of `LATENCY_MS` milliseconds in case the default input and output streams are not -//! precisely synchronised. - -extern crate anyhow; -extern crate cpal; -extern crate ringbuf; - -use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; -use ringbuf::RingBuffer; - -const LATENCY_MS: f32 = 10.0; - -fn main() -> Result<(), anyhow::Error> { - // Condintionally compile with jack if the feature is specified. - #[cfg(all( - any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), - feature = "jack" - ))] - let host = cpal::host_from_id(cpal::HostId::Jack).unwrap(); - #[cfg(any( - not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")), - not(feature = "jack") - ))] - let host = cpal::default_host(); - - // Default devices. - let input_device = host - .default_input_device() - .expect("failed to get default input device"); - let output_device = host - .default_output_device() - .expect("failed to get default output device"); - println!("Using default input device: \"{}\"", input_device.name()?); - println!("Using default output device: \"{}\"", output_device.name()?); - - // We'll try and use the same configuration between streams to keep it simple. - let config: cpal::StreamConfig = input_device.default_input_config()?.into(); - - // Create a delay in case the input and output devices aren't synced. - let latency_frames = (LATENCY_MS / 1_000.0) * config.sample_rate.0 as f32; - let latency_samples = latency_frames as usize * config.channels as usize; - - // The buffer to share samples - let ring = RingBuffer::new(latency_samples * 2); - let (mut producer, mut consumer) = ring.split(); - - // Fill the samples with 0.0 equal to the length of the delay. - for _ in 0..latency_samples { - // The ring buffer has twice as much space as necessary to add latency here, - // so this should never fail - producer.push(0.0).unwrap(); - } - - let input_data_fn = move |data: &[f32], _: &cpal::InputCallbackInfo| { - let mut output_fell_behind = false; - for &sample in data { - if producer.push(sample).is_err() { - output_fell_behind = true; - } - } - if output_fell_behind { - eprintln!("output stream fell behind: try increasing latency"); - } - }; - - let output_data_fn = move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { - let mut input_fell_behind = None; - for sample in data { - *sample = match consumer.pop() { - Ok(s) => s, - Err(err) => { - input_fell_behind = Some(err); - 0.0 - } - }; - } - if let Some(err) = input_fell_behind { - eprintln!( - "input stream fell behind: {:?}: try increasing latency", - err - ); - } - }; - - // Build streams. - println!( - "Attempting to build both streams with f32 samples and `{:?}`.", - config - ); - let input_stream = input_device.build_input_stream(&config, input_data_fn, err_fn)?; - let output_stream = output_device.build_output_stream(&config, output_data_fn, err_fn)?; - // input_stream.link_to_output_stream(&mut output_stream); - println!("Successfully built streams."); - - // Play the streams. - println!( - "Starting the input and output streams with `{}` milliseconds of latency.", - LATENCY_MS - ); - input_stream.play()?; - output_stream.play()?; - - // Run for 3 seconds before closing. - println!("Playing for 3 seconds... "); - std::thread::sleep(std::time::Duration::from_secs(3)); - drop(input_stream); - drop(output_stream); - println!("Done!"); - Ok(()) -} - -fn err_fn(err: cpal::StreamError) { - eprintln!("an error occurred on stream: {}", err); -} From 43693335e78bbf1ca0a8ae3da1799d49f31de5d8 Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Thu, 17 Sep 2020 19:27:07 +0200 Subject: [PATCH 24/27] Add libjack installation to CI --- .github/workflows/cpal.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cpal.yml b/.github/workflows/cpal.yml index 8b23ef9ff..d1ffc167d 100644 --- a/.github/workflows/cpal.yml +++ b/.github/workflows/cpal.yml @@ -73,6 +73,8 @@ jobs: run: sudo apt update - name: Install alsa run: sudo apt-get install libasound2-dev + - name: Install libjack + run: sudo apt-get install libjack-jackd2-dev libjack-jackd2-0 - name: Install stable uses: actions-rs/toolchain@v1 with: From 6973454001d354029d5e1a4d55eb5114bb645c73 Mon Sep 17 00:00:00 2001 From: Federico Dolce Date: Thu, 1 Oct 2020 10:02:33 +0200 Subject: [PATCH 25/27] Fixed warnings --- examples/beep.rs | 29 +++++++++++------------------ examples/feedback.rs | 28 ++++++++++------------------ src/host/jack/device.rs | 2 +- src/host/jack/stream.rs | 21 +++++++-------------- 4 files changed, 29 insertions(+), 51 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index c7ab1f05c..2f36f3f72 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -1,43 +1,36 @@ extern crate anyhow; extern crate cpal; -use std::env; -use cpal::{ - traits::{DeviceTrait, HostTrait, StreamTrait}, - HostId, -}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; fn main() -> Result<(), anyhow::Error> { - // Manually check for flags. Can be passed through cargo with -- e.g. - // cargo run --release --example beep --features jack -- --jack - let args: Vec = env::args().collect(); - let mut jack_flag = false; - for arg in args { - if arg == "--jack" { - jack_flag = true; - } - } - - // Condintionally compile with jack if the feature is specified. + // Conditionally compile with jack if the feature is specified. #[cfg(all( any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), feature = "jack" ))] - let host = if jack_flag { + // Manually check for flags. Can be passed through cargo with -- e.g. + // cargo run --release --example beep --features jack -- --jack + let host = if std::env::args() + .collect::() + .contains(&String::from("--jack")) + { cpal::host_from_id(cpal::available_hosts() .into_iter() - .find(|id| *id == HostId::Jack) + .find(|id| *id == cpal::HostId::Jack) .expect( "make sure --features jack is specified. only works on OSes where jack is available", )).expect("jack host unavailable") } else { cpal::default_host() }; + #[cfg(any( not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")), not(feature = "jack") ))] let host = cpal::default_host(); + let device = host .default_output_device() .expect("failed to find a default output device"); diff --git a/examples/feedback.rs b/examples/feedback.rs index 7847b1283..72b584168 100644 --- a/examples/feedback.rs +++ b/examples/feedback.rs @@ -10,41 +10,33 @@ extern crate anyhow; extern crate cpal; extern crate ringbuf; -use cpal::{ - traits::{DeviceTrait, HostTrait, StreamTrait}, - HostId, -}; +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use ringbuf::RingBuffer; -use std::env; const LATENCY_MS: f32 = 150.0; fn main() -> Result<(), anyhow::Error> { - // Manually check for flags. Can be passed through cargo with -- e.g. - // cargo run --release --example beep --features jack -- --jack - let args: Vec = env::args().collect(); - let mut jack_flag = false; - for arg in args { - if arg == "--jack" { - jack_flag = true; - } - } - - // Condintionally compile with jack if the feature is specified. + // Conditionally compile with jack if the feature is specified. #[cfg(all( any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"), feature = "jack" ))] - let host = if jack_flag { + // Manually check for flags. Can be passed through cargo with -- e.g. + // cargo run --release --example beep --features jack -- --jack + let host = if std::env::args() + .collect::() + .contains(&String::from("--jack")) + { cpal::host_from_id(cpal::available_hosts() .into_iter() - .find(|id| *id == HostId::Jack) + .find(|id| *id == cpal::HostId::Jack) .expect( "make sure --features jack is specified. only works on OSes where jack is available", )).expect("jack host unavailable") } else { cpal::default_host() }; + #[cfg(any( not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")), not(feature = "jack") diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 5e7cad44b..a98d813c4 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -108,7 +108,7 @@ impl Device { } pub fn supported_configs(&self) -> Vec { - let mut f = match self.default_config() { + let f = match self.default_config() { Err(_) => return vec![], Ok(f) => f, }; diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index b2c0b9eab..b66ba354c 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -8,7 +8,6 @@ use crate::{ PlayStreamError, SampleRate, StreamError, }; -const TEMP_BUFFER_SIZE: usize = 16; use super::JACK_SAMPLE_FORMAT; pub struct Stream { // TODO: It might be faster to send a message when playing/pausing than to check this every iteration @@ -24,7 +23,7 @@ impl Stream { pub fn new_input( client: jack::Client, channels: ChannelCount, - mut data_callback: D, + data_callback: D, mut error_callback: E, ) -> Stream where @@ -35,7 +34,7 @@ impl Stream { let mut port_names: Vec = vec![]; // Create ports for i in 0..channels { - let mut port_try = client.register_port(&format!("in_{}", i), jack::AudioIn::default()); + let port_try = client.register_port(&format!("in_{}", i), jack::AudioIn::default()); match port_try { Ok(port) => { // Get the port name in order to later connect it automatically @@ -86,7 +85,7 @@ impl Stream { pub fn new_output( client: jack::Client, channels: ChannelCount, - mut data_callback: D, + data_callback: D, mut error_callback: E, ) -> Stream where @@ -97,8 +96,7 @@ impl Stream { let mut port_names: Vec = vec![]; // Create ports for i in 0..channels { - let mut port_try = - client.register_port(&format!("out_{}", i), jack::AudioOut::default()); + let port_try = client.register_port(&format!("out_{}", i), jack::AudioOut::default()); match port_try { Ok(port) => { // Get the port name in order to later connect it automatically @@ -222,9 +220,6 @@ struct LocalProcessHandler { output_data_callback: Option>, temp_input_buffer: Vec, - /// The number of frames in the temp_input_buffer i.e. temp_input_buffer.len() / in_ports.len() - temp_input_buffer_size: usize, - temp_input_buffer_index: usize, // JACK audio samples are 32 bit float (unless you do some custom dark magic) temp_output_buffer: Vec, @@ -249,9 +244,9 @@ impl LocalProcessHandler { ) -> Self { // buffer_size is the maximum number of samples per port JACK can request/provide in a single call // If it can be fewer than that per call the temp_input_buffer needs to be the smallest multiple of that. - let mut temp_input_buffer = vec![0.0; in_ports.len() * buffer_size]; + let temp_input_buffer = vec![0.0; in_ports.len() * buffer_size]; - let mut temp_output_buffer = vec![0.0; out_ports.len() * buffer_size]; + let temp_output_buffer = vec![0.0; out_ports.len() * buffer_size]; // let out_port_buffers = Vec::with_capacity(out_ports.len()); // let in_port_buffers = Vec::with_capacity(in_ports.len()); @@ -265,8 +260,6 @@ impl LocalProcessHandler { input_data_callback, output_data_callback, temp_input_buffer, - temp_input_buffer_size: buffer_size, - temp_input_buffer_index: 0, temp_output_buffer, temp_output_buffer_size: buffer_size, temp_output_buffer_index: 0, @@ -428,7 +421,7 @@ struct JackNotificationHandler { } impl JackNotificationHandler { - pub fn new(mut error_callback: E) -> Self + pub fn new(error_callback: E) -> Self where E: FnMut(StreamError) + Send + 'static, { From b8456ed7668f17ee6fa9292fa1cbbec2fe4e5dcb Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Thu, 1 Oct 2020 10:42:49 +0200 Subject: [PATCH 26/27] Renaming and comments for added clarity, removes some commented out code --- src/host/jack/stream.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index b66ba354c..02d13c16c 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -213,8 +213,7 @@ struct LocalProcessHandler { /// No new ports are allowed to be created after the creation of the LocalProcessHandler as that would invalidate the buffer sizes out_ports: Vec>, in_ports: Vec>, - // out_port_buffers: Vec<&mut [f32]>, - // in_port_buffers: Vec<&[f32]>, + sample_rate: SampleRate, input_data_callback: Option>, output_data_callback: Option>, @@ -261,7 +260,7 @@ impl LocalProcessHandler { output_data_callback, temp_input_buffer, temp_output_buffer, - temp_output_buffer_size: buffer_size, + temp_output_buffer_size_in_frames: buffer_size, temp_output_buffer_index: 0, playing, creation_timestamp: std::time::Instant::now(), @@ -344,18 +343,14 @@ impl jack::ProcessHandler for LocalProcessHandler { } if let Some(output_callback) = &mut self.output_data_callback { - // Get the mutable slices for each output port buffer - // for i in 0..self.out_ports.len() { - // self.out_port_buffers[i] = self.out_ports[i].as_mut_slice(process_scope); - // } - let num_out_channels = self.out_ports.len(); // Run the output callback on the temporary output buffer until we have filled the output ports // JACK ports each provide a mutable slice to be filled with samples whereas CPAL uses interleaved // channels. The formats therefore have to be bridged. for i in 0..current_frame_count { - if self.temp_output_buffer_index == self.temp_output_buffer_size { + // Check if we have gotten all of the frames from the temp_output_buffer + if self.temp_output_buffer_frames_index == self.temp_output_buffer_size_in_frames { // Get new samples if the temporary buffer is depleted. This can theoretically happen // several times per cycle or once every few cycles if the buffer size changes, but in practice // it should generally happen once per cycle if the buffer size is not changed. @@ -377,7 +372,7 @@ impl jack::ProcessHandler for LocalProcessHandler { let timestamp = crate::OutputStreamTimestamp { callback, playback }; let info = crate::OutputCallbackInfo { timestamp }; output_callback(&mut data, &info); - self.temp_output_buffer_index = 0; + self.temp_output_buffer_frames_index = 0; } // Write the interleaved samples e.g. [l0, r0, l1, r1, ..] to each output buffer for ch_ix in 0..num_out_channels { @@ -385,10 +380,10 @@ impl jack::ProcessHandler for LocalProcessHandler { // to avoid lifetime issues and allocation let output_channel = &mut self.out_ports[ch_ix].as_mut_slice(process_scope); output_channel[i] = self.temp_output_buffer - [ch_ix + self.temp_output_buffer_index * num_out_channels]; + [ch_ix + self.temp_output_buffer_frames_index * num_out_channels]; } - // Increase the index into the temporary buffer - self.temp_output_buffer_index += 1; + // Count the number of frames that have been read from the temp buffer + self.temp_output_buffer_frames_index += 1; } } From 308845997a272a2374d8e12a87b5e7b29b4f2e3a Mon Sep 17 00:00:00 2001 From: Erik Natanael Gustafsson Date: Thu, 1 Oct 2020 10:45:56 +0200 Subject: [PATCH 27/27] Renaming and comments for added clarity, removes some commented out code --- src/host/jack/stream.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index 02d13c16c..6086302a5 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -223,8 +223,8 @@ struct LocalProcessHandler { // JACK audio samples are 32 bit float (unless you do some custom dark magic) temp_output_buffer: Vec, /// The number of frames in the temp_output_buffer - temp_output_buffer_size: usize, - temp_output_buffer_index: usize, + temp_output_buffer_size_in_frames: usize, + temp_output_buffer_frames_index: usize, playing: Arc, creation_timestamp: std::time::Instant, } @@ -261,7 +261,7 @@ impl LocalProcessHandler { temp_input_buffer, temp_output_buffer, temp_output_buffer_size_in_frames: buffer_size, - temp_output_buffer_index: 0, + temp_output_buffer_frames_index: 0, playing, creation_timestamp: std::time::Instant::now(), }