diff --git a/.gitignore b/.gitignore index b14cebf..6d067b3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ /Cargo.lock .cargo/ .DS_Store -recorded.wav +recorded.wav \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index b6d42c4..37c72c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,15 +8,20 @@ documentation = "https://docs.rs/cpal" license = "Apache-2.0" keywords = ["audio", "sound"] +[features] +asio = ["asio-sys"] # Only available on Windows. See README for setup instructions. + [dependencies] failure = "0.1.5" lazy_static = "1.3" +num-traits = "0.2.6" [dev-dependencies] hound = "3.4" [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3", features = ["audiosessiontypes", "audioclient", "coml2api", "combaseapi", "debug", "devpkey", "handleapi", "ksmedia", "mmdeviceapi", "objbase", "std", "synchapi", "winuser"] } +asio-sys = { version = "0.1", path = "asio-sys", optional = true } [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd"))'.dependencies] alsa-sys = { version = "0.1", path = "alsa-sys" } diff --git a/README.md b/README.md index 705ad39..d68b357 100644 --- a/README.md +++ b/README.md @@ -13,10 +13,10 @@ This library currently supports the following: - Get the current default input and output stream formats for a device. - Build and run input and output PCM streams on a chosen device with a given stream format. -Currently supported backends include: +Currently supported hosts include: - Linux (via ALSA) -- Windows +- Windows (via WASAPI by default, see ASIO instructions below) - macOS (via CoreAudio) - iOS (via CoreAudio) - Emscripten @@ -24,3 +24,79 @@ Currently supported backends include: Note that on Linux, the ALSA development files are required. These are provided as part of the `libasound2-dev` package on Debian and Ubuntu distributions and `alsa-lib-devel` on Fedora. + +## ASIO on Windows + +[ASIO](https://en.wikipedia.org/wiki/Audio_Stream_Input/Output) is an audio +driver protocol by Steinberg. While it is available on multiple operating +systems, it is most commonly used on Windows to work around limitations of +WASAPI including access to large numbers of channels and lower-latency audio +processing. + +CPAL allows for using the ASIO SDK as the audio host on Windows instead of +WASAPI. To do so, follow these steps: + +1. **Download the ASIO SDK** `.zip` from [this + link](https://www.steinberg.net/en/company/developers.html). The version as + of writing this is 2.3.1. +2. Extract the files and place the directory somewhere you are happy for it to stay + (e.g. `~/.asio`). +3. Assign the full path of the directory (that contains the `readme`, `changes`, + `ASIO SDK 2.3` pdf, etc) to the `CPAL_ASIO_DIR` environment variable. This is + necessary for the `asio-sys` build script to build and bind to the SDK. +4. `bindgen`, the library used to generate bindings to the C++ SDK, requires + clang. **Download and install LLVM** from + [here](http://releases.llvm.org/download.html) under the "Pre-Built Binaries" + section. The version as of writing this is 7.0.0. +5. Add the LLVM `bin` directory to a `LIBCLANG_PATH` environment variable. If + you installed LLVM to the default directory, this should work in the command + prompt: + ``` + setx LIBCLANG_PATH "C:\Program Files\LLVM\bin" + ``` +6. If you don't have any ASIO devices or drivers available, you can [**download + and install ASIO4ALL**](http://www.asio4all.org/). Be sure to enable the + "offline" feature during installation despite what the installer says about + it being useless. +7. **Loading VCVARS**. `rust-bindgen` uses the C++ tool-chain when generating + bindings to the ASIO SDK. As a result, it is necessary to load some + environment variables in the command prompt that we use to build our project. + On 64-bit machines run: + ``` + "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64 + ``` + On 32-bit machines run: + ``` + "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 + ``` + Note that, depending on your version of Visual Studio, this script might be + in a slightly different location. +8. Select the ASIO host at the start of our program with the following code: + + ```rust + let host; + #[cfg(target_os = "windows")] + { + host = cpal::host_from_id(cpal::HostId::Asio).expect("failed to initialise ASIO host"); + } + ``` + + If you run into compilations errors produced by `asio-sys` or `bindgen`, make + sure that `CPAL_ASIO_DIR` is set correctly and try `cargo clean`. +9. Make sure to enable the `asio` feature when building CPAL: + + ``` + cargo build --features "asio" + ``` + + or if you are using CPAL as a dependency in a downstream project, enable the + feature like this: + + ```toml + cpal = { version = "*", features = ["asio"] } + ``` + +In the future we would like to work on automating this process to make it +easier, but we are not familiar enough with the ASIO license to do so yet. + +*Updated as of ASIO version 2.3.3.* diff --git a/asio-sys/.gitignore b/asio-sys/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/asio-sys/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/asio-sys/Cargo.toml b/asio-sys/Cargo.toml new file mode 100644 index 0000000..b1208d5 --- /dev/null +++ b/asio-sys/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "asio-sys" +version = "0.1.0" +authors = ["Tom Gowan "] +build = "build.rs" + +[target.'cfg(any(target_os = "windows"))'.build-dependencies] +bindgen = "0.42.0" +walkdir = "2" +cc = "1.0.25" + +[dependencies] +lazy_static = "1.0.0" +num-derive = "0.2" +num-traits = "0.2" diff --git a/asio-sys/asio-link/helpers.cpp b/asio-sys/asio-link/helpers.cpp new file mode 100644 index 0000000..79ec8bb --- /dev/null +++ b/asio-sys/asio-link/helpers.cpp @@ -0,0 +1,29 @@ +#include "helpers.hpp" +#include + +extern "C" ASIOError get_sample_rate(double * rate){ + return ASIOGetSampleRate(reinterpret_cast(rate)); +} + +extern "C" ASIOError set_sample_rate(double rate){ + return ASIOSetSampleRate(rate); +} + +extern "C" ASIOError can_sample_rate(double rate){ + return ASIOCanSampleRate(rate); +} + +extern AsioDrivers* asioDrivers; +bool loadAsioDriver(char *name); + +extern "C" bool load_asio_driver(char * name){ + return loadAsioDriver(name); +} + +extern "C" void remove_current_driver() { + asioDrivers->removeCurrentDriver(); +} +extern "C" long get_driver_names(char **names, long maxDrivers) { + AsioDrivers ad; + return ad.getDriverNames(names, maxDrivers); +} \ No newline at end of file diff --git a/asio-sys/asio-link/helpers.hpp b/asio-sys/asio-link/helpers.hpp new file mode 100644 index 0000000..d63f968 --- /dev/null +++ b/asio-sys/asio-link/helpers.hpp @@ -0,0 +1,16 @@ +#pragma once +#include "asiodrivers.h" +#include "asio.h" + +// Helper function to wrap confusing preprocessor +extern "C" ASIOError get_sample_rate(double * rate); + +// Helper function to wrap confusing preprocessor +extern "C" ASIOError set_sample_rate(double rate); + +// Helper function to wrap confusing preprocessor +extern "C" ASIOError can_sample_rate(double rate); + +extern "C" bool load_asio_driver(char * name); +extern "C" void remove_current_driver(); +extern "C" long get_driver_names(char **names, long maxDrivers); \ No newline at end of file diff --git a/asio-sys/build.rs b/asio-sys/build.rs new file mode 100644 index 0000000..1ef7e1f --- /dev/null +++ b/asio-sys/build.rs @@ -0,0 +1,216 @@ +extern crate bindgen; +extern crate cc; +extern crate walkdir; + +use std::env; +use std::path::PathBuf; +use walkdir::WalkDir; + +const CPAL_ASIO_DIR: &'static str = "CPAL_ASIO_DIR"; + +const ASIO_HEADER: &'static str = "asio.h"; +const ASIO_SYS_HEADER: &'static str = "asiosys.h"; +const ASIO_DRIVERS_HEADER: &'static str = "asiodrivers.h"; + +fn main() { + // If ASIO directory isn't set silently return early + let cpal_asio_dir_var = match env::var(CPAL_ASIO_DIR) { + Err(_) => return, + Ok(var) => var, + }; + + // Asio directory + let cpal_asio_dir = PathBuf::from(cpal_asio_dir_var); + + // Directory where bindings and library are created + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("bad path")); + + // Check if library exists + // if it doesn't create it + let mut lib_path = out_dir.clone(); + lib_path.push("libasio.a"); + if !lib_path.exists() { + create_lib(&cpal_asio_dir); + } + + // Print out links to needed libraries + println!("cargo:rustc-link-lib=dylib=ole32"); + println!("cargo:rustc-link-lib=dylib=User32"); + println!("cargo:rustc-link-search={}", out_dir.display()); + println!("cargo:rustc-link-lib=static=asio"); + println!("cargo:rustc-cfg=asio"); + + // Check if bindings exist + // if they dont create them + let mut binding_path = out_dir.clone(); + binding_path.push("asio_bindings.rs"); + if !binding_path.exists() { + create_bindings(&cpal_asio_dir); + } +} + +fn create_lib(cpal_asio_dir: &PathBuf) { + let mut cpp_paths: Vec = Vec::new(); + let mut host_dir = cpal_asio_dir.clone(); + let mut pc_dir = cpal_asio_dir.clone(); + let mut common_dir = cpal_asio_dir.clone(); + host_dir.push("host"); + common_dir.push("common"); + pc_dir.push("host/pc"); + + // Gathers cpp files from directories + let walk_a_dir = |dir_to_walk, paths: &mut Vec| { + for entry in WalkDir::new(&dir_to_walk).max_depth(1) { + let entry = match entry { + Err(e) => { + println!("error: {}", e); + continue + }, + Ok(entry) => entry, + }; + match entry.path().extension().and_then(|s| s.to_str()) { + None => continue, + Some("cpp") => { + // Skip macos bindings + if entry.path().file_name().unwrap().to_str() == Some("asiodrvr.cpp") { + continue; + } + paths.push(entry.path().to_path_buf()) + } + Some(_) => continue, + }; + } + }; + + // Get all cpp files for building SDK library + walk_a_dir(host_dir, &mut cpp_paths); + walk_a_dir(pc_dir, &mut cpp_paths); + walk_a_dir(common_dir, &mut cpp_paths); + + // build the asio lib + cc::Build::new() + .include(format!("{}/{}", cpal_asio_dir.display(), "host")) + .include(format!("{}/{}", cpal_asio_dir.display(), "common")) + .include(format!("{}/{}", cpal_asio_dir.display(), "host/pc")) + .include("asio-link/helpers.hpp") + .file("asio-link/helpers.cpp") + .files(cpp_paths) + .cpp(true) + .compile("libasio.a"); +} + +fn create_bindings(cpal_asio_dir: &PathBuf) { + let mut asio_header = None; + let mut asio_sys_header = None; + let mut asio_drivers_header = None; + + // Recursively walk given cpal dir to find required headers + for entry in WalkDir::new(&cpal_asio_dir) { + let entry = match entry { + Err(_) => continue, + Ok(entry) => entry, + }; + let file_name = match entry.path().file_name().and_then(|s| s.to_str()) { + None => continue, + Some(file_name) => file_name, + }; + + match file_name { + ASIO_HEADER => asio_header = Some(entry.path().to_path_buf()), + ASIO_SYS_HEADER => asio_sys_header = Some(entry.path().to_path_buf()), + ASIO_DRIVERS_HEADER => asio_drivers_header = Some(entry.path().to_path_buf()), + _ => (), + } + } + + macro_rules! header_or_panic { + ($opt_header:expr, $FILE_NAME:expr) => { + match $opt_header.as_ref() { + None => { + panic!("Could not find {} in {}: {}", $FILE_NAME, CPAL_ASIO_DIR, cpal_asio_dir.display()); + }, + Some(path) => path.to_str().expect("Could not convert path to str"), + } + }; + } + + // Only continue if found all headers that we need + let asio_header = header_or_panic!(asio_header, ASIO_HEADER); + let asio_sys_header = header_or_panic!(asio_sys_header, ASIO_SYS_HEADER); + let asio_drivers_header = header_or_panic!(asio_drivers_header, ASIO_DRIVERS_HEADER); + + // The bindgen::Builder is the main entry point + // to bindgen, and lets you build up options for + // the resulting bindings. + let bindings = bindgen::Builder::default() + // The input header we would like to generate + // bindings for. + .header(asio_header) + .header(asio_sys_header) + .header(asio_drivers_header) + .header("asio-link/helpers.hpp") + .clang_arg("-x") + .clang_arg("c++") + .clang_arg("-std=c++14") + .clang_arg( format!("-I{}/{}", cpal_asio_dir.display(), "host/pc") ) + .clang_arg( format!("-I{}/{}", cpal_asio_dir.display(), "host") ) + .clang_arg( format!("-I{}/{}", cpal_asio_dir.display(), "common") ) + // Need to whitelist to avoid binding tp c++ std::* + .whitelist_type("AsioDrivers") + .whitelist_type("AsioDriver") + .whitelist_type("ASIOTime") + .whitelist_type("ASIOTimeInfo") + .whitelist_type("ASIODriverInfo") + .whitelist_type("ASIOBufferInfo") + .whitelist_type("ASIOCallbacks") + .whitelist_type("ASIOSamples") + .whitelist_type("ASIOSampleType") + .whitelist_type("ASIOSampleRate") + .whitelist_type("ASIOChannelInfo") + .whitelist_type("AsioTimeInfoFlags") + .whitelist_type("ASIOTimeCodeFlags") + .whitelist_var("kAsioSelectorSupported") + .whitelist_var("kAsioEngineVersion") + .whitelist_var("kAsioResetRequest") + .whitelist_var("kAsioBufferSizeChange") + .whitelist_var("kAsioResyncRequest") + .whitelist_var("kAsioLatenciesChanged") + .whitelist_var("kAsioSupportsTimeInfo") + .whitelist_var("kAsioSupportsTimeCode") + .whitelist_var("kAsioMMCCommand") + .whitelist_var("kAsioSupportsInputMonitor") + .whitelist_var("kAsioSupportsInputGain") + .whitelist_var("kAsioSupportsInputMeter") + .whitelist_var("kAsioSupportsOutputGain") + .whitelist_var("kAsioSupportsOutputMeter") + .whitelist_var("kAsioOverload") + .whitelist_function("ASIOGetChannels") + .whitelist_function("ASIOGetChannelInfo") + .whitelist_function("ASIOGetBufferSize") + .whitelist_function("ASIOGetSamplePosition") + .whitelist_function("get_sample_rate") + .whitelist_function("set_sample_rate") + .whitelist_function("can_sample_rate") + .whitelist_function("ASIOInit") + .whitelist_function("ASIOCreateBuffers") + .whitelist_function("ASIOStart") + .whitelist_function("ASIOStop") + .whitelist_function("ASIODisposeBuffers") + .whitelist_function("ASIOExit") + .whitelist_function("load_asio_driver") + .whitelist_function("remove_current_driver") + .whitelist_function("get_driver_names") + .bitfield_enum("AsioTimeInfoFlags") + .bitfield_enum("ASIOTimeCodeFlags") + // Finish the builder and generate the bindings. + .generate() + // Unwrap the Result and panic on failure. + .expect("Unable to generate bindings"); + + // Write the bindings to the $OUT_DIR/bindings.rs file. + let out_path = PathBuf::from(env::var("OUT_DIR").expect("bad path")); + //panic!("path: {}", out_path.display()); + bindings + .write_to_file(out_path.join("asio_bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/asio-sys/examples/enumerate.rs b/asio-sys/examples/enumerate.rs new file mode 100644 index 0000000..edc2623 --- /dev/null +++ b/asio-sys/examples/enumerate.rs @@ -0,0 +1,56 @@ +/* This example aims to produce the same behaviour + * as the enumerate example in cpal + * by Tom Gowan + */ + +extern crate asio_sys as sys; + +// This is the same data that enumerate +// is trying to find +// Basically these are stubbed versions +// +// Format that each sample has. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SampleFormat { + // The value 0 corresponds to 0. + I16, + // The value 0 corresponds to 32768. + U16, + // The boundaries are (-1.0, 1.0). + F32, +} +// Number of channels. +pub type ChannelCount = u16; + +// The number of samples processed per second for a single channel of audio. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct SampleRate(pub u32); + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Format { + pub channels: ChannelCount, + pub sample_rate: SampleRate, + pub data_type: SampleFormat, +} + +fn main() { + let asio = sys::Asio::new(); + for name in asio.driver_names() { + println!("Driver: {:?}", name); + let driver = asio.load_driver(&name).expect("failed to load driver"); + let channels = driver.channels().expect("failed to retrieve channel counts"); + let sample_rate = driver.sample_rate().expect("failed to retrieve sample rate"); + let in_fmt = Format { + channels: channels.ins as _, + sample_rate: SampleRate(sample_rate as _), + data_type: SampleFormat::F32, + }; + let out_fmt = Format { + channels: channels.outs as _, + sample_rate: SampleRate(sample_rate as _), + data_type: SampleFormat::F32, + }; + println!(" Input {:?}", in_fmt); + println!(" Output {:?}", out_fmt); + } +} diff --git a/asio-sys/examples/test.rs b/asio-sys/examples/test.rs new file mode 100644 index 0000000..3aa5869 --- /dev/null +++ b/asio-sys/examples/test.rs @@ -0,0 +1,11 @@ +extern crate asio_sys as sys; + +fn main() { + let asio = sys::Asio::new(); + for driver in asio.driver_names() { + println!("Driver: {}", driver); + let driver = asio.load_driver(&driver).expect("failed to load drivers"); + println!(" Channels: {:?}", driver.channels().expect("failed to get channels")); + println!(" Sample rate: {:?}", driver.sample_rate().expect("failed to get sample rate")); + } +} diff --git a/asio-sys/src/bindings/asio_import.rs b/asio-sys/src/bindings/asio_import.rs new file mode 100644 index 0000000..dd6b441 --- /dev/null +++ b/asio-sys/src/bindings/asio_import.rs @@ -0,0 +1,6 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] + +include!(concat!(env!("OUT_DIR"), "/asio_bindings.rs")); diff --git a/asio-sys/src/bindings/errors.rs b/asio-sys/src/bindings/errors.rs new file mode 100644 index 0000000..4c770e6 --- /dev/null +++ b/asio-sys/src/bindings/errors.rs @@ -0,0 +1,124 @@ +use std::error::Error; +use std::fmt; + +/// Errors that might occur during `Asio::load_driver`. +#[derive(Debug)] +pub enum LoadDriverError { + LoadDriverFailed, + DriverAlreadyExists, + InitializationFailed(AsioError), +} + +/// General errors returned by ASIO. +#[derive(Debug)] +pub enum AsioError { + NoDrivers, + HardwareMalfunction, + InvalidInput, + BadMode, + HardwareStuck, + NoRate, + ASE_NoMemory, + UnknownError, +} + +#[derive(Debug)] +pub enum AsioErrorWrapper { + ASE_OK = 0, // This value will be returned whenever the call succeeded + ASE_SUCCESS = 0x3f4847a0, // unique success return value for ASIOFuture calls + ASE_NotPresent = -1000, // hardware input or output is not present or available + ASE_HWMalfunction, // hardware is malfunctioning (can be returned by any ASIO function) + ASE_InvalidParameter, // input parameter invalid + ASE_InvalidMode, // hardware is in a bad mode or used in a bad mode + ASE_SPNotAdvancing, // hardware is not running when sample position is inquired + ASE_NoClock, // sample clock or rate cannot be determined or is not present + ASE_NoMemory, // not enough memory for completing the request + Invalid, +} + +impl fmt::Display for LoadDriverError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.description()) + } +} + +impl fmt::Display for AsioError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + AsioError::NoDrivers => { + write!(f, "hardware input or output is not present or available") + } + AsioError::HardwareMalfunction => write!( + f, + "hardware is malfunctioning (can be returned by any ASIO function)" + ), + AsioError::InvalidInput => write!(f, "input parameter invalid"), + AsioError::BadMode => write!(f, "hardware is in a bad mode or used in a bad mode"), + AsioError::HardwareStuck => write!( + f, + "hardware is not running when sample position is inquired" + ), + AsioError::NoRate => write!( + f, + "sample clock or rate cannot be determined or is not present" + ), + AsioError::ASE_NoMemory => write!(f, "not enough memory for completing the request"), + AsioError::UnknownError => write!(f, "Error not in SDK"), + } + } +} + +impl Error for LoadDriverError { + fn description(&self) -> &str { + match *self { + LoadDriverError::LoadDriverFailed => { + "ASIO `loadDriver` function returned `false` indicating failure" + } + LoadDriverError::InitializationFailed(ref err) => err.description(), + LoadDriverError::DriverAlreadyExists => { + "ASIO only supports loading one driver at a time" + } + } + } +} + +impl Error for AsioError { + fn description(&self) -> &str { + match *self { + AsioError::NoDrivers => "hardware input or output is not present or available", + AsioError::HardwareMalfunction => { + "hardware is malfunctioning (can be returned by any ASIO function)" + } + AsioError::InvalidInput => "input parameter invalid", + AsioError::BadMode => "hardware is in a bad mode or used in a bad mode", + AsioError::HardwareStuck => "hardware is not running when sample position is inquired", + AsioError::NoRate => "sample clock or rate cannot be determined or is not present", + AsioError::ASE_NoMemory => "not enough memory for completing the request", + AsioError::UnknownError => "Error not in SDK", + } + } +} + +impl From for LoadDriverError { + fn from(err: AsioError) -> Self { + LoadDriverError::InitializationFailed(err) + } +} + +macro_rules! asio_result { + ($e:expr) => {{ + let res = { $e }; + match res { + r if r == AsioErrorWrapper::ASE_OK as i32 => Ok(()), + r if r == AsioErrorWrapper::ASE_SUCCESS as i32 => Ok(()), + r if r == AsioErrorWrapper::ASE_NotPresent as i32 => Err(AsioError::NoDrivers), + r if r == AsioErrorWrapper::ASE_HWMalfunction as i32 => Err(AsioError::HardwareMalfunction), + r if r == AsioErrorWrapper::ASE_InvalidParameter as i32 => Err(AsioError::InvalidInput), + r if r == AsioErrorWrapper::ASE_InvalidMode as i32 => Err(AsioError::BadMode), + r if r == AsioErrorWrapper::ASE_SPNotAdvancing as i32 => Err(AsioError::HardwareStuck), + r if r == AsioErrorWrapper::ASE_NoClock as i32 => Err(AsioError::NoRate), + r if r == AsioErrorWrapper::ASE_NoMemory as i32 => Err(AsioError::ASE_NoMemory), + _ => Err(AsioError::UnknownError), + } + }}; +} diff --git a/asio-sys/src/bindings/mod.rs b/asio-sys/src/bindings/mod.rs new file mode 100644 index 0000000..6896b60 --- /dev/null +++ b/asio-sys/src/bindings/mod.rs @@ -0,0 +1,905 @@ +pub mod asio_import; +#[macro_use] +pub mod errors; + +use num_traits::FromPrimitive; +use self::errors::{AsioError, AsioErrorWrapper, LoadDriverError}; +use std::ffi::CStr; +use std::ffi::CString; +use std::os::raw::{c_char, c_double, c_long, c_void}; +use std::sync::{Arc, Mutex, Weak}; + +// Bindings import +use self::asio_import as ai; + +/// A handle to the ASIO API. +/// +/// There should only be one instance of this type at any point in time. +#[derive(Debug)] +pub struct Asio { + // Keeps track of whether or not a driver is already loaded. + // + // This is necessary as ASIO only supports one `Driver` at a time. + loaded_driver: Mutex>, +} + +/// A handle to a single ASIO driver. +/// +/// Creating an instance of this type loads and initialises the driver. +/// +/// Dropping all `Driver` instances will automatically dispose of any resources and de-initialise +/// the driver. +#[derive(Clone, Debug)] +pub struct Driver { + inner: Arc, +} + +// Contains the state associated with a `Driver`. +// +// This state may be shared between multiple `Driver` handles representing the same underlying +// driver. Only when the last `Driver` is dropped will the `Drop` implementation for this type run +// and the necessary driver resources will be de-allocated and unloaded. +// +// The same could be achieved by returning an `Arc` from the `Host::load_driver` API, +// however the `DriverInner` abstraction is required in order to allow for the `Driver::destroy` +// method to exist safely. By wrapping the `Arc` in the `Driver` type, we can make +// sure the user doesn't `try_unwrap` the `Arc` and invalidate the `Asio` instance's weak pointer. +// This would allow for instantiation of a separate driver before the existing one is destroyed, +// which is disallowed by ASIO. +#[derive(Debug)] +struct DriverInner { + state: Mutex, + // The unique name associated with this driver. + name: String, + // Track whether or not the driver has been destroyed. + // + // This allows for the user to manually destroy the driver and handle any errors if they wish. + // + // In the case that the driver has been manually destroyed this flag will be set to `true` + // indicating to the `drop` implementation that there is nothing to be done. + destroyed: bool, +} + +/// All possible states of an ASIO `Driver` instance. +/// +/// Mapped to the finite state machine in the ASIO SDK docs. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum DriverState { + Initialized, + Prepared, + Running, +} + +/// Amount of input and output +/// channels available. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct Channels { + pub ins: c_long, + pub outs: c_long, +} + +/// Sample rate of the ASIO driver. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct SampleRate { + pub rate: u32, +} + +/// Holds the pointer to the callbacks that come from cpal +struct BufferCallback(Box); + +/// Input and Output streams. +/// +/// There is only ever max one input and one output. +/// +/// Only one is required. +pub struct AsioStreams { + pub input: Option, + pub output: Option, +} + +/// A stream to ASIO. +/// +/// Contains the buffers. +pub struct AsioStream { + /// A Double buffer per channel + pub buffer_infos: Vec, + /// Size of each buffer + pub buffer_size: i32, +} + +/// All the possible types from ASIO. +/// This is a direct copy of the ASIOSampleType +/// inside ASIO SDK. +#[derive(Debug, FromPrimitive)] +#[repr(C)] +pub enum AsioSampleType { + ASIOSTInt16MSB = 0, + ASIOSTInt24MSB = 1, // used for 20 bits as well + ASIOSTInt32MSB = 2, + ASIOSTFloat32MSB = 3, // IEEE 754 32 bit float + ASIOSTFloat64MSB = 4, // IEEE 754 64 bit double float + + // these are used for 32 bit data buffer, with different alignment of the data inside + // 32 bit PCI bus systems can be more easily used with these + ASIOSTInt32MSB16 = 8, // 32 bit data with 16 bit alignment + ASIOSTInt32MSB18 = 9, // 32 bit data with 18 bit alignment + ASIOSTInt32MSB20 = 10, // 32 bit data with 20 bit alignment + ASIOSTInt32MSB24 = 11, // 32 bit data with 24 bit alignment + + ASIOSTInt16LSB = 16, + ASIOSTInt24LSB = 17, // used for 20 bits as well + ASIOSTInt32LSB = 18, + ASIOSTFloat32LSB = 19, // IEEE 754 32 bit float, as found on Intel x86 architecture + ASIOSTFloat64LSB = 20, // IEEE 754 64 bit double float, as found on Intel x86 architecture + + // these are used for 32 bit data buffer, with different alignment of the data inside + // 32 bit PCI bus systems can more easily used with these + ASIOSTInt32LSB16 = 24, // 32 bit data with 18 bit alignment + ASIOSTInt32LSB18 = 25, // 32 bit data with 18 bit alignment + ASIOSTInt32LSB20 = 26, // 32 bit data with 20 bit alignment + ASIOSTInt32LSB24 = 27, // 32 bit data with 24 bit alignment + + // ASIO DSD format. + ASIOSTDSDInt8LSB1 = 32, // DSD 1 bit data, 8 samples per byte. First sample in Least significant bit. + ASIOSTDSDInt8MSB1 = 33, // DSD 1 bit data, 8 samples per byte. First sample in Most significant bit. + ASIOSTDSDInt8NER8 = 40, // DSD 8 bit data, 1 sample per byte. No Endianness required. + + ASIOSTLastEntry, +} + +/// Gives information about buffers +/// Receives pointers to buffers +#[derive(Debug, Copy, Clone)] +#[repr(C)] +pub struct AsioBufferInfo { + /// 0 for output 1 for input + pub is_input: c_long, + /// Which channel. Starts at 0 + pub channel_num: c_long, + /// Pointer to each half of the double buffer. + pub buffers: [*mut c_void; 2], +} + +/// Callbacks that ASIO calls +#[repr(C)] +struct AsioCallbacks { + buffer_switch: extern "C" fn(double_buffer_index: c_long, direct_process: c_long) -> (), + sample_rate_did_change: extern "C" fn(s_rate: c_double) -> (), + asio_message: + extern "C" fn(selector: c_long, value: c_long, message: *mut (), opt: *mut c_double) + -> c_long, + buffer_switch_time_info: extern "C" fn( + params: *mut ai::ASIOTime, + double_buffer_index: c_long, + direct_process: c_long, + ) -> *mut ai::ASIOTime, +} + +/// A rust-usable version of the `ASIOTime` type that does not contain a binary blob for fields. +#[repr(C)] +pub struct AsioTime { + /// Must be `0`. + pub reserved: [c_long; 4], + /// Required. + pub time_info: AsioTimeInfo, + /// Optional, evaluated if (time_code.flags & ktcValid). + pub time_code: AsioTimeCode, +} + +/// A rust-compatible version of the `ASIOTimeInfo` type that does not contain a binary blob for +/// fields. +#[repr(C)] +pub struct AsioTimeInfo { + /// Absolute speed (1. = nominal). + pub speed: c_double, + /// System time related to sample_position, in nanoseconds. + /// + /// On Windows, must be derived from timeGetTime(). + pub system_time: ai::ASIOTimeStamp, + /// Sample position since `ASIOStart()`. + pub sample_position: ai::ASIOSamples, + /// Current rate, unsigned. + pub sample_rate: AsioSampleRate, + /// See `AsioTimeInfoFlags`. + pub flags: c_long, + /// Must be `0`. + pub reserved: [c_char; 12], +} + +/// A rust-compatible version of the `ASIOTimeCode` type that does not use a binary blob for its +/// fields. +#[repr(C)] +pub struct AsioTimeCode { + /// Speed relation (fraction of nominal speed) optional. + /// + /// Set to 0. or 1. if not supported. + pub speed: c_double, + /// Time in samples unsigned. + pub time_code_samples: ai::ASIOSamples, + /// See `ASIOTimeCodeFlags`. + pub flags: c_long, + /// Set to `0`. + pub future: [c_char; 64], +} + +/// A rust-compatible version of the `ASIOSampleRate` type that does not use a binary blob for its +/// fields. +pub type AsioSampleRate = f64; + +// A helper type to simplify retrieval of available buffer sizes. +#[derive(Default)] +struct BufferSizes { + min: c_long, + max: c_long, + pref: c_long, + grans: c_long, +} + +lazy_static! { + /// A global way to access all the callbacks. + /// + /// This is required because of how ASIO calls the `buffer_switch` function with no data + /// parameters. + /// + /// Options are used so that when a callback is removed we don't change the Vec indices. + /// + /// The indices are how we match a callback with a stream. + static ref BUFFER_CALLBACK: Mutex>> = Mutex::new(Vec::new()); +} + +impl Asio { + /// Initialise the ASIO API. + pub fn new() -> Self { + let loaded_driver = Mutex::new(Weak::new()); + Asio { loaded_driver } + } + + /// Returns the name for each available driver. + /// + /// This is used at the start to allow the user to choose which driver they want. + pub fn driver_names(&self) -> Vec { + // The most drivers we can take + const MAX_DRIVERS: usize = 100; + // Max length for divers name + const MAX_DRIVER_NAME_LEN: usize = 32; + + // 2D array of driver names set to 0. + let mut driver_names: [[c_char; MAX_DRIVER_NAME_LEN]; MAX_DRIVERS] = + [[0; MAX_DRIVER_NAME_LEN]; MAX_DRIVERS]; + // Pointer to each driver name. + let mut driver_name_ptrs: [*mut i8; MAX_DRIVERS] = [0 as *mut i8; MAX_DRIVERS]; + for (ptr, name) in driver_name_ptrs.iter_mut().zip(&mut driver_names[..]) { + *ptr = (*name).as_mut_ptr(); + } + + unsafe { + let num_drivers = ai::get_driver_names(driver_name_ptrs.as_mut_ptr(), MAX_DRIVERS as i32); + (0 .. num_drivers) + .map(|i| driver_name_to_utf8(&driver_names[i as usize]).to_string()) + .collect() + } + } + + /// If a driver has already been loaded, this will return that driver. + /// + /// Returns `None` if no driver is currently loaded. + /// + /// This can be useful to check before calling `load_driver` as ASIO only supports loading a + /// single driver at a time. + pub fn loaded_driver(&self) -> Option { + self.loaded_driver + .lock() + .expect("failed to acquire loaded driver lock") + .upgrade() + .map(|inner| Driver { inner }) + } + + /// Load a driver from the given name. + /// + /// Driver names compatible with this method can be produced via the `asio.driver_names()` + /// method. + /// + /// NOTE: Despite many requests from users, ASIO only supports loading a single driver at a + /// time. Calling this method while a previously loaded `Driver` instance exists will result in + /// an error. That said, if this method is called with the name of a driver that has already + /// been loaded, that driver will be returned successfully. + pub fn load_driver(&self, driver_name: &str) -> Result { + // Check whether or not a driver is already loaded. + if let Some(driver) = self.loaded_driver() { + if driver.name() == driver_name { + return Ok(driver); + } else { + return Err(LoadDriverError::DriverAlreadyExists); + } + } + + // Make owned CString to send to load driver + let driver_name_cstring = CString::new(driver_name) + .expect("failed to create `CString` from driver name"); + let mut driver_info = ai::ASIODriverInfo { + _bindgen_opaque_blob: [0u32; 43], + }; + + unsafe { + // TODO: Check that a driver of the same name does not already exist? + match ai::load_asio_driver(driver_name_cstring.as_ptr() as *mut i8) { + false => Err(LoadDriverError::LoadDriverFailed), + true => { + // Initialize ASIO. + asio_result!(ai::ASIOInit(&mut driver_info))?; + let state = Mutex::new(DriverState::Initialized); + let name = driver_name.to_string(); + let destroyed = false; + let inner = Arc::new(DriverInner { name, state, destroyed }); + *self.loaded_driver.lock().expect("failed to acquire loaded driver lock") = + Arc::downgrade(&inner); + let driver = Driver { inner }; + Ok(driver) + } + } + } + } +} + +impl BufferCallback { + /// Calls the inner callback. + fn run(&mut self, index: i32) { + let cb = &mut self.0; + cb(index); + } +} + +impl Driver { + /// The name used to uniquely identify this driver. + pub fn name(&self) -> &str { + &self.inner.name + } + + /// Returns the number of input and output channels available on the driver. + pub fn channels(&self) -> Result { + let mut ins: c_long = 0; + let mut outs: c_long = 0; + unsafe { + asio_result!(ai::ASIOGetChannels(&mut ins, &mut outs))?; + } + let channel = Channels { ins, outs }; + Ok(channel) + } + + /// Get current sample rate of the driver. + pub fn sample_rate(&self) -> Result { + let mut rate: c_double = 0.0; + unsafe { + asio_result!(ai::get_sample_rate(&mut rate))?; + } + Ok(rate) + } + + /// Can the driver accept the given sample rate. + pub fn can_sample_rate(&self, sample_rate: c_double) -> Result { + unsafe { + match asio_result!(ai::can_sample_rate(sample_rate)) { + Ok(()) => Ok(true), + Err(AsioError::NoRate) => Ok(false), + Err(err) => Err(err), + } + } + } + + /// Set the sample rate for the driver. + pub fn set_sample_rate(&self, sample_rate: c_double) -> Result<(), AsioError> { + unsafe { + asio_result!(ai::set_sample_rate(sample_rate))?; + } + Ok(()) + } + + /// Get the current data type of the driver's input stream. + /// + /// This queries a single channel's type assuming all channels have the same sample type. + pub fn input_data_type(&self) -> Result { + stream_data_type(true) + } + + /// Get the current data type of the driver's output stream. + /// + /// This queries a single channel's type assuming all channels have the same sample type. + pub fn output_data_type(&self) -> Result { + stream_data_type(false) + } + + /// Ask ASIO to allocate the buffers and give the callback pointers. + /// + /// This will destroy any already allocated buffers. + /// + /// The preferred buffer size from ASIO is used. + fn create_buffers(&self, buffer_infos: &mut [AsioBufferInfo]) -> Result { + let num_channels = buffer_infos.len(); + + // To pass as ai::ASIOCallbacks + let mut callbacks = create_asio_callbacks(); + + // Retrieve the available buffer sizes. + let buffer_sizes = asio_get_buffer_sizes()?; + if buffer_sizes.pref <= 0 { + panic!( + "`ASIOGetBufferSize` produced unusable preferred buffer size of {}", + buffer_sizes.pref, + ); + } + + // Ensure the driver is in the `Initialized` state. + if let DriverState::Running = self.inner.state() { + self.stop()?; + } + if let DriverState::Prepared = self.inner.state() { + self.dispose_buffers()?; + } + + unsafe { + asio_result!(ai::ASIOCreateBuffers( + buffer_infos.as_mut_ptr() as *mut _, + num_channels as i32, + buffer_sizes.pref, + &mut callbacks as *mut _ as *mut _, + ))?; + } + + self.inner.set_state(DriverState::Prepared); + Ok(buffer_sizes.pref) + } + + /// Creates the streams. + /// + /// Both input and output streams need to be created together as a single slice of + /// `ASIOBufferInfo`. + fn create_streams( + &self, + mut input_buffer_infos: Vec, + mut output_buffer_infos: Vec, + ) -> Result { + let (input, output) = match (input_buffer_infos.is_empty(), output_buffer_infos.is_empty()) { + // Both stream exist. + (false, false) => { + // Create one continuous slice of buffers. + let split_point = input_buffer_infos.len(); + let mut all_buffer_infos = input_buffer_infos; + all_buffer_infos.append(&mut output_buffer_infos); + // Create the buffers. On success, split the output and input again. + let buffer_size = self.create_buffers(&mut all_buffer_infos)?; + let output_buffer_infos = all_buffer_infos.split_off(split_point); + let input_buffer_infos = all_buffer_infos; + let input = Some(AsioStream { + buffer_infos: input_buffer_infos, + buffer_size, + }); + let output = Some(AsioStream { + buffer_infos: output_buffer_infos, + buffer_size, + }); + (input, output) + }, + // Just input + (false, true) => { + let buffer_size = self.create_buffers(&mut input_buffer_infos)?; + let input = Some(AsioStream { + buffer_infos: input_buffer_infos, + buffer_size, + }); + let output = None; + (input, output) + }, + // Just output + (true, false) => { + let buffer_size = self.create_buffers(&mut output_buffer_infos)?; + let input = None; + let output = Some(AsioStream { + buffer_infos: output_buffer_infos, + buffer_size, + }); + (input, output) + }, + // Impossible + (true, true) => unreachable!("Trying to create streams without preparing"), + }; + Ok(AsioStreams { input, output }) + } + + /// Prepare the input stream. + /// + /// Because only the latest call to ASIOCreateBuffers is relevant this call will destroy all + /// past active buffers and recreate them. + /// + /// For this reason we take the output stream if it exists. + /// + /// `num_channels` is the desired number of input channels. + /// + /// This returns a full AsioStreams with both input and output if output was active. + pub fn prepare_input_stream( + &self, + output: Option, + num_channels: usize, + ) -> Result { + let input_buffer_infos = prepare_buffer_infos(true, num_channels); + let output_buffer_infos = output + .map(|output| output.buffer_infos) + .unwrap_or_else(Vec::new); + self.create_streams(input_buffer_infos, output_buffer_infos) + } + + /// Prepare the output stream. + /// + /// Because only the latest call to ASIOCreateBuffers is relevant this call will destroy all + /// past active buffers and recreate them. + /// + /// For this reason we take the input stream if it exists. + /// + /// `num_channels` is the desired number of output channels. + /// + /// This returns a full AsioStreams with both input and output if input was active. + pub fn prepare_output_stream( + &self, + input: Option, + num_channels: usize, + ) -> Result { + let input_buffer_infos = input + .map(|input| input.buffer_infos) + .unwrap_or_else(Vec::new); + let output_buffer_infos = prepare_buffer_infos(false, num_channels); + self.create_streams(input_buffer_infos, output_buffer_infos) + } + + /// Releases buffers allocations. + /// + /// This will `stop` the stream if the driver is `Running`. + /// + /// No-op if no buffers are allocated. + pub fn dispose_buffers(&self) -> Result<(), AsioError> { + self.inner.dispose_buffers_inner() + } + + /// Starts ASIO streams playing. + /// + /// The driver must be in the `Prepared` state + /// + /// If called successfully, the driver will be in the `Running` state. + /// + /// No-op if already `Running`. + pub fn start(&self) -> Result<(), AsioError> { + if let DriverState::Running = self.inner.state() { + return Ok(()); + } + unsafe { + asio_result!(ai::ASIOStart())?; + } + self.inner.set_state(DriverState::Running); + Ok(()) + } + + /// Stops ASIO streams playing. + /// + /// No-op if the state is not `Running`. + /// + /// If the state was `Running` and the stream is stopped successfully, the driver will be in + /// the `Prepared` state. + pub fn stop(&self) -> Result<(), AsioError> { + self.inner.stop_inner() + } + + /// Adds a callback to the list of active callbacks. + /// + /// The given function receives the index of the buffer currently ready for processing. + pub fn set_callback(&self, callback: F) + where + F: 'static + FnMut(i32) + Send, + { + let mut bc = BUFFER_CALLBACK.lock().unwrap(); + bc.push(Some(BufferCallback(Box::new(callback)))); + } + + /// Consumes and destroys the `Driver`, stopping the streams if they are running and releasing + /// any associated resources. + /// + /// Returns `Ok(true)` if the driver was successfully destroyed. + /// + /// Returns `Ok(false)` if the driver was not destroyed because another handle to the driver + /// still exists. + /// + /// Returns `Err` if some switching driver states failed or if ASIO returned an error on exit. + pub fn destroy(self) -> Result { + let Driver { inner } = self; + match Arc::try_unwrap(inner) { + Err(_) => Ok(false), + Ok(mut inner) => { + inner.destroy_inner()?; + Ok(true) + } + } + } +} + +impl DriverInner { + fn state(&self) -> DriverState { + *self.state.lock().expect("failed to lock `DriverState`") + } + + fn set_state(&self, state: DriverState) { + *self.state.lock().expect("failed to lock `DriverState`") = state; + } + + fn stop_inner(&self) -> Result<(), AsioError> { + if let DriverState::Running = self.state() { + unsafe { + asio_result!(ai::ASIOStop())?; + } + self.set_state(DriverState::Prepared); + } + Ok(()) + } + + fn dispose_buffers_inner(&self) -> Result<(), AsioError> { + if let DriverState::Initialized = self.state() { + return Ok(()); + } + if let DriverState::Running = self.state() { + self.stop_inner()?; + } + unsafe { + asio_result!(ai::ASIODisposeBuffers())?; + } + self.set_state(DriverState::Initialized); + Ok(()) + } + + fn destroy_inner(&mut self) -> Result<(), AsioError> { + // Drop back through the driver state machine one state at a time. + if let DriverState::Running = self.state() { + self.stop_inner()?; + } + if let DriverState::Prepared = self.state() { + self.dispose_buffers_inner()?; + } + unsafe { + asio_result!(ai::ASIOExit())?; + ai::remove_current_driver(); + } + + // Clear any existing stream callbacks. + if let Ok(mut bcs) = BUFFER_CALLBACK.lock() { + bcs.clear(); + } + + // Signal that the driver has been destroyed. + self.destroyed = true; + + Ok(()) + } +} + +impl Drop for DriverInner { + fn drop(&mut self) { + if !self.destroyed { + // We probably shouldn't `panic!` in the destructor? We also shouldn't ignore errors + // though either. + self.destroy_inner().ok(); + } + } +} + +unsafe impl Send for AsioStream {} + +/// Used by the input and output stream creation process. +fn prepare_buffer_infos(is_input: bool, n_channels: usize) -> Vec { + let is_input = if is_input { 1 } else { 0 }; + (0..n_channels) + .map(|ch| { + let channel_num = ch as c_long; + // To be filled by ASIOCreateBuffers. + let buffers = [std::ptr::null_mut(); 2]; + AsioBufferInfo { is_input, channel_num, buffers } + }) + .collect() +} + +/// The set of callbacks passed to `ASIOCreateBuffers`. +fn create_asio_callbacks() -> AsioCallbacks { + AsioCallbacks { + buffer_switch: buffer_switch, + sample_rate_did_change: sample_rate_did_change, + asio_message: asio_message, + buffer_switch_time_info: buffer_switch_time_info, + } +} + +/// Retrieve the minimum, maximum and preferred buffer sizes along with the available +/// buffer size granularity. +fn asio_get_buffer_sizes() -> Result { + let mut b = BufferSizes::default(); + unsafe { + let res = ai::ASIOGetBufferSize(&mut b.min, &mut b.max, &mut b.pref, &mut b.grans); + asio_result!(res)?; + } + Ok(b) +} + +/// Retrieve the `ASIOChannelInfo` associated with the channel at the given index on either the +/// input or output stream (`true` for input). +fn asio_channel_info(channel: c_long, is_input: bool) -> Result { + let mut channel_info = ai::ASIOChannelInfo { + // Which channel we are querying + channel, + // Was it input or output + isInput: if is_input { 1 } else { 0 }, + // Was it active + isActive: 0, + channelGroup: 0, + // The sample type + type_: 0, + name: [0 as c_char; 32], + }; + unsafe { + asio_result!(ai::ASIOGetChannelInfo(&mut channel_info))?; + Ok(channel_info) + } +} + +/// Retrieve the data type of either the input or output stream. +/// +/// If `is_input` is true, this will be queried on the input stream. +fn stream_data_type(is_input: bool) -> Result { + let channel_info = asio_channel_info(0, is_input)?; + Ok(FromPrimitive::from_i32(channel_info.type_).expect("unkown `ASIOSampletype` value")) +} + +/// ASIO uses null terminated c strings for driver names. +/// +/// This converts to utf8. +fn driver_name_to_utf8(bytes: &[c_char]) -> std::borrow::Cow { + unsafe { + CStr::from_ptr(bytes.as_ptr()).to_string_lossy() + } +} + +/// ASIO uses null terminated c strings for channel names. +/// +/// This converts to utf8. +fn _channel_name_to_utf8(bytes: &[c_char]) -> std::borrow::Cow { + unsafe { + CStr::from_ptr(bytes.as_ptr()).to_string_lossy() + } +} + +/// Indicates the stream sample rate has changed. +/// +/// TODO: Provide some way of allowing CPAL to handle this. +extern "C" fn sample_rate_did_change(s_rate: c_double) -> () { + eprintln!("unhandled sample rate change to {}", s_rate); +} + +/// Message callback for ASIO to notify of certain events. +extern "C" fn asio_message( + selector: c_long, + value: c_long, + _message: *mut (), + _opt: *mut c_double, +) -> c_long { + match selector { + ai::kAsioSelectorSupported => { + // Indicate what message selectors are supported. + match value { + | ai::kAsioResetRequest + | ai::kAsioEngineVersion + | ai::kAsioResyncRequest + | ai::kAsioLatenciesChanged + // Following added in ASIO 2.0. + | ai::kAsioSupportsTimeInfo + | ai::kAsioSupportsTimeCode + | ai::kAsioSupportsInputMonitor => 1, + _ => 0, + } + } + + ai::kAsioResetRequest => { + // Defer the task and perform the reset of the driver during the next "safe" situation + // You cannot reset the driver right now, as this code is called from the driver. Reset + // the driver is done by completely destruct it. I.e. ASIOStop(), ASIODisposeBuffers(), + // Destruction. Afterwards you initialize the driver again. + // TODO: Handle this. + 1 + } + + ai::kAsioResyncRequest => { + // This informs the application, that the driver encountered some non fatal data loss. + // It is used for synchronization purposes of different media. Added mainly to work + // around the Win16Mutex problems in Windows 95/98 with the Windows Multimedia system, + // which could loose data because the Mutex was hold too long by another thread. + // However a driver can issue it in other situations, too. + // TODO: Handle this. + 1 + } + + ai::kAsioLatenciesChanged => { + // This will inform the host application that the drivers were latencies changed. + // Beware, it this does not mean that the buffer sizes have changed! You might need to + // update internal delay data. + // TODO: Handle this. + 1 + } + + ai::kAsioEngineVersion => { + // Return the supported ASIO version of the host application If a host applications + // does not implement this selector, ASIO 1.0 is assumed by the driver + 2 + } + + ai::kAsioSupportsTimeInfo => { + // Informs the driver whether the asioCallbacks.bufferSwitchTimeInfo() callback is + // supported. For compatibility with ASIO 1.0 drivers the host application should + // always support the "old" bufferSwitch method, too, which we do. + 1 + } + + ai::kAsioSupportsTimeCode => { + // Informs the driver whether the application is interested in time code info. If an + // application does not need to know about time code, the driver has less work to do. + // TODO: Provide an option for this? + 0 + } + + _ => 0, // Unknown/unhandled message type. + } +} + +/// Similar to buffer switch but with time info. +/// +/// If only `buffer_switch` is called by the driver instead, the `buffer_switch` callback will +/// create the necessary timing info and call this function. +/// +/// TODO: Provide some access to `ai::ASIOTime` once CPAL gains support for time stamps. +extern "C" fn buffer_switch_time_info( + time: *mut ai::ASIOTime, + double_buffer_index: c_long, + _direct_process: c_long, +) -> *mut ai::ASIOTime { + // This lock is probably unavoidable, but locks in the audio stream are not great. + let mut bcs = BUFFER_CALLBACK.lock().unwrap(); + for mut bc in bcs.iter_mut() { + if let Some(ref mut bc) = bc { + bc.run(double_buffer_index); + } + } + time +} + +/// This is called by ASIO. +/// +/// Here we run the callback for each stream. +/// +/// `double_buffer_index` is either `0` or `1` indicating which buffer to fill. +extern "C" fn buffer_switch(double_buffer_index: c_long, direct_process: c_long) -> () { + // Emulate the time info provided by the `buffer_switch_time_info` callback. + // This is an attempt at matching the behaviour in `hostsample.cpp` from the SDK. + let mut time = unsafe { + let mut time: AsioTime = std::mem::zeroed(); + let res = ai::ASIOGetSamplePosition( + &mut time.time_info.sample_position, + &mut time.time_info.system_time, + ); + if let Ok(()) = asio_result!(res) { + time.time_info.flags = + (ai::AsioTimeInfoFlags::kSystemTimeValid | ai::AsioTimeInfoFlags::kSamplePositionValid).0; + } + time + }; + + // Actual processing happens within the `buffer_switch_time_info` callback. + let asio_time_ptr = &mut time as *mut AsioTime as *mut ai::ASIOTime; + buffer_switch_time_info(asio_time_ptr, double_buffer_index, direct_process); +} + +#[test] +fn check_type_sizes() { + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); + assert_eq!(std::mem::size_of::(), std::mem::size_of::()); +} diff --git a/asio-sys/src/lib.rs b/asio-sys/src/lib.rs new file mode 100644 index 0000000..a071e7b --- /dev/null +++ b/asio-sys/src/lib.rs @@ -0,0 +1,18 @@ +#![allow(non_camel_case_types)] + +#[allow(unused_imports)] +#[macro_use] +extern crate lazy_static; + +#[allow(unused_imports)] +#[macro_use] +extern crate num_derive; +#[allow(unused_imports)] +extern crate num_traits; + +#[cfg(asio)] +pub mod bindings; +#[cfg(asio)] +pub use bindings::*; +#[cfg(asio)] +pub use bindings::errors::{AsioError, LoadDriverError}; diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..3975ea9 --- /dev/null +++ b/build.rs @@ -0,0 +1,12 @@ +use std::env; + +const CPAL_ASIO_DIR: &'static str = "CPAL_ASIO_DIR"; + +fn main() { + // If ASIO directory isn't set silently return early + // otherwise set the asio config flag + match env::var(CPAL_ASIO_DIR) { + Err(_) => return, + Ok(_) => println!("cargo:rustc-cfg=asio"), + }; +} \ No newline at end of file diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs new file mode 100644 index 0000000..ad6159d --- /dev/null +++ b/src/host/asio/device.rs @@ -0,0 +1,192 @@ +use std; +pub type SupportedInputFormats = std::vec::IntoIter; +pub type SupportedOutputFormats = std::vec::IntoIter; + +use std::hash::{Hash, Hasher}; +use std::sync::Arc; +use BackendSpecificError; +use DefaultFormatError; +use DeviceNameError; +use DevicesError; +use Format; +use SampleFormat; +use SampleRate; +use SupportedFormat; +use SupportedFormatsError; +use super::sys; + +/// A ASIO Device +#[derive(Debug)] +pub struct Device { + /// The driver represented by this device. + pub driver: Arc, +} + +/// All available devices. +pub struct Devices { + asio: Arc, + drivers: std::vec::IntoIter, +} + +impl PartialEq for Device { + fn eq(&self, other: &Self) -> bool { + self.driver.name() == other.driver.name() + } +} + +impl Eq for Device {} + +impl Hash for Device { + fn hash(&self, state: &mut H) { + self.driver.name().hash(state); + } +} + +impl Device { + pub fn name(&self) -> Result { + Ok(self.driver.name().to_string()) + } + + /// Gets the supported input formats. + /// TODO currently only supports the default. + /// Need to find all possible formats. + pub fn supported_input_formats( + &self, + ) -> Result { + // Retrieve the default format for the total supported channels and supported sample + // format. + let mut f = match self.default_input_format() { + Err(_) => return Err(SupportedFormatsError::DeviceNotAvailable), + Ok(f) => f, + }; + + // Collect a format for every combination of supported sample rate and number of channels. + let mut supported_formats = vec![]; + for &rate in ::COMMON_SAMPLE_RATES { + if !self.driver.can_sample_rate(rate.0.into()).ok().unwrap_or(false) { + continue; + } + for channels in 1..f.channels + 1 { + f.channels = channels; + f.sample_rate = rate; + supported_formats.push(SupportedFormat::from(f.clone())); + } + } + Ok(supported_formats.into_iter()) + } + + /// Gets the supported output formats. + /// TODO currently only supports the default. + /// Need to find all possible formats. + pub fn supported_output_formats( + &self, + ) -> Result { + // Retrieve the default format for the total supported channels and supported sample + // format. + let mut f = match self.default_output_format() { + Err(_) => return Err(SupportedFormatsError::DeviceNotAvailable), + Ok(f) => f, + }; + + // Collect a format for every combination of supported sample rate and number of channels. + let mut supported_formats = vec![]; + for &rate in ::COMMON_SAMPLE_RATES { + if !self.driver.can_sample_rate(rate.0.into()).ok().unwrap_or(false) { + continue; + } + for channels in 1..f.channels + 1 { + f.channels = channels; + f.sample_rate = rate; + supported_formats.push(SupportedFormat::from(f.clone())); + } + } + Ok(supported_formats.into_iter()) + } + + /// Returns the default input format + pub fn default_input_format(&self) -> Result { + let channels = self.driver.channels().map_err(default_format_err)?.ins as u16; + let sample_rate = SampleRate(self.driver.sample_rate().map_err(default_format_err)? as _); + // Map th ASIO sample type to a CPAL sample type + let data_type = self.driver.input_data_type().map_err(default_format_err)?; + let data_type = convert_data_type(&data_type) + .ok_or(DefaultFormatError::StreamTypeNotSupported)?; + Ok(Format { + channels, + sample_rate, + data_type, + }) + } + + /// Returns the default output format + pub fn default_output_format(&self) -> Result { + let channels = self.driver.channels().map_err(default_format_err)?.outs as u16; + let sample_rate = SampleRate(self.driver.sample_rate().map_err(default_format_err)? as _); + let data_type = self.driver.output_data_type().map_err(default_format_err)?; + let data_type = convert_data_type(&data_type) + .ok_or(DefaultFormatError::StreamTypeNotSupported)?; + Ok(Format { + channels, + sample_rate, + data_type, + }) + } +} + +impl Devices { + pub fn new(asio: Arc) -> Result { + let drivers = asio.driver_names().into_iter(); + Ok(Devices { asio, drivers }) + } +} + +impl Iterator for Devices { + type Item = Device; + + /// Load drivers and return device + fn next(&mut self) -> Option { + loop { + match self.drivers.next() { + Some(name) => match self.asio.load_driver(&name) { + Ok(driver) => return Some(Device { driver: Arc::new(driver) }), + Err(_) => continue, + } + None => return None, + } + } + } + + fn size_hint(&self) -> (usize, Option) { + unimplemented!() + } +} + +pub(crate) fn convert_data_type(ty: &sys::AsioSampleType) -> Option { + let fmt = match *ty { + sys::AsioSampleType::ASIOSTInt16MSB => SampleFormat::I16, + sys::AsioSampleType::ASIOSTInt16LSB => SampleFormat::I16, + sys::AsioSampleType::ASIOSTFloat32MSB => SampleFormat::F32, + sys::AsioSampleType::ASIOSTFloat32LSB => SampleFormat::F32, + // NOTE: While ASIO does not support these formats directly, the stream callback created by + // CPAL supports converting back and forth between the following. This is because many ASIO + // drivers only support `Int32` formats, while CPAL does not support this format at all. We + // allow for this implicit conversion temporarily until CPAL gets support for an `I32` + // format. + sys::AsioSampleType::ASIOSTInt32MSB => SampleFormat::I16, + sys::AsioSampleType::ASIOSTInt32LSB => SampleFormat::I16, + _ => return None, + }; + Some(fmt) +} + +fn default_format_err(e: sys::AsioError) -> DefaultFormatError { + match e { + sys::AsioError::NoDrivers | + sys::AsioError::HardwareMalfunction => DefaultFormatError::DeviceNotAvailable, + sys::AsioError::NoRate => DefaultFormatError::StreamTypeNotSupported, + err => { + let description = format!("{}", err); + BackendSpecificError { description }.into() + } + } +} diff --git a/src/host/asio/mod.rs b/src/host/asio/mod.rs new file mode 100644 index 0000000..f752cd9 --- /dev/null +++ b/src/host/asio/mod.rs @@ -0,0 +1,136 @@ +extern crate asio_sys as sys; + +use { + BuildStreamError, + DefaultFormatError, + DeviceNameError, + DevicesError, + Format, + PauseStreamError, + PlayStreamError, + StreamDataResult, + SupportedFormatsError, +}; +use traits::{ + DeviceTrait, + EventLoopTrait, + HostTrait, + StreamIdTrait, +}; + +pub use self::device::{Device, Devices, SupportedInputFormats, SupportedOutputFormats}; +pub use self::stream::{EventLoop, StreamId}; +use std::sync::Arc; + +mod device; +mod stream; + +/// The host for ASIO. +#[derive(Debug)] +pub struct Host { + asio: Arc, +} + +impl Host { + pub fn new() -> Result { + let asio = Arc::new(sys::Asio::new()); + let host = Host { asio }; + Ok(host) + } +} + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + type EventLoop = EventLoop; + + fn is_available() -> bool { + true + //unimplemented!("check how to do this using asio-sys") + } + + fn devices(&self) -> Result { + Devices::new(self.asio.clone()) + } + + fn default_input_device(&self) -> Option { + // ASIO has no concept of a default device, so just use the first. + self.input_devices().ok().and_then(|mut ds| ds.next()) + } + + fn default_output_device(&self) -> Option { + // ASIO has no concept of a default device, so just use the first. + self.output_devices().ok().and_then(|mut ds| ds.next()) + } + + fn event_loop(&self) -> Self::EventLoop { + EventLoop::new() + } +} + +impl DeviceTrait for Device { + type SupportedInputFormats = SupportedInputFormats; + type SupportedOutputFormats = SupportedOutputFormats; + + fn name(&self) -> Result { + Device::name(self) + } + + fn supported_input_formats(&self) -> Result { + Device::supported_input_formats(self) + } + + fn supported_output_formats(&self) -> Result { + Device::supported_output_formats(self) + } + + fn default_input_format(&self) -> Result { + Device::default_input_format(self) + } + + fn default_output_format(&self) -> Result { + Device::default_output_format(self) + } +} + +impl EventLoopTrait for EventLoop { + type Device = Device; + type StreamId = StreamId; + + fn build_input_stream( + &self, + device: &Self::Device, + format: &Format, + ) -> Result { + EventLoop::build_input_stream(self, device, format) + } + + fn build_output_stream( + &self, + device: &Self::Device, + format: &Format, + ) -> Result { + EventLoop::build_output_stream(self, device, format) + } + + fn play_stream(&self, stream: Self::StreamId) -> Result<(), PlayStreamError> { + EventLoop::play_stream(self, stream) + } + + fn pause_stream(&self, stream: Self::StreamId) -> Result<(), PauseStreamError> { + EventLoop::pause_stream(self, stream) + } + + fn destroy_stream(&self, stream: Self::StreamId) { + EventLoop::destroy_stream(self, stream) + } + + fn run(&self, callback: F) -> ! + where + F: FnMut(Self::StreamId, StreamDataResult) + Send, + { + EventLoop::run(self, callback) + } +} + +impl StreamIdTrait for StreamId {} diff --git a/src/host/asio/stream.rs b/src/host/asio/stream.rs new file mode 100644 index 0000000..661620a --- /dev/null +++ b/src/host/asio/stream.rs @@ -0,0 +1,814 @@ +extern crate asio_sys as sys; +extern crate num_traits; + +use self::num_traits::PrimInt; +use super::Device; +use std; +use std::mem; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; +use BackendSpecificError; +use BuildStreamError; +use Format; +use PauseStreamError; +use PlayStreamError; +use SampleFormat; +use StreamData; +use StreamDataResult; +use UnknownTypeInputBuffer; +use UnknownTypeOutputBuffer; + +/// Sample types whose constant silent value is known. +trait Silence { + const SILENCE: Self; +} + +/// Constraints on the interleaved sample buffer format required by the CPAL API. +trait InterleavedSample: Clone + Copy + Silence { + fn unknown_type_input_buffer(&[Self]) -> UnknownTypeInputBuffer; + fn unknown_type_output_buffer(&mut [Self]) -> UnknownTypeOutputBuffer; +} + +/// Constraints on the ASIO sample types. +trait AsioSample: Clone + Copy + Silence + std::ops::Add {} + +/// Controls all streams +pub struct EventLoop { + /// The input and output ASIO streams + asio_streams: Arc>, + /// List of all CPAL streams + cpal_streams: Arc>>>, + /// Total stream count. + stream_count: AtomicUsize, + /// The CPAL callback that the user gives to fill the buffers. + callbacks: Arc>>, +} + +/// Id for each stream. +/// Created depending on the number they are created. +/// Starting at one! not zero. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct StreamId(usize); + +/// CPAL stream. +/// This decouples the many cpal streams +/// from the single input and single output +/// ASIO streams. +/// Each stream can be playing or paused. +struct Stream { + playing: bool, + // The driver associated with this stream. + driver: Arc, +} + +// Used to keep track of whether or not the current current asio stream buffer requires +// being silencing before summing audio. +#[derive(Default)] +struct SilenceAsioBuffer { + first: bool, + second: bool, +} + +impl EventLoop { + pub fn new() -> EventLoop { + EventLoop { + asio_streams: Arc::new(Mutex::new(sys::AsioStreams { + input: None, + output: None, + })), + cpal_streams: Arc::new(Mutex::new(Vec::new())), + // This is why the Id's count from one not zero + // because at this point there is no streams + stream_count: AtomicUsize::new(0), + callbacks: Arc::new(Mutex::new(None)), + } + } + + /// Create a new CPAL Input Stream. + /// + /// If there is no existing ASIO Input Stream it will be created. + /// + /// On success, the buffer size of the stream is returned. + fn get_or_create_input_stream( + &self, + driver: &sys::Driver, + format: &Format, + device: &Device, + ) -> Result { + match device.default_input_format() { + Ok(f) => { + let num_asio_channels = f.channels; + check_format(driver, format, num_asio_channels) + }, + Err(_) => Err(BuildStreamError::FormatNotSupported), + }?; + let num_channels = format.channels as usize; + let ref mut streams = *self.asio_streams.lock().unwrap(); + // Either create a stream if thers none or had back the + // size of the current one. + match streams.input { + Some(ref input) => Ok(input.buffer_size as usize), + None => { + let output = streams.output.take(); + driver + .prepare_input_stream(output, num_channels) + .map(|new_streams| { + let bs = match new_streams.input { + Some(ref inp) => inp.buffer_size as usize, + None => unreachable!(), + }; + *streams = new_streams; + bs + }).map_err(|ref e| { + println!("Error preparing stream: {}", e); + BuildStreamError::DeviceNotAvailable + }) + } + } + } + + /// Create a new CPAL Output Stream. + /// + /// If there is no existing ASIO Output Stream it will be created. + /// + /// On success, the buffer size of the stream is returned. + fn get_or_create_output_stream( + &self, + driver: &sys::Driver, + format: &Format, + device: &Device, + ) -> Result { + match device.default_output_format() { + Ok(f) => { + let num_asio_channels = f.channels; + check_format(driver, format, num_asio_channels) + }, + Err(_) => Err(BuildStreamError::FormatNotSupported), + }?; + let num_channels = format.channels as usize; + let ref mut streams = *self.asio_streams.lock().unwrap(); + // Either create a stream if there's none or return the size of the current one. + match streams.output { + Some(ref output) => Ok(output.buffer_size as usize), + None => { + let input = streams.input.take(); + driver + .prepare_output_stream(input, num_channels) + .map(|new_streams| { + let bs = match new_streams.output { + Some(ref out) => out.buffer_size as usize, + None => unreachable!(), + }; + *streams = new_streams; + bs + }).map_err(|ref e| { + println!("Error preparing stream: {}", e); + BuildStreamError::DeviceNotAvailable + }) + } + } + } + + /// Builds a new cpal input stream + pub fn build_input_stream( + &self, + device: &Device, + format: &Format, + ) -> Result { + let Device { driver, .. } = device; + let stream_type = driver.input_data_type().map_err(build_stream_err)?; + + // Ensure that the desired sample type is supported. + let data_type = super::device::convert_data_type(&stream_type) + .ok_or(BuildStreamError::FormatNotSupported)?; + if format.data_type != data_type { + return Err(BuildStreamError::FormatNotSupported); + } + + let num_channels = format.channels.clone(); + let stream_buffer_size = self.get_or_create_input_stream(&driver, format, device)?; + let cpal_num_samples = stream_buffer_size * num_channels as usize; + let count = self.stream_count.fetch_add(1, Ordering::SeqCst); + let asio_streams = self.asio_streams.clone(); + let cpal_streams = self.cpal_streams.clone(); + let callbacks = self.callbacks.clone(); + + // Create the buffer depending on the size of the data type. + let stream_id = StreamId(count); + let len_bytes = cpal_num_samples * data_type.sample_size(); + let mut interleaved = vec![0u8; len_bytes]; + + // Set the input callback. + // This is most performance critical part of the ASIO bindings. + driver.set_callback(move |buffer_index| unsafe { + // If not playing return early. + // TODO: Don't assume `count` is valid - we should search for the matching `StreamId`. + if let Some(s) = cpal_streams.lock().unwrap().get(count) { + if let Some(s) = s { + if !s.playing { + return; + } + } + } + + // Acquire the stream and callback. + let stream_lock = asio_streams.lock().unwrap(); + let ref asio_stream = match stream_lock.input { + Some(ref asio_stream) => asio_stream, + None => return, + }; + let mut callbacks = callbacks.lock().unwrap(); + let callback = match callbacks.as_mut() { + Some(callback) => callback, + None => return, + }; + + /// 1. Write from the ASIO buffer to the interleaved CPAL buffer. + /// 2. Deliver the CPAL buffer to the user callback. + unsafe fn process_input_callback( + stream_id: StreamId, + callback: &mut (dyn FnMut(StreamId, StreamDataResult) + Send), + interleaved: &mut [u8], + asio_stream: &sys::AsioStream, + buffer_index: usize, + from_endianness: F, + to_cpal_sample: G, + ) + where + A: AsioSample, + B: InterleavedSample, + F: Fn(A) -> A, + G: Fn(A) -> B, + { + // 1. Write the ASIO channels to the CPAL buffer. + let interleaved: &mut [B] = cast_slice_mut(interleaved); + let n_channels = interleaved.len() / asio_stream.buffer_size as usize; + for ch_ix in 0..n_channels { + let asio_channel = asio_channel_slice::(asio_stream, buffer_index, ch_ix); + for (frame, s_asio) in interleaved.chunks_mut(n_channels).zip(asio_channel) { + frame[ch_ix] = to_cpal_sample(from_endianness(*s_asio)); + } + } + + // 2. Deliver the interleaved buffer to the callback. + callback( + stream_id, + Ok(StreamData::Input { buffer: B::unknown_type_input_buffer(interleaved) }), + ); + } + + match (&stream_type, data_type) { + (&sys::AsioSampleType::ASIOSTInt16LSB, SampleFormat::I16) => { + process_input_callback::( + stream_id, + callback, + &mut interleaved, + asio_stream, + buffer_index as usize, + from_le, + std::convert::identity::, + ); + } + (&sys::AsioSampleType::ASIOSTInt16MSB, SampleFormat::I16) => { + process_input_callback::( + stream_id, + callback, + &mut interleaved, + asio_stream, + buffer_index as usize, + from_be, + std::convert::identity::, + ); + } + + // TODO: Handle endianness conversion for floats? We currently use the `PrimInt` + // trait for the `to_le` and `to_be` methods, but this does not support floats. + (&sys::AsioSampleType::ASIOSTFloat32LSB, SampleFormat::F32) | + (&sys::AsioSampleType::ASIOSTFloat32MSB, SampleFormat::F32) => { + process_input_callback::( + stream_id, + callback, + &mut interleaved, + asio_stream, + buffer_index as usize, + std::convert::identity::, + std::convert::identity::, + ); + } + + // TODO: Add support for the following sample formats to CPAL and simplify the + // `process_output_callback` function above by removing the unnecessary sample + // conversion function. + (&sys::AsioSampleType::ASIOSTInt32LSB, SampleFormat::I16) => { + process_input_callback::( + stream_id, + callback, + &mut interleaved, + asio_stream, + buffer_index as usize, + from_le, + |s| (s >> 16) as i16, + ); + } + (&sys::AsioSampleType::ASIOSTInt32MSB, SampleFormat::I16) => { + process_input_callback::( + stream_id, + callback, + &mut interleaved, + asio_stream, + buffer_index as usize, + from_be, + |s| (s >> 16) as i16, + ); + } + // TODO: Handle endianness conversion for floats? We currently use the `PrimInt` + // trait for the `to_le` and `to_be` methods, but this does not support floats. + (&sys::AsioSampleType::ASIOSTFloat64LSB, SampleFormat::F32) | + (&sys::AsioSampleType::ASIOSTFloat64MSB, SampleFormat::F32) => { + process_input_callback::( + stream_id, + callback, + &mut interleaved, + asio_stream, + buffer_index as usize, + std::convert::identity::, + |s| s as f32, + ); + } + + unsupported_format_pair => { + unreachable!("`build_input_stream` should have returned with unsupported \ + format {:?}", unsupported_format_pair) + } + } + }); + + // Create stream and set to paused + self.cpal_streams + .lock() + .unwrap() + .push(Some(Stream { driver: driver.clone(), playing: false })); + + Ok(StreamId(count)) + } + + /// Create the an output cpal stream. + pub fn build_output_stream( + &self, + device: &Device, + format: &Format, + ) -> Result { + let Device { driver, .. } = device; + let stream_type = driver.output_data_type().map_err(build_stream_err)?; + + // Ensure that the desired sample type is supported. + let data_type = super::device::convert_data_type(&stream_type) + .ok_or(BuildStreamError::FormatNotSupported)?; + if format.data_type != data_type { + return Err(BuildStreamError::FormatNotSupported); + } + + let num_channels = format.channels.clone(); + let stream_buffer_size = self.get_or_create_output_stream(&driver, format, device)?; + let cpal_num_samples = stream_buffer_size * num_channels as usize; + let count = self.stream_count.fetch_add(1, Ordering::SeqCst); + let asio_streams = self.asio_streams.clone(); + let cpal_streams = self.cpal_streams.clone(); + let callbacks = self.callbacks.clone(); + + // Create buffers depending on data type. + let stream_id = StreamId(count); + let len_bytes = cpal_num_samples * data_type.sample_size(); + let mut interleaved = vec![0u8; len_bytes]; + let mut silence_asio_buffer = SilenceAsioBuffer::default(); + + driver.set_callback(move |buffer_index| unsafe { + // If not playing, return early. + // TODO: Don't assume `count` is valid - we should search for the matching `StreamId`. + if let Some(s) = cpal_streams.lock().unwrap().get(count) { + if let Some(s) = s { + if !s.playing { + return (); + } + } + } + + // Acquire the stream and callback. + let stream_lock = asio_streams.lock().unwrap(); + let ref asio_stream = match stream_lock.output { + Some(ref asio_stream) => asio_stream, + None => return, + }; + let mut callbacks = callbacks.lock().unwrap(); + let callback = callbacks.as_mut(); + + // Silence the ASIO buffer that is about to be used. + // + // This checks if any other callbacks have already silenced the buffer associated with + // the current `buffer_index`. + // + // If not, we will silence it and set the opposite buffer half to unsilenced. + let silence = match buffer_index { + 0 if !silence_asio_buffer.first => { + silence_asio_buffer.first = true; + silence_asio_buffer.second = false; + true + } + 0 => false, + 1 if !silence_asio_buffer.second => { + silence_asio_buffer.second = true; + silence_asio_buffer.first = false; + true + } + 1 => false, + _ => unreachable!("ASIO uses a double-buffer so there should only be 2"), + }; + + /// 1. Render the given callback to the given buffer of interleaved samples. + /// 2. If required, silence the ASIO buffer. + /// 3. Finally, write the interleaved data to the non-interleaved ASIO buffer, + /// performing endianness conversions as necessary. + unsafe fn process_output_callback( + stream_id: StreamId, + callback: Option<&mut &mut (dyn FnMut(StreamId, StreamDataResult) + Send)>, + interleaved: &mut [u8], + silence_asio_buffer: bool, + asio_stream: &sys::AsioStream, + buffer_index: usize, + to_asio_sample: F, + to_endianness: G, + ) + where + A: InterleavedSample, + B: AsioSample, + F: Fn(A) -> B, + G: Fn(B) -> B, + { + // 1. Render interleaved buffer from callback. + let interleaved: &mut [A] = cast_slice_mut(interleaved); + match callback { + None => interleaved.iter_mut().for_each(|s| *s = A::SILENCE), + Some(callback) => { + let buffer = A::unknown_type_output_buffer(interleaved); + callback(stream_id, Ok(StreamData::Output { buffer })); + } + } + + // 2. Silence ASIO channels if necessary. + let n_channels = interleaved.len() / asio_stream.buffer_size as usize; + if silence_asio_buffer { + for ch_ix in 0..n_channels { + let asio_channel = + asio_channel_slice_mut::(asio_stream, buffer_index, ch_ix); + asio_channel.iter_mut().for_each(|s| *s = to_endianness(B::SILENCE)); + } + } + + // 3. Write interleaved samples to ASIO channels, one channel at a time. + for ch_ix in 0..n_channels { + let asio_channel = + asio_channel_slice_mut::(asio_stream, buffer_index, ch_ix); + for (frame, s_asio) in interleaved.chunks(n_channels).zip(asio_channel) { + *s_asio = *s_asio + to_endianness(to_asio_sample(frame[ch_ix])); + } + } + } + + match (data_type, &stream_type) { + (SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16LSB) => { + process_output_callback::( + stream_id, + callback, + &mut interleaved, + silence, + asio_stream, + buffer_index as usize, + std::convert::identity::, + to_le, + ); + } + (SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16MSB) => { + process_output_callback::( + stream_id, + callback, + &mut interleaved, + silence, + asio_stream, + buffer_index as usize, + std::convert::identity::, + to_be, + ); + } + + // TODO: Handle endianness conversion for floats? We currently use the `PrimInt` + // trait for the `to_le` and `to_be` methods, but this does not support floats. + (SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32LSB) | + (SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat32MSB) => { + process_output_callback::( + stream_id, + callback, + &mut interleaved, + silence, + asio_stream, + buffer_index as usize, + std::convert::identity::, + std::convert::identity::, + ); + } + + // TODO: Add support for the following sample formats to CPAL and simplify the + // `process_output_callback` function above by removing the unnecessary sample + // conversion function. + (SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt32LSB) => { + process_output_callback::( + stream_id, + callback, + &mut interleaved, + silence, + asio_stream, + buffer_index as usize, + |s| (s as i32) << 16, + to_le, + ); + } + (SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt32MSB) => { + process_output_callback::( + stream_id, + callback, + &mut interleaved, + silence, + asio_stream, + buffer_index as usize, + |s| (s as i32) << 16, + to_be, + ); + } + // TODO: Handle endianness conversion for floats? We currently use the `PrimInt` + // trait for the `to_le` and `to_be` methods, but this does not support floats. + (SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat64LSB) | + (SampleFormat::F32, &sys::AsioSampleType::ASIOSTFloat64MSB) => { + process_output_callback::( + stream_id, + callback, + &mut interleaved, + silence, + asio_stream, + buffer_index as usize, + |s| s as f64, + std::convert::identity::, + ); + } + + unsupported_format_pair => { + unreachable!("`build_output_stream` should have returned with unsupported \ + format {:?}", unsupported_format_pair) + } + } + }); + + // Create the stream paused + self.cpal_streams + .lock() + .unwrap() + .push(Some(Stream { driver: driver.clone(), playing: false })); + + // Give the ID based on the stream count + Ok(StreamId(count)) + } + + /// Play the cpal stream for the given ID. + pub fn play_stream(&self, stream_id: StreamId) -> Result<(), PlayStreamError> { + let mut streams = self.cpal_streams.lock().unwrap(); + if let Some(s) = streams.get_mut(stream_id.0).expect("Bad play stream index") { + s.playing = true; + // Calling play when already playing is a no-op + s.driver.start().map_err(play_stream_err)?; + } + Ok(()) + } + + /// Pause the cpal stream for the given ID. + /// + /// Pause the ASIO streams if there are no other CPAL streams playing, as ASIO only allows + /// stopping the entire driver. + pub fn pause_stream(&self, stream_id: StreamId) -> Result<(), PauseStreamError> { + let mut streams = self.cpal_streams.lock().unwrap(); + let streams_playing = streams.iter() + .filter(|s| s.as_ref().map(|s| s.playing).unwrap_or(false)) + .count(); + if let Some(s) = streams.get_mut(stream_id.0).expect("Bad pause stream index") { + if streams_playing <= 1 { + s.driver.stop().map_err(pause_stream_err)?; + } + s.playing = false; + } + Ok(()) + } + + /// Destroy the cpal stream based on the ID. + pub fn destroy_stream(&self, stream_id: StreamId) { + // TODO: Should we not also remove an ASIO stream here? + // Yes, and we should update the logic in the callbacks to search for the stream with + // the matching ID, rather than assuming the index associated with the ID is valid. + let mut streams = self.cpal_streams.lock().unwrap(); + streams.get_mut(stream_id.0).take(); + } + + /// Run the cpal callbacks + pub fn run(&self, mut callback: F) -> ! + where + F: FnMut(StreamId, StreamDataResult) + Send, + { + let callback: &mut (FnMut(StreamId, StreamDataResult) + Send) = &mut callback; + // Transmute needed to convince the compiler that the callback has a static lifetime + *self.callbacks.lock().unwrap() = Some(unsafe { mem::transmute(callback) }); + loop { + // A sleep here to prevent the loop being + // removed in --release + thread::sleep(Duration::new(1u64, 0u32)); + } + } +} + +/// Clean up if event loop is dropped. +/// Currently event loop is never dropped. +impl Drop for EventLoop { + fn drop(&mut self) { + *self.asio_streams.lock().unwrap() = sys::AsioStreams { + output: None, + input: None, + }; + } +} + +impl Silence for i16 { + const SILENCE: Self = 0; +} + +impl Silence for i32 { + const SILENCE: Self = 0; +} + +impl Silence for f32 { + const SILENCE: Self = 0.0; +} + +impl Silence for f64 { + const SILENCE: Self = 0.0; +} + +impl InterleavedSample for i16 { + fn unknown_type_input_buffer(buffer: &[Self]) -> UnknownTypeInputBuffer { + UnknownTypeInputBuffer::I16(::InputBuffer { buffer }) + } + + fn unknown_type_output_buffer(buffer: &mut [Self]) -> UnknownTypeOutputBuffer { + UnknownTypeOutputBuffer::I16(::OutputBuffer { buffer }) + } +} + +impl InterleavedSample for f32 { + fn unknown_type_input_buffer(buffer: &[Self]) -> UnknownTypeInputBuffer { + UnknownTypeInputBuffer::F32(::InputBuffer { buffer }) + } + + fn unknown_type_output_buffer(buffer: &mut [Self]) -> UnknownTypeOutputBuffer { + UnknownTypeOutputBuffer::F32(::OutputBuffer { buffer }) + } +} + +impl AsioSample for i16 {} + +impl AsioSample for i32 {} + +impl AsioSample for f32 {} + +impl AsioSample for f64 {} + +/// Check whether or not the desired format is supported by the stream. +/// +/// Checks sample rate, data type and then finally the number of channels. +fn check_format( + driver: &sys::Driver, + format: &Format, + num_asio_channels: u16, +) -> Result<(), BuildStreamError> { + let Format { + channels, + sample_rate, + data_type, + } = format; + // Try and set the sample rate to what the user selected. + let sample_rate = sample_rate.0.into(); + if sample_rate != driver.sample_rate().map_err(build_stream_err)? { + if driver.can_sample_rate(sample_rate).map_err(build_stream_err)? { + driver + .set_sample_rate(sample_rate) + .map_err(build_stream_err)?; + } else { + return Err(BuildStreamError::FormatNotSupported); + } + } + // unsigned formats are not supported by asio + match data_type { + SampleFormat::I16 | SampleFormat::F32 => (), + SampleFormat::U16 => return Err(BuildStreamError::FormatNotSupported), + } + if *channels > num_asio_channels { + return Err(BuildStreamError::FormatNotSupported); + } + Ok(()) +} + +/// Cast a byte slice into a mutable slice of desired type. +/// +/// Safety: it's up to the caller to ensure that the input slice has valid bit representations. +unsafe fn cast_slice_mut(v: &mut [u8]) -> &mut [T] { + debug_assert!(v.len() % std::mem::size_of::() == 0); + std::slice::from_raw_parts_mut(v.as_mut_ptr() as *mut T, v.len() / std::mem::size_of::()) +} + +/// Helper function to convert to little endianness. +fn to_le(t: T) -> T { + t.to_le() +} + +/// Helper function to convert to big endianness. +fn to_be(t: T) -> T { + t.to_be() +} + +/// Helper function to convert from little endianness. +fn from_le(t: T) -> T { + T::from_le(t) +} + +/// Helper function to convert from little endianness. +fn from_be(t: T) -> T { + T::from_be(t) +} + +/// Shorthand for retrieving the asio buffer slice associated with a channel. +/// +/// Safety: it's up to the user to ensure that this function is not called multiple times for the +/// same channel. +unsafe fn asio_channel_slice( + asio_stream: &sys::AsioStream, + buffer_index: usize, + channel_index: usize, +) -> &[T] { + asio_channel_slice_mut(asio_stream, buffer_index, channel_index) +} + +/// Shorthand for retrieving the asio buffer slice associated with a channel. +/// +/// Safety: it's up to the user to ensure that this function is not called multiple times for the +/// same channel. +unsafe fn asio_channel_slice_mut( + asio_stream: &sys::AsioStream, + buffer_index: usize, + channel_index: usize, +) -> &mut [T] { + let buff_ptr: *mut T = asio_stream + .buffer_infos[channel_index] + .buffers[buffer_index as usize] + as *mut _; + std::slice::from_raw_parts_mut(buff_ptr, asio_stream.buffer_size as usize) +} + +fn build_stream_err(e: sys::AsioError) -> BuildStreamError { + match e { + sys::AsioError::NoDrivers | + sys::AsioError::HardwareMalfunction => BuildStreamError::DeviceNotAvailable, + sys::AsioError::InvalidInput | + sys::AsioError::BadMode => BuildStreamError::InvalidArgument, + err => { + let description = format!("{}", err); + BackendSpecificError { description }.into() + } + } +} + +fn pause_stream_err(e: sys::AsioError) -> PauseStreamError { + match e { + sys::AsioError::NoDrivers | + sys::AsioError::HardwareMalfunction => PauseStreamError::DeviceNotAvailable, + err => { + let description = format!("{}", err); + BackendSpecificError { description }.into() + } + } +} + +fn play_stream_err(e: sys::AsioError) -> PlayStreamError { + match e { + sys::AsioError::NoDrivers | + sys::AsioError::HardwareMalfunction => PlayStreamError::DeviceNotAvailable, + err => { + let description = format!("{}", err); + BackendSpecificError { description }.into() + } + } +} diff --git a/src/host/mod.rs b/src/host/mod.rs index 0c3155e..3b1b61a 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -1,5 +1,7 @@ #[cfg(any(target_os = "linux", target_os = "freebsd"))] pub(crate) mod alsa; +#[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")] diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index a70b591..f57ba04 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -490,7 +490,6 @@ impl Device { format.sample_rate = SampleRate(rate as _); supported_formats.push(SupportedFormat::from(format.clone())); } - Ok(supported_formats.into_iter()) } } diff --git a/src/lib.rs b/src/lib.rs index 8257051..244be18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -377,6 +377,9 @@ pub enum BuildStreamError { /// them immediately. #[derive(Debug, Fail)] pub enum PlayStreamError { + /// The device associated with the stream is no longer available. + #[fail(display = "the device associated with the stream is no longer available")] + DeviceNotAvailable, /// See the `BackendSpecificError` docs for more information about this error variant. #[fail(display = "{}", err)] BackendSpecific { @@ -392,6 +395,9 @@ pub enum PlayStreamError { /// them immediately. #[derive(Debug, Fail)] pub enum PauseStreamError { + /// The device associated with the stream is no longer available. + #[fail(display = "the device associated with the stream is no longer available")] + DeviceNotAvailable, /// See the `BackendSpecificError` docs for more information about this error variant. #[fail(display = "{}", err)] BackendSpecific { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 74030a5..314705d 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -256,6 +256,7 @@ macro_rules! impl_platform_host { type StreamId = StreamId; type Device = Device; + #[allow(unreachable_patterns)] fn build_input_stream( &self, device: &Self::Device, @@ -269,9 +270,11 @@ macro_rules! impl_platform_host { .map(StreamId) } )* + _ => panic!("tried to build a stream with a device from another host"), } } + #[allow(unreachable_patterns)] fn build_output_stream( &self, device: &Self::Device, @@ -285,9 +288,11 @@ macro_rules! impl_platform_host { .map(StreamId) } )* + _ => panic!("tried to build a stream with a device from another host"), } } + #[allow(unreachable_patterns)] fn play_stream(&self, stream: Self::StreamId) -> Result<(), crate::PlayStreamError> { match (&self.0, stream.0) { $( @@ -295,9 +300,11 @@ macro_rules! impl_platform_host { e.play_stream(s.clone()) } )* + _ => panic!("tried to play a stream with an ID associated with another host"), } } + #[allow(unreachable_patterns)] fn pause_stream(&self, stream: Self::StreamId) -> Result<(), crate::PauseStreamError> { match (&self.0, stream.0) { $( @@ -305,9 +312,11 @@ macro_rules! impl_platform_host { e.pause_stream(s.clone()) } )* + _ => panic!("tried to pause a stream with an ID associated with another host"), } } + #[allow(unreachable_patterns)] fn destroy_stream(&self, stream: Self::StreamId) { match (&self.0, stream.0) { $( @@ -315,6 +324,7 @@ macro_rules! impl_platform_host { e.destroy_stream(s.clone()) } )* + _ => panic!("tried to destroy a stream with an ID associated with another host"), } } @@ -513,9 +523,18 @@ mod platform_impl { } } -// TODO: Add `Asio asio` once #221 lands. #[cfg(windows)] mod platform_impl { + #[cfg(feature = "asio")] + pub use crate::host::asio::{ + Device as AsioDevice, + Devices as AsioDevices, + EventLoop as AsioEventLoop, + Host as AsioHost, + StreamId as AsioStreamId, + SupportedInputFormats as AsioSupportedInputFormats, + SupportedOutputFormats as AsioSupportedOutputFormats, + }; pub use crate::host::wasapi::{ Device as WasapiDevice, Devices as WasapiDevices, @@ -526,6 +545,10 @@ mod platform_impl { SupportedOutputFormats as WasapiSupportedOutputFormats, }; + #[cfg(feature = "asio")] + impl_platform_host!(Asio asio, Wasapi wasapi); + + #[cfg(not(feature = "asio"))] impl_platform_host!(Wasapi wasapi); /// The default host for the current compilation target platform.