Merge pull request #292 from mitchmindtree/asio

Add ASIO Host for Windows (rebase and refactor of #221)
This commit is contained in:
mitchmindtree 2019-07-05 19:02:12 +02:00 committed by GitHub
commit 9095bbc00e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 2668 additions and 5 deletions

2
.gitignore vendored
View File

@ -2,4 +2,4 @@
/Cargo.lock /Cargo.lock
.cargo/ .cargo/
.DS_Store .DS_Store
recorded.wav recorded.wav

View File

@ -8,15 +8,20 @@ documentation = "https://docs.rs/cpal"
license = "Apache-2.0" license = "Apache-2.0"
keywords = ["audio", "sound"] keywords = ["audio", "sound"]
[features]
asio = ["asio-sys"] # Only available on Windows. See README for setup instructions.
[dependencies] [dependencies]
failure = "0.1.5" failure = "0.1.5"
lazy_static = "1.3" lazy_static = "1.3"
num-traits = "0.2.6"
[dev-dependencies] [dev-dependencies]
hound = "3.4" hound = "3.4"
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["audiosessiontypes", "audioclient", "coml2api", "combaseapi", "debug", "devpkey", "handleapi", "ksmedia", "mmdeviceapi", "objbase", "std", "synchapi", "winuser"] } 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] [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd"))'.dependencies]
alsa-sys = { version = "0.1", path = "alsa-sys" } alsa-sys = { version = "0.1", path = "alsa-sys" }

View File

@ -13,10 +13,10 @@ This library currently supports the following:
- Get the current default input and output stream formats for a device. - 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. - 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) - Linux (via ALSA)
- Windows - Windows (via WASAPI by default, see ASIO instructions below)
- macOS (via CoreAudio) - macOS (via CoreAudio)
- iOS (via CoreAudio) - iOS (via CoreAudio)
- Emscripten - Emscripten
@ -24,3 +24,79 @@ Currently supported backends include:
Note that on Linux, the ALSA development files are required. These are provided 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 as part of the `libasound2-dev` package on Debian and Ubuntu distributions and
`alsa-lib-devel` on Fedora. `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.*

2
asio-sys/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

15
asio-sys/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "asio-sys"
version = "0.1.0"
authors = ["Tom Gowan <tomrgowan@gmail.com>"]
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"

View File

@ -0,0 +1,29 @@
#include "helpers.hpp"
#include <stdio.h>
extern "C" ASIOError get_sample_rate(double * rate){
return ASIOGetSampleRate(reinterpret_cast<ASIOSampleRate *>(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);
}

View File

@ -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);

216
asio-sys/build.rs Normal file
View File

@ -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<PathBuf> = 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<PathBuf>| {
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!");
}

View File

@ -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);
}
}

11
asio-sys/examples/test.rs Normal file
View File

@ -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"));
}
}

View File

@ -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"));

View File

@ -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<AsioError> 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),
}
}};
}

View File

@ -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<Weak<DriverInner>>,
}
/// 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<DriverInner>,
}
// 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<Driver>` 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<DriverInner>` 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<DriverState>,
// 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<FnMut(i32) + Send>);
/// Input and Output streams.
///
/// There is only ever max one input and one output.
///
/// Only one is required.
pub struct AsioStreams {
pub input: Option<AsioStream>,
pub output: Option<AsioStream>,
}
/// A stream to ASIO.
///
/// Contains the buffers.
pub struct AsioStream {
/// A Double buffer per channel
pub buffer_infos: Vec<AsioBufferInfo>,
/// 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<Vec<Option<BufferCallback>>> = 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<String> {
// 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<Driver> {
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<Driver, LoadDriverError> {
// 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<Channels, AsioError> {
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<c_double, AsioError> {
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<bool, AsioError> {
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<AsioSampleType, AsioError> {
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<AsioSampleType, AsioError> {
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<c_long, AsioError> {
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<AsioBufferInfo>,
mut output_buffer_infos: Vec<AsioBufferInfo>,
) -> Result<AsioStreams, AsioError> {
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<AsioStream>,
num_channels: usize,
) -> Result<AsioStreams, AsioError> {
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<AsioStream>,
num_channels: usize,
) -> Result<AsioStreams, AsioError> {
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<F>(&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<bool, AsioError> {
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<AsioBufferInfo> {
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<BufferSizes, AsioError> {
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<ai::ASIOChannelInfo, AsioError> {
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<AsioSampleType, AsioError> {
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<str> {
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<str> {
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::<AsioSampleRate>(), std::mem::size_of::<ai::ASIOSampleRate>());
assert_eq!(std::mem::size_of::<AsioTimeCode>(), std::mem::size_of::<ai::ASIOTimeCode>());
assert_eq!(std::mem::size_of::<AsioTime>(), std::mem::size_of::<ai::ASIOTime>());
}

18
asio-sys/src/lib.rs Normal file
View File

@ -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};

12
build.rs Normal file
View File

@ -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"),
};
}

192
src/host/asio/device.rs Normal file
View File

@ -0,0 +1,192 @@
use std;
pub type SupportedInputFormats = std::vec::IntoIter<SupportedFormat>;
pub type SupportedOutputFormats = std::vec::IntoIter<SupportedFormat>;
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<sys::Driver>,
}
/// All available devices.
pub struct Devices {
asio: Arc<sys::Asio>,
drivers: std::vec::IntoIter<String>,
}
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<H: Hasher>(&self, state: &mut H) {
self.driver.name().hash(state);
}
}
impl Device {
pub fn name(&self) -> Result<String, DeviceNameError> {
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<SupportedInputFormats, SupportedFormatsError> {
// 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<SupportedOutputFormats, SupportedFormatsError> {
// 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<Format, DefaultFormatError> {
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<Format, DefaultFormatError> {
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<sys::Asio>) -> Result<Self, DevicesError> {
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<Device> {
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<usize>) {
unimplemented!()
}
}
pub(crate) fn convert_data_type(ty: &sys::AsioSampleType) -> Option<SampleFormat> {
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()
}
}
}

136
src/host/asio/mod.rs Normal file
View File

@ -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<sys::Asio>,
}
impl Host {
pub fn new() -> Result<Self, crate::HostUnavailable> {
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<Self::Devices, DevicesError> {
Devices::new(self.asio.clone())
}
fn default_input_device(&self) -> Option<Self::Device> {
// 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<Self::Device> {
// 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<String, DeviceNameError> {
Device::name(self)
}
fn supported_input_formats(&self) -> Result<Self::SupportedInputFormats, SupportedFormatsError> {
Device::supported_input_formats(self)
}
fn supported_output_formats(&self) -> Result<Self::SupportedOutputFormats, SupportedFormatsError> {
Device::supported_output_formats(self)
}
fn default_input_format(&self) -> Result<Format, DefaultFormatError> {
Device::default_input_format(self)
}
fn default_output_format(&self) -> Result<Format, DefaultFormatError> {
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<Self::StreamId, BuildStreamError> {
EventLoop::build_input_stream(self, device, format)
}
fn build_output_stream(
&self,
device: &Self::Device,
format: &Format,
) -> Result<Self::StreamId, BuildStreamError> {
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<F>(&self, callback: F) -> !
where
F: FnMut(Self::StreamId, StreamDataResult) + Send,
{
EventLoop::run(self, callback)
}
}
impl StreamIdTrait for StreamId {}

814
src/host/asio/stream.rs Normal file
View File

@ -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<Self, Output = Self> {}
/// Controls all streams
pub struct EventLoop {
/// The input and output ASIO streams
asio_streams: Arc<Mutex<sys::AsioStreams>>,
/// List of all CPAL streams
cpal_streams: Arc<Mutex<Vec<Option<Stream>>>>,
/// Total stream count.
stream_count: AtomicUsize,
/// The CPAL callback that the user gives to fill the buffers.
callbacks: Arc<Mutex<Option<&'static mut (FnMut(StreamId, StreamDataResult) + Send)>>>,
}
/// 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<sys::Driver>,
}
// 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<usize, BuildStreamError> {
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<usize, BuildStreamError> {
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<StreamId, BuildStreamError> {
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<A, B, F, G>(
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::<A>(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::<i16, i16, _, _>(
stream_id,
callback,
&mut interleaved,
asio_stream,
buffer_index as usize,
from_le,
std::convert::identity::<i16>,
);
}
(&sys::AsioSampleType::ASIOSTInt16MSB, SampleFormat::I16) => {
process_input_callback::<i16, i16, _, _>(
stream_id,
callback,
&mut interleaved,
asio_stream,
buffer_index as usize,
from_be,
std::convert::identity::<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::ASIOSTFloat32LSB, SampleFormat::F32) |
(&sys::AsioSampleType::ASIOSTFloat32MSB, SampleFormat::F32) => {
process_input_callback::<f32, f32, _, _>(
stream_id,
callback,
&mut interleaved,
asio_stream,
buffer_index as usize,
std::convert::identity::<f32>,
std::convert::identity::<f32>,
);
}
// 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::<i32, i16, _, _>(
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::<i32, i16, _, _>(
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::<f64, f32, _, _>(
stream_id,
callback,
&mut interleaved,
asio_stream,
buffer_index as usize,
std::convert::identity::<f64>,
|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<StreamId, BuildStreamError> {
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<A, B, F, G>(
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::<B>(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::<B>(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::<i16, i16, _, _>(
stream_id,
callback,
&mut interleaved,
silence,
asio_stream,
buffer_index as usize,
std::convert::identity::<i16>,
to_le,
);
}
(SampleFormat::I16, &sys::AsioSampleType::ASIOSTInt16MSB) => {
process_output_callback::<i16, i16, _, _>(
stream_id,
callback,
&mut interleaved,
silence,
asio_stream,
buffer_index as usize,
std::convert::identity::<i16>,
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::<f32, f32, _, _>(
stream_id,
callback,
&mut interleaved,
silence,
asio_stream,
buffer_index as usize,
std::convert::identity::<f32>,
std::convert::identity::<f32>,
);
}
// 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::<i16, i32, _, _>(
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::<i16, i32, _, _>(
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::<f32, f64, _, _>(
stream_id,
callback,
&mut interleaved,
silence,
asio_stream,
buffer_index as usize,
|s| s as f64,
std::convert::identity::<f64>,
);
}
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<F>(&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<T>(v: &mut [u8]) -> &mut [T] {
debug_assert!(v.len() % std::mem::size_of::<T>() == 0);
std::slice::from_raw_parts_mut(v.as_mut_ptr() as *mut T, v.len() / std::mem::size_of::<T>())
}
/// Helper function to convert to little endianness.
fn to_le<T: PrimInt>(t: T) -> T {
t.to_le()
}
/// Helper function to convert to big endianness.
fn to_be<T: PrimInt>(t: T) -> T {
t.to_be()
}
/// Helper function to convert from little endianness.
fn from_le<T: PrimInt>(t: T) -> T {
T::from_le(t)
}
/// Helper function to convert from little endianness.
fn from_be<T: PrimInt>(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<T>(
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<T>(
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()
}
}
}

View File

@ -1,5 +1,7 @@
#[cfg(any(target_os = "linux", target_os = "freebsd"))] #[cfg(any(target_os = "linux", target_os = "freebsd"))]
pub(crate) mod alsa; pub(crate) mod alsa;
#[cfg(all(windows, feature = "asio"))]
pub(crate) mod asio;
#[cfg(any(target_os = "macos", target_os = "ios"))] #[cfg(any(target_os = "macos", target_os = "ios"))]
pub(crate) mod coreaudio; pub(crate) mod coreaudio;
#[cfg(target_os = "emscripten")] #[cfg(target_os = "emscripten")]

View File

@ -490,7 +490,6 @@ impl Device {
format.sample_rate = SampleRate(rate as _); format.sample_rate = SampleRate(rate as _);
supported_formats.push(SupportedFormat::from(format.clone())); supported_formats.push(SupportedFormat::from(format.clone()));
} }
Ok(supported_formats.into_iter()) Ok(supported_formats.into_iter())
} }
} }

View File

@ -377,6 +377,9 @@ pub enum BuildStreamError {
/// them immediately. /// them immediately.
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
pub enum PlayStreamError { 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. /// See the `BackendSpecificError` docs for more information about this error variant.
#[fail(display = "{}", err)] #[fail(display = "{}", err)]
BackendSpecific { BackendSpecific {
@ -392,6 +395,9 @@ pub enum PlayStreamError {
/// them immediately. /// them immediately.
#[derive(Debug, Fail)] #[derive(Debug, Fail)]
pub enum PauseStreamError { 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. /// See the `BackendSpecificError` docs for more information about this error variant.
#[fail(display = "{}", err)] #[fail(display = "{}", err)]
BackendSpecific { BackendSpecific {

View File

@ -256,6 +256,7 @@ macro_rules! impl_platform_host {
type StreamId = StreamId; type StreamId = StreamId;
type Device = Device; type Device = Device;
#[allow(unreachable_patterns)]
fn build_input_stream( fn build_input_stream(
&self, &self,
device: &Self::Device, device: &Self::Device,
@ -269,9 +270,11 @@ macro_rules! impl_platform_host {
.map(StreamId) .map(StreamId)
} }
)* )*
_ => panic!("tried to build a stream with a device from another host"),
} }
} }
#[allow(unreachable_patterns)]
fn build_output_stream( fn build_output_stream(
&self, &self,
device: &Self::Device, device: &Self::Device,
@ -285,9 +288,11 @@ macro_rules! impl_platform_host {
.map(StreamId) .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> { fn play_stream(&self, stream: Self::StreamId) -> Result<(), crate::PlayStreamError> {
match (&self.0, stream.0) { match (&self.0, stream.0) {
$( $(
@ -295,9 +300,11 @@ macro_rules! impl_platform_host {
e.play_stream(s.clone()) 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> { fn pause_stream(&self, stream: Self::StreamId) -> Result<(), crate::PauseStreamError> {
match (&self.0, stream.0) { match (&self.0, stream.0) {
$( $(
@ -305,9 +312,11 @@ macro_rules! impl_platform_host {
e.pause_stream(s.clone()) 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) { fn destroy_stream(&self, stream: Self::StreamId) {
match (&self.0, stream.0) { match (&self.0, stream.0) {
$( $(
@ -315,6 +324,7 @@ macro_rules! impl_platform_host {
e.destroy_stream(s.clone()) 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)] #[cfg(windows)]
mod platform_impl { 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::{ pub use crate::host::wasapi::{
Device as WasapiDevice, Device as WasapiDevice,
Devices as WasapiDevices, Devices as WasapiDevices,
@ -526,6 +545,10 @@ mod platform_impl {
SupportedOutputFormats as WasapiSupportedOutputFormats, SupportedOutputFormats as WasapiSupportedOutputFormats,
}; };
#[cfg(feature = "asio")]
impl_platform_host!(Asio asio, Wasapi wasapi);
#[cfg(not(feature = "asio"))]
impl_platform_host!(Wasapi wasapi); impl_platform_host!(Wasapi wasapi);
/// The default host for the current compilation target platform. /// The default host for the current compilation target platform.