Merge pull request #292 from mitchmindtree/asio
Add ASIO Host for Windows (rebase and refactor of #221)
This commit is contained in:
commit
9095bbc00e
|
@ -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" }
|
||||
|
|
80
README.md
80
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.*
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
|
@ -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"
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
|
@ -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!");
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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"));
|
|
@ -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),
|
||||
}
|
||||
}};
|
||||
}
|
|
@ -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>());
|
||||
}
|
|
@ -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};
|
|
@ -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"),
|
||||
};
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")]
|
||||
|
|
|
@ -490,7 +490,6 @@ impl Device {
|
|||
format.sample_rate = SampleRate(rate as _);
|
||||
supported_formats.push(SupportedFormat::from(format.clone()));
|
||||
}
|
||||
|
||||
Ok(supported_formats.into_iter())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue