Skip to content
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `audio_thread_priority` dependency bumped to 0.35.
- `DeviceTrait` now requires `PartialEq + Eq + Hash + Debug + Display` with a stable device ID.
- Error messages are now consistent across all hosts.
- `DeviceId` fields are now private; construct with `DeviceId::new(host, id)` and access with
`.host()` / `.id()`. See [UPGRADING.md](UPGRADING.md) for migration details.
- `DeviceDescription::extended()` now returns `impl Iterator<Item = &str>` instead of `&[String]`.
See [UPGRADING.md](UPGRADING.md) for migration details.
- `DeviceDescriptionBuilder` setters now accept `impl AsRef<str>` instead of `impl Into<String>`.
- **AAudio**: Device names now include the device type suffix (e.g. "Speaker (Builtin Speaker)")
for easier identification when enumerating devices.
- **AAudio**: `supported_input_configs()` and `supported_output_configs()` now return an error for
Expand Down
60 changes: 60 additions & 0 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ This guide covers breaking changes requiring code updates. See [CHANGELOG.md](CH
`Stream::connect_to_system_inputs()`.
- [ ] **ALSA, CoreAudio, JACK**: Add an explicit `stream.play()` call after `build_*_stream()` if
you were relying on these backends to auto-start streams.
- [ ] Replace `DeviceId(host, string)` tuple construction with `DeviceId::new(host, id)`
- [ ] Replace `device_id.0` / `device_id.1` field access with `device_id.host()` / `device_id.id()`
- [ ] Update `DeviceDescription::extended()` call sites to iterate `&str` instead of `&[String]`
- [ ] If you implement a custom host, update `DeviceDescriptionBuilder` setter arguments from
`impl Into<String>` to `impl AsRef<str>`

## 1. Unified `Error` and `ErrorKind` type

Expand Down Expand Up @@ -247,6 +252,61 @@ If you must target `wasm32-unknown-emscripten` specifically, consider using Open

**Why:** The old `emscripten` host relied on deprecated Emscripten audio APIs that are no longer functional.

## 9. `DeviceId` is now opaque

**What changed:** The tuple fields of `DeviceId` are no longer `pub`. Direct struct construction
and field access are replaced by a typed API.

```rust
// Before (v0.17): direct tuple construction and field access
let id = DeviceId(HostId::Alsa, "hw:CARD=PCH,DEV=0".to_string());
let host = id.0;
let device_str = &id.1;

// After (v0.18): typed constructor and accessors
let id = DeviceId::new(HostId::Alsa, "hw:CARD=PCH,DEV=0");
let host = id.host();
let device_str = id.id();
```

The `Display` / `FromStr` round-trip for config persistence is **unchanged**:

```rust
// Serialize to a config file and restore on next launch — unchanged in v0.18
let id_string = device.id()?.to_string();
let id: DeviceId = id_string.parse()?;
let device = host.device_by_id(&id);
```

**Why:** The internal representation can change without breaking callers.

## 10. `DeviceDescription::extended()` returns an iterator

**What changed:** `extended()` now returns `impl Iterator<Item = &str>` instead of `&[String]`.

```rust
// Before (v0.17)
for line in desc.extended() { // &[String]
println!("{}", line); // line: &String
}

// After (v0.18)
for line in desc.extended() { // impl Iterator<Item = &str>
println!("{}", line); // line: &str — Display, write!, format! all unchanged
}
```

If you need random access or a collected copy, call `.collect()`:

```rust
let lines: Vec<&str> = desc.extended().collect();
println!("{}", lines[0]);
```

**Why:** Decouples the return type from the backing store, making future storage changes
non-breaking. The iterator yields `&str` directly, which is simpler than `&String` at every
call site.

---

# Upgrading from v0.16 to v0.17
Expand Down
36 changes: 18 additions & 18 deletions src/device_description.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub struct DeviceDescription {
address: Option<String>,

/// Additional description lines with non-structured, detailed information.
extended: Vec<String>,
extended: Box<[String]>,
}

/// Categorization of audio device types.
Expand Down Expand Up @@ -155,7 +155,7 @@ impl DeviceDescription {
///
/// This is always available and is the primary user-facing identifier.
pub fn name(&self) -> &str {
&self.name
self.name.as_str()
}

/// Returns the manufacturer/vendor name if available.
Expand Down Expand Up @@ -209,8 +209,8 @@ impl DeviceDescription {
}

/// Returns additional description lines with detailed information.
pub fn extended(&self) -> &[String] {
&self.extended
pub fn extended(&self) -> impl Iterator<Item = &str> {
self.extended.iter().map(String::as_str)
}
}

Expand Down Expand Up @@ -302,9 +302,9 @@ pub struct DeviceDescriptionBuilder {

impl DeviceDescriptionBuilder {
/// Creates a new builder with the device name (required).
pub fn new(name: impl Into<String>) -> Self {
pub fn new(name: impl AsRef<str>) -> Self {
Self {
name: name.into(),
name: name.as_ref().to_owned(),
manufacturer: None,
driver: None,
device_type: DeviceType::default(),
Expand All @@ -316,14 +316,14 @@ impl DeviceDescriptionBuilder {
}

/// Sets the manufacturer name.
pub fn manufacturer(mut self, manufacturer: impl Into<String>) -> Self {
self.manufacturer = Some(manufacturer.into());
pub fn manufacturer(mut self, manufacturer: impl AsRef<str>) -> Self {
self.manufacturer = Some(manufacturer.as_ref().to_owned());
self
}

/// Sets the driver name.
pub fn driver(mut self, driver: impl Into<String>) -> Self {
self.driver = Some(driver.into());
pub fn driver(mut self, driver: impl AsRef<str>) -> Self {
self.driver = Some(driver.as_ref().to_owned());
self
}

Expand All @@ -346,20 +346,20 @@ impl DeviceDescriptionBuilder {
}

/// Sets the physical address.
pub fn address(mut self, address: impl Into<String>) -> Self {
self.address = Some(address.into());
pub fn address(mut self, address: impl AsRef<str>) -> Self {
self.address = Some(address.as_ref().to_owned());
self
}

/// Sets the description lines.
pub fn extended(mut self, lines: Vec<String>) -> Self {
self.extended = lines;
/// Sets the description lines, replacing any previously added lines.
pub fn extended<S: AsRef<str>>(mut self, lines: impl IntoIterator<Item = S>) -> Self {
self.extended = lines.into_iter().map(|s| s.as_ref().to_owned()).collect();
self
}

/// Adds a single description line.
pub fn add_extended_line(mut self, line: impl Into<String>) -> Self {
self.extended.push(line.into());
pub fn add_extended_line(mut self, line: impl AsRef<str>) -> Self {
self.extended.push(line.as_ref().to_owned());
self
}

Expand All @@ -373,7 +373,7 @@ impl DeviceDescriptionBuilder {
interface_type: self.interface_type,
direction: self.direction,
address: self.address,
extended: self.extended,
extended: self.extended.into_boxed_slice(),
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/host/aaudio/java_interface/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,17 +59,17 @@ pub struct AudioDeviceInfo {
/**
* Available channel configurations
*/
pub channel_counts: Vec<i32>,
pub channel_counts: Box<[i32]>,

/**
* Supported sample rates
*/
pub sample_rates: Vec<i32>,
pub sample_rates: Box<[i32]>,

/**
* Supported audio formats
*/
pub formats: Vec<SampleFormat>,
pub formats: Box<[SampleFormat]>,
}

/**
Expand Down
8 changes: 5 additions & 3 deletions src/host/aaudio/java_interface/devices_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ fn try_request_devices_info<'j>(
let is_sink = call_method_no_args_ret_bool(env, &device, "isSink")?;
let direction = crate::device_description::direction_from_caps(is_source, is_sink);
let channel_counts =
call_method_no_args_ret_int_array(env, &device, "getChannelCounts")?;
let sample_rates = call_method_no_args_ret_int_array(env, &device, "getSampleRates")?;
call_method_no_args_ret_int_array(env, &device, "getChannelCounts")?
.into_boxed_slice();
let sample_rates = call_method_no_args_ret_int_array(env, &device, "getSampleRates")?
.into_boxed_slice();
let formats = call_method_no_args_ret_int_array(env, &device, "getEncodings")?
.into_iter()
.filter_map(SampleFormat::from_encoding)
.collect::<Vec<_>>();
.collect::<Box<[_]>>();

Ok(AudioDeviceInfo {
id,
Expand Down
18 changes: 9 additions & 9 deletions src/host/aaudio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,22 +234,22 @@ fn default_supported_configs() -> VecIntoIter<SupportedStreamConfigRange> {
}

fn device_supported_configs(device: &AudioDeviceInfo) -> VecIntoIter<SupportedStreamConfigRange> {
let sample_rates = if !device.sample_rates.is_empty() {
device.sample_rates.as_slice()
let sample_rates: &[i32] = if !device.sample_rates.is_empty() {
&device.sample_rates
} else {
&SAMPLE_RATES
};

const ALL_CHANNELS: [i32; 2] = [1, 2];
let channel_counts = if !device.channel_counts.is_empty() {
device.channel_counts.as_slice()
let channel_counts: &[i32] = if !device.channel_counts.is_empty() {
&device.channel_counts
} else {
&ALL_CHANNELS
};

const ALL_FORMATS: [SampleFormat; 2] = [SampleFormat::I16, SampleFormat::F32];
let formats = if !device.formats.is_empty() {
device.formats.as_slice()
let formats: &[SampleFormat] = if !device.formats.is_empty() {
&device.formats
} else {
&ALL_FORMATS
};
Expand Down Expand Up @@ -532,7 +532,7 @@ impl DeviceTrait for Device {

fn description(&self) -> Result<DeviceDescription, Error> {
match &self.0 {
None => Ok(DeviceDescriptionBuilder::new("Default Device".to_string()).build()),
None => Ok(DeviceDescriptionBuilder::new("Default Device").build()),
Some(info) => {
let device_type: DeviceType = info.device_type.into();
let name = match device_type {
Expand All @@ -546,7 +546,7 @@ impl DeviceTrait for Device {

// Add address if not empty
if !info.address.is_empty() {
builder = builder.address(info.address.clone());
builder = builder.address(&info.address);
}

Ok(builder.build())
Expand All @@ -559,7 +559,7 @@ impl DeviceTrait for Device {
None => "-1".to_string(), // Default device
Some(info) => info.id.to_string(),
};
Ok(DeviceId(crate::platform::HostId::AAudio, device_str))
Ok(DeviceId::new(crate::platform::HostId::AAudio, device_str))
}

fn supported_input_configs(&self) -> Result<Self::SupportedInputConfigs, Error> {
Expand Down
10 changes: 5 additions & 5 deletions src/host/alsa/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ impl Host {
// NULL IOID means both Input/Output. Whether a stream can actually open in a
// given direction can only be determined by attempting to open it.
let direction = hint.direction.map_or(DeviceDirection::Duplex, Into::into);
seen_pcm_ids.insert(pcm_id.clone());
let device = Device {
pcm_id,
desc: hint.desc,
direction,
_context: self.inner.clone(),
};

seen_pcm_ids.insert(device.pcm_id.clone());
devices.push(device);
}
}
Expand Down Expand Up @@ -105,7 +105,7 @@ fn physical_devices() -> Vec<PhysicalDevice> {
let card_name = ctl
.card_info()
.ok()
.and_then(|info| info.get_name().ok().map(|s| s.to_string()));
.and_then(|info| info.get_name().ok().map(|s| s.to_owned()));

for device_index in alsa::ctl::DeviceIter::new(&ctl) {
let device_index = device_index as u32;
Expand All @@ -117,15 +117,15 @@ fn physical_devices() -> Vec<PhysicalDevice> {
let (direction, device_name) = match (&playback_info, &capture_info) {
(Some(p_info), Some(_c_info)) => (
DeviceDirection::Duplex,
p_info.get_name().ok().map(|s| s.to_string()),
p_info.get_name().ok().map(|s| s.to_owned()),
),
(Some(p_info), None) => (
DeviceDirection::Output,
p_info.get_name().ok().map(|s| s.to_string()),
p_info.get_name().ok().map(|s| s.to_owned()),
),
(None, Some(c_info)) => (
DeviceDirection::Input,
c_info.get_name().ok().map(|s| s.to_string()),
c_info.get_name().ok().map(|s| s.to_owned()),
),
(None, None) => {
// Device doesn't exist - skip
Expand Down
18 changes: 6 additions & 12 deletions src/host/alsa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ impl HostTrait for Host {
}

fn device_by_id(&self, id: &DeviceId) -> Option<Self::Device> {
let canonical_id = DeviceId(id.0, canonical_pcm_id(&id.1));
let canonical_id = DeviceId::new(id.host(), canonical_pcm_id(id.id()));
self.devices()
.ok()?
.find(|d| d.id().ok().as_ref() == Some(&canonical_id))
Expand Down Expand Up @@ -431,27 +431,21 @@ impl Device {
.desc
.as_ref()
.and_then(|desc| desc.lines().next())
.unwrap_or(&self.pcm_id)
.to_string();
.unwrap_or(self.pcm_id.as_str());

let mut builder = DeviceDescriptionBuilder::new(name)
.driver(self.pcm_id.clone())
.driver(self.pcm_id.as_str())
.direction(self.direction);

if let Some(ref desc) = self.desc {
let lines = desc
.lines()
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect();
builder = builder.extended(lines);
builder = builder.extended(desc.lines().map(|l| l.trim()).filter(|l| !l.is_empty()));
}

Ok(builder.build())
}

fn id(&self) -> Result<DeviceId, Error> {
Ok(DeviceId(crate::platform::HostId::Alsa, self.pcm_id.clone()))
Ok(DeviceId::new(crate::platform::HostId::Alsa, &self.pcm_id))
}

fn supported_configs(
Expand Down Expand Up @@ -663,7 +657,7 @@ impl Default for Device {
// determine its actual capabilities without opening it, so we return Unknown direction.
Self {
pcm_id: DEFAULT_DEVICE.to_owned(),
desc: Some("Default Audio Device".to_string()),
desc: Some("Default Audio Device".to_owned()),
direction: DeviceDirection::Unknown,
_context: Arc::new(
AlsaContext::new().expect("Failed to initialize ALSA configuration"),
Expand Down
Loading
Loading