Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Added `DeviceTrait::input_channel_descriptions()` and `DeviceTrait::output_channel_descriptions()`,
returning per-channel metadata index-aligned with the channels of a stream built on the device.
Both have a default implementation returning an empty `Vec`, so existing custom hosts are
unaffected.
- Added `ChannelDescription` (an optional driver-supplied `name` and an optional `position`) and
`ChannelPosition` (the WAVE `dwChannelMask` speaker positions).
- **ASIO**: `output_channel_descriptions()` / `input_channel_descriptions()` report the driver's
per-channel names. For aggregating drivers such as ASIO4ALL the name is the only stable handle on
a channel, since enabling another device renumbers everything after it.
- **WASAPI**: `output_channel_descriptions()` / `input_channel_descriptions()` report speaker
positions expanded from the mix format's `dwChannelMask`.
- `examples/enumerate.rs` now prints each channel's index, name and position.
- **AAudio**: Xruns are now reported as `ErrorKind::Xrun`.
- **CoreAudio**: Xruns are now reported as `ErrorKind::Xrun`.
- **PipeWire**: Xruns are now reported as `ErrorKind::Xrun`.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ windows = { version = "0.62", features = [
"Win32_UI_Shell_PropertiesSystem",
] }
audio_thread_priority = { version = "0.35", optional = true, default-features = false }
asio-sys = { version = "0.3.0", path = "asio-sys", optional = true }
asio-sys = { version = "0.3.1", path = "asio-sys", optional = true }
num-traits = { version = "0.2", optional = true }
jack = { version = "0.13.5", optional = true }

Expand Down
13 changes: 13 additions & 0 deletions asio-sys/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.3.1] - 2026-08-10

### Added
- Added `ChannelInfo`, describing a single channel: its index, direction, active flag, channel
group, sample type and driver-assigned name. Marked `#[non_exhaustive]` so further fields can
be added without a breaking change
- Added `Driver::input_channel_info()` and `Driver::output_channel_info()` to query one channel,
and `Driver::input_channel_infos()` / `Driver::output_channel_infos()` to query every channel in
a direction. Channel names are the only stable way to identify a channel on aggregating drivers
such as ASIO4ALL, whose channel count and ordering change with its control-panel selection
- `AsioSampleType` now derives `Clone`, `Copy`, `Eq`, `Hash` and `PartialEq`

## [0.3.0] - 2026-06-06

### Added
Expand Down Expand Up @@ -125,6 +137,7 @@ Initial release.
- Support for MSVC toolchain on Windows
- Basic error types: `AsioError`, `LoadDriverError`

[0.3.1]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.3.0...asio-sys-v0.3.1
[0.3.0]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.6...asio-sys-v0.3.0
[0.2.6]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.5...asio-sys-v0.2.6
[0.2.5]: https://github.com/RustAudio/cpal/compare/asio-sys-v0.2.4...asio-sys-v0.2.5
Expand Down
2 changes: 1 addition & 1 deletion asio-sys/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "asio-sys"
version = "0.3.0"
version = "0.3.1"
authors = ["Tom Gowan <tomrgowan@gmail.com>"]
description = "Low-level interface and binding generation for the steinberg ASIO SDK."
repository = "https://github.com/RustAudio/cpal/"
Expand Down
30 changes: 30 additions & 0 deletions asio-sys/examples/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,36 @@ fn main() {
data_type: SampleFormat::F32,
};
println!(" Input {:?}", in_fmt);
print_channels(
&driver
.input_channel_infos()
.expect("failed to retrieve input channel info"),
);
println!(" Output {:?}", out_fmt);
print_channels(
&driver
.output_channel_infos()
.expect("failed to retrieve output channel info"),
);
}
}

fn print_channels(infos: &[sys::ChannelInfo]) {
for info in infos {
// An unnamed channel is legal, but an empty name here is also what a botched string
// decode would look like, so call it out rather than printing a blank. Quote real names
// ourselves so the placeholder is not mistaken for one.
let name = if info.name.is_empty() {
"<unnamed>".to_string()
} else {
format!("{:?}", info.name)
};
println!(
" {}: {} (group {}, {})",
info.channel,
name,
info.channel_group,
if info.is_active { "active" } else { "inactive" },
);
}
}
144 changes: 137 additions & 7 deletions asio-sys/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,33 @@ pub struct Channels {
pub outs: i32,
}

/// Information about a single input or output channel of a driver.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub struct ChannelInfo {
/// Index of this channel within its direction (0-based).
pub channel: i32,
/// `true` for an input channel, `false` for an output channel.
pub is_input: bool,
/// Whether the driver currently has this channel active (buffers created for it).
pub is_active: bool,
/// Driver-defined grouping; channels of one physical device usually share a group.
pub channel_group: i32,
/// Sample format of this channel. `None` if the driver reported a value this crate does
/// not know — the channel is still usable and still has a name.
pub sample_type: Option<AsioSampleType>,
/// Human-readable name, e.g. "Realtek HD Audio output 1".
///
/// May be empty: a driver is permitted to leave a channel unnamed.
///
/// The ASIO SDK does not specify an encoding for this field and drivers write it in the
/// system code page, which this crate decodes as UTF-8 with replacement characters. A name
/// containing non-ASCII characters is therefore mangled, and two channels whose names differ
/// only in such characters can decode to the same string — so do not rely on the name alone
/// being unique. ASCII names, which is what drivers overwhelmingly use, are exact.
pub name: String,
}

/// Hardware latency in frames for the input and output streams.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Latencies {
Expand Down Expand Up @@ -149,7 +176,7 @@ pub struct AsioStream {
/// All the possible types from ASIO.
/// This is a direct copy of the ASIOSampleType
/// inside ASIO SDK.
#[derive(Debug, FromPrimitive)]
#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, PartialEq)]
#[repr(C)]
pub enum AsioSampleType {
ASIOSTInt16MSB = 0,
Expand Down Expand Up @@ -504,12 +531,51 @@ impl Driver {
/// Returns the number of input and output channels available on the driver.
pub fn channels(&self) -> Result<Channels, AsioError> {
let _guard = self.inner.lock_state();
let mut ins: c_long = 0;
let mut outs: c_long = 0;
unsafe {
asio_result!(ai::ASIOGetChannels(&mut ins, &mut outs))?;
}
Ok(Channels { ins, outs })
asio_channels()
}

/// Information about a single input channel.
///
/// Returns `AsioError::InvalidInput` if `index` is not below `channels()?.ins`. That is also
/// the error a driver reports for a parameter it rejects, so the two are indistinguishable.
///
/// Each call queries the channel count as well as the channel, so prefer
/// [`Driver::input_channel_infos`] over calling this in a loop.
pub fn input_channel_info(&self, index: i32) -> Result<ChannelInfo, AsioError> {
let _guard = self.inner.lock_state();
let channels = asio_channels()?;
checked_channel_info(index, true, channels.ins)
}

/// Information about a single output channel.
///
/// Returns `AsioError::InvalidInput` if `index` is not below `channels()?.outs`. That is also
/// the error a driver reports for a parameter it rejects, so the two are indistinguishable.
///
/// Each call queries the channel count as well as the channel, so prefer
/// [`Driver::output_channel_infos`] over calling this in a loop.
pub fn output_channel_info(&self, index: i32) -> Result<ChannelInfo, AsioError> {
let _guard = self.inner.lock_state();
let channels = asio_channels()?;
checked_channel_info(index, false, channels.outs)
}

/// Information for every input channel, in index order.
pub fn input_channel_infos(&self) -> Result<Vec<ChannelInfo>, AsioError> {
let _guard = self.inner.lock_state();
let channels = asio_channels()?;
(0..channels.ins)
.map(|index| checked_channel_info(index, true, channels.ins))
.collect()
}

/// Information for every output channel, in index order.
pub fn output_channel_infos(&self) -> Result<Vec<ChannelInfo>, AsioError> {
let _guard = self.inner.lock_state();
let channels = asio_channels()?;
(0..channels.outs)
.map(|index| checked_channel_info(index, false, channels.outs))
.collect()
}

/// Get the input and output hardware latency in frames.
Expand Down Expand Up @@ -1075,6 +1141,70 @@ fn asio_channel_info(channel: c_long, is_input: bool) -> Result<ai::ASIOChannelI
}
}

/// Retrieve the number of input and output channels from the currently loaded driver.
///
/// The caller must hold the `DriverState` lock.
fn asio_channels() -> Result<Channels, AsioError> {
let mut ins: c_long = 0;
let mut outs: c_long = 0;
unsafe {
asio_result!(ai::ASIOGetChannels(&mut ins, &mut outs))?;
}
Ok(Channels { ins, outs })
}

/// Retrieve the `ChannelInfo` for the given channel, having first checked the index against the
/// number of channels in that direction.
///
/// Drivers are not required to validate the index and some read out of range, so the bounds check
/// must happen before we call into the SDK.
///
/// The caller must hold the `DriverState` lock.
fn checked_channel_info(index: i32, is_input: bool, count: i32) -> Result<ChannelInfo, AsioError> {
if index < 0 || index >= count {
return Err(AsioError::InvalidInput);
}
Ok(ChannelInfo::from_raw(
index,
is_input,
asio_channel_info(index, is_input)?,
))
}

impl ChannelInfo {
/// Build a `ChannelInfo` from the struct the driver filled in for channel `index` in the
/// direction given by `is_input`.
fn from_raw(index: i32, is_input: bool, info: ai::ASIOChannelInfo) -> Self {
// The name is NUL-terminated only by convention: a driver may fill all 32 bytes without a
// terminator, so stop at the first NUL *or* the end of the array. The SDK does not specify
// an encoding and drivers write the system code page, which we decode as UTF-8 lossily:
// doing it properly needs `MultiByteToWideChar`, and this crate has no Windows API
// dependency. See the caveat on `ChannelInfo::name`. `c_char` is `i8` on the MSVC target,
// hence the cast.
let bytes: Vec<u8> = info
.name
.iter()
.take_while(|&&c| c != 0)
.map(|&c| c as u8)
.collect();
// Several drivers pad the name with trailing spaces.
let name = String::from_utf8_lossy(&bytes).trim_end().to_string();
ChannelInfo {
// `channel` and `isInput` are inputs to `ASIOGetChannelInfo` that the driver is under
// no obligation to preserve, so report what we asked for rather than what came back.
channel: index,
is_input,
// `isActive` is an `ASIOBool`, for which the SDK only guarantees zero/non-zero.
is_active: info.isActive != 0,
channel_group: info.channelGroup,
// Unlike `stream_data_type`, an unrecognised sample type must not panic: the caller is
// most likely after the channel's name, which is valid regardless.
sample_type: FromPrimitive::from_i32(info.type_),
name,
}
}
}

/// Retrieve the data type of either the input or output stream.
///
/// If `is_input` is true, this will be queried on the input stream.
Expand Down
43 changes: 42 additions & 1 deletion examples/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,44 @@
//! - Retrieving device IDs for persistent identification
//! - Getting device descriptions with metadata
//! - Listing supported input and output stream configurations
//! - Printing per-channel names and speaker positions where the host reports them
//!
//! Run with: `cargo run --example enumerate`

extern crate anyhow;
extern crate cpal;

use cpal::traits::{DeviceTrait, HostTrait};
use cpal::{
traits::{DeviceTrait, HostTrait},
ChannelDescription,
};

/// Prints one line per channel: its index, and whatever the host knows about it.
///
/// An empty list is the normal result for a host with no per-channel metadata (ALSA, JACK, ...),
/// so nothing is printed at all in that case.
fn print_channel_descriptions(label: &str, descriptions: &[ChannelDescription]) {
if descriptions.is_empty() {
return;
}
println!(" {label} channels:");
for (index, description) in descriptions.iter().enumerate() {
let name = description
.name
.as_deref()
.map_or_else(|| "<none>".to_string(), |n| format!("{n:?}"));
let position = description
.position
.map_or_else(|| "<none>".to_string(), |p| p.to_string());
println!(" {index}. name: {name}, position: {position}");

// A driver may legally leave a channel unnamed, but an empty name where one was
// expected is also what a decoding bug looks like — so say when it happens.
if description.name.is_none() && description.position.is_none() {
println!(" (host reported neither a name nor a position)");
}
}
}

fn main() -> Result<(), anyhow::Error> {
// To print raw ALSA errors to stderr during enumeration, comment out the line below:
Expand Down Expand Up @@ -50,6 +81,16 @@ fn main() -> Result<(), anyhow::Error> {
println!(" {}. {id}", device_index + 1);
}

// Per-channel metadata
match device.input_channel_descriptions() {
Ok(descriptions) => print_channel_descriptions("Input", &descriptions),
Err(e) => println!(" Error getting input channel descriptions: {e:?}"),
}
match device.output_channel_descriptions() {
Ok(descriptions) => print_channel_descriptions("Output", &descriptions),
Err(e) => println!(" Error getting output channel descriptions: {e:?}"),
}

// Input configs
if let Ok(conf) = device.default_input_config() {
println!(" Default input stream config:\n {conf:?}");
Expand Down
Loading
Loading