Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JACK Host #389

Merged
merged 30 commits into from
Oct 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
9560ebf
Adds jack dependency
ErikNatanael Apr 10, 2020
b3d6a20
First almost compiling commit of JACK implementation
ErikNatanael Apr 13, 2020
d6531c9
First JACK Host version that compiles
ErikNatanael Apr 14, 2020
e80087d
Add JACK as a Host if the feature is enabled
ErikNatanael Apr 14, 2020
1cf6fff
Zero temp_output_buffer before use
ErikNatanael Apr 14, 2020
20464a3
Automatically connect to system ports when creating a stream
ErikNatanael Apr 14, 2020
fab47f2
Formatting
ErikNatanael Apr 14, 2020
b29926f
Input streams now work and connect correctly to the system capture ports
ErikNatanael Apr 14, 2020
634d1e1
Fix input data buffer size
ErikNatanael Apr 14, 2020
820d153
Adds device enumeration (Devices)
ErikNatanael Apr 16, 2020
bebea2d
Clarify motivation for the Host::is_available() function always retur…
ErikNatanael Jul 5, 2020
d42dc3e
Clarify motivation for the Host::is_available() function always retur…
ErikNatanael Jul 5, 2020
e77125e
Removes .expect() on Stream creation, instead sending the error throu…
ErikNatanael Jul 5, 2020
60f3697
Adds a notification handler for error reporting
ErikNatanael Jul 5, 2020
3f13df0
Adds a notification handler for error reporting
ErikNatanael Jul 5, 2020
64b9b4e
Remove confusing success message
ErikNatanael Jul 5, 2020
43b0b6c
Bump jack dependency to 0.6.5
ErikNatanael Sep 15, 2020
891d6eb
Remove ArcMutex wrapper around callbacks
ErikNatanael Sep 15, 2020
290d10c
Merge dependencies
ErikNatanael Sep 15, 2020
67d635b
Bring up to date with master branch, implement timestamps, run rustfmt
ErikNatanael Sep 15, 2020
18bd609
Add jack examples to make it easy to test
ErikNatanael Sep 15, 2020
a5eb8be
Add jack examples to make it easy to test
ErikNatanael Sep 15, 2020
e580954
Conditionally compile jack examples to avoid CI build failures
ErikNatanael Sep 15, 2020
ac7f9d9
Fix conditional compilation to not try to use jack on Win or Mac
ErikNatanael Sep 15, 2020
2ce3961
Merge jack examples into ordinary examples
ErikNatanael Sep 17, 2020
4369333
Add libjack installation to CI
ErikNatanael Sep 17, 2020
6973454
Fixed warnings
Psykopear Oct 1, 2020
4314e58
Merge pull request #1 from Psykopear/jack-host
ErikNatanael Oct 1, 2020
b8456ed
Renaming and comments for added clarity, removes some commented out code
ErikNatanael Oct 1, 2020
3088459
Renaming and comments for added clarity, removes some commented out code
ErikNatanael Oct 1, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/cpal.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ keywords = ["audio", "sound"]
[features]
asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions.


[dependencies]
thiserror = "1.0.2"

Expand All @@ -30,6 +31,7 @@ lazy_static = "1.3"
alsa = "0.4.1"
nix = "0.15.0"
libc = "0.2.65"
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"] }
Expand Down
26 changes: 26 additions & 0 deletions examples/beep.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,33 @@ extern crate cpal;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};

fn main() -> Result<(), anyhow::Error> {
// Conditionally compile with jack if the feature is specified.
#[cfg(all(
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"),
feature = "jack"
))]
// 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::<String>()
.contains(&String::from("--jack"))
{
cpal::host_from_id(cpal::available_hosts()
.into_iter()
.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");
Expand Down
25 changes: 25 additions & 0 deletions examples/feedback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ use ringbuf::RingBuffer;
const LATENCY_MS: f32 = 150.0;

fn main() -> Result<(), anyhow::Error> {
// Conditionally compile with jack if the feature is specified.
#[cfg(all(
any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"),
feature = "jack"
))]
// 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::<String>()
.contains(&String::from("--jack"))
{
cpal::host_from_id(cpal::available_hosts()
.into_iter()
.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();

// Default devices.
Expand Down
275 changes: 275 additions & 0 deletions src/host/jack/device.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
use crate::{
BackendSpecificError, BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError,
InputCallbackInfo, OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError,
SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange,
SupportedStreamConfigsError,
};
use std::hash::{Hash, Hasher};
use traits::DeviceTrait;

use super::stream::Stream;
use super::JACK_SAMPLE_FORMAT;

pub type SupportedInputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;
pub type SupportedOutputConfigs = std::vec::IntoIter<SupportedStreamConfigRange>;

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,
buffer_size: SupportedBufferSize,
device_type: DeviceType,
start_server_automatically: bool,
connect_ports_automatically: bool,
}

impl Device {
fn new_device(
name: String,
connect_ports_automatically: bool,
start_server_automatically: bool,
device_type: DeviceType,
) -> Result<Self, String> {
// 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 {
// 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),
buffer_size: SupportedBufferSize::Range {
min: client.buffer_size(),
max: client.buffer_size(),
},
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<Self, String> {
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<Self, String> {
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<SupportedStreamConfig, DefaultStreamConfigError> {
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.
let sample_format = JACK_SAMPLE_FORMAT;
Ok(SupportedStreamConfig {
channels,
sample_rate,
buffer_size,
sample_format,
})
}

pub fn supported_configs(&self) -> Vec<SupportedStreamConfigRange> {
let f = match self.default_config() {
Err(_) => return vec![],
Ok(f) => f,
};

let mut supported_configs = vec![];

for &channels in DEFAULT_SUPPORTED_CHANNELS.iter() {
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
}

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 {
type SupportedInputConfigs = SupportedInputConfigs;
type SupportedOutputConfigs = SupportedOutputConfigs;
type Stream = Stream;

fn name(&self) -> Result<String, DeviceNameError> {
Ok(self.name.clone())
}

fn supported_input_configs(
&self,
) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
Ok(self.supported_configs().into_iter())
}

fn supported_output_configs(
&self,
) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
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<SupportedStreamConfig, DefaultStreamConfigError> {
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<SupportedStreamConfig, DefaultStreamConfigError> {
self.default_config()
}

fn build_input_stream_raw<D, E>(
&self,
conf: &StreamConfig,
sample_format: SampleFormat,
data_callback: D,
error_callback: E,
) -> Result<Self::Stream, BuildStreamError>
where
D: FnMut(&Data, &InputCallbackInfo) + 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 mut stream = Stream::new_input(client, conf.channels, data_callback, error_callback);

if self.connect_ports_automatically {
stream.connect_to_system_inputs();
}

Ok(stream)
}

fn build_output_stream_raw<D, E>(
&self,
conf: &StreamConfig,
sample_format: SampleFormat,
data_callback: D,
error_callback: E,
) -> Result<Self::Stream, BuildStreamError>
where
D: FnMut(&mut Data, &OutputCallbackInfo) + 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 mut stream = Stream::new_output(client, conf.channels, data_callback, error_callback);

if self.connect_ports_automatically {
stream.connect_to_system_outputs();
}

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<H: Hasher>(&self, state: &mut H) {
self.name().unwrap().hash(state);
}
}
Loading