diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d27b4b6f..afbe164d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` instead of `&[String]`. + See [UPGRADING.md](UPGRADING.md) for migration details. +- `DeviceDescriptionBuilder` setters now accept `impl AsRef` instead of `impl Into`. - **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 diff --git a/UPGRADING.md b/UPGRADING.md index 45e103c32..bb85eb697 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -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` to `impl AsRef` ## 1. Unified `Error` and `ErrorKind` type @@ -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` 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 + 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 diff --git a/src/device_description.rs b/src/device_description.rs index c99bb240a..f797aa00b 100644 --- a/src/device_description.rs +++ b/src/device_description.rs @@ -36,7 +36,7 @@ pub struct DeviceDescription { address: Option, /// Additional description lines with non-structured, detailed information. - extended: Vec, + extended: Box<[String]>, } /// Categorization of audio device types. @@ -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. @@ -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 { + self.extended.iter().map(String::as_str) } } @@ -302,9 +302,9 @@ pub struct DeviceDescriptionBuilder { impl DeviceDescriptionBuilder { /// Creates a new builder with the device name (required). - pub fn new(name: impl Into) -> Self { + pub fn new(name: impl AsRef) -> Self { Self { - name: name.into(), + name: name.as_ref().to_owned(), manufacturer: None, driver: None, device_type: DeviceType::default(), @@ -316,14 +316,14 @@ impl DeviceDescriptionBuilder { } /// Sets the manufacturer name. - pub fn manufacturer(mut self, manufacturer: impl Into) -> Self { - self.manufacturer = Some(manufacturer.into()); + pub fn manufacturer(mut self, manufacturer: impl AsRef) -> Self { + self.manufacturer = Some(manufacturer.as_ref().to_owned()); self } /// Sets the driver name. - pub fn driver(mut self, driver: impl Into) -> Self { - self.driver = Some(driver.into()); + pub fn driver(mut self, driver: impl AsRef) -> Self { + self.driver = Some(driver.as_ref().to_owned()); self } @@ -346,20 +346,20 @@ impl DeviceDescriptionBuilder { } /// Sets the physical address. - pub fn address(mut self, address: impl Into) -> Self { - self.address = Some(address.into()); + pub fn address(mut self, address: impl AsRef) -> Self { + self.address = Some(address.as_ref().to_owned()); self } - /// Sets the description lines. - pub fn extended(mut self, lines: Vec) -> Self { - self.extended = lines; + /// Sets the description lines, replacing any previously added lines. + pub fn extended>(mut self, lines: impl IntoIterator) -> 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) -> Self { - self.extended.push(line.into()); + pub fn add_extended_line(mut self, line: impl AsRef) -> Self { + self.extended.push(line.as_ref().to_owned()); self } @@ -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(), } } } diff --git a/src/host/aaudio/java_interface/definitions.rs b/src/host/aaudio/java_interface/definitions.rs index f6438d007..0c4e4251a 100644 --- a/src/host/aaudio/java_interface/definitions.rs +++ b/src/host/aaudio/java_interface/definitions.rs @@ -59,17 +59,17 @@ pub struct AudioDeviceInfo { /** * Available channel configurations */ - pub channel_counts: Vec, + pub channel_counts: Box<[i32]>, /** * Supported sample rates */ - pub sample_rates: Vec, + pub sample_rates: Box<[i32]>, /** * Supported audio formats */ - pub formats: Vec, + pub formats: Box<[SampleFormat]>, } /** diff --git a/src/host/aaudio/java_interface/devices_info.rs b/src/host/aaudio/java_interface/devices_info.rs index d74fb7090..ea87dc533 100644 --- a/src/host/aaudio/java_interface/devices_info.rs +++ b/src/host/aaudio/java_interface/devices_info.rs @@ -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::>(); + .collect::>(); Ok(AudioDeviceInfo { id, diff --git a/src/host/aaudio/mod.rs b/src/host/aaudio/mod.rs index 1abf86870..b31ed8268 100644 --- a/src/host/aaudio/mod.rs +++ b/src/host/aaudio/mod.rs @@ -234,22 +234,22 @@ fn default_supported_configs() -> VecIntoIter { } fn device_supported_configs(device: &AudioDeviceInfo) -> VecIntoIter { - 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 }; @@ -532,7 +532,7 @@ impl DeviceTrait for Device { fn description(&self) -> Result { 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 { @@ -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()) @@ -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 { diff --git a/src/host/alsa/enumerate.rs b/src/host/alsa/enumerate.rs index 1e5405928..d9bd648e0 100644 --- a/src/host/alsa/enumerate.rs +++ b/src/host/alsa/enumerate.rs @@ -38,6 +38,7 @@ 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, @@ -45,7 +46,6 @@ impl Host { _context: self.inner.clone(), }; - seen_pcm_ids.insert(device.pcm_id.clone()); devices.push(device); } } @@ -105,7 +105,7 @@ fn physical_devices() -> Vec { 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; @@ -117,15 +117,15 @@ fn physical_devices() -> Vec { 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 diff --git a/src/host/alsa/mod.rs b/src/host/alsa/mod.rs index 2ab69e5b8..f967811f4 100644 --- a/src/host/alsa/mod.rs +++ b/src/host/alsa/mod.rs @@ -131,7 +131,7 @@ impl HostTrait for Host { } fn device_by_id(&self, id: &DeviceId) -> Option { - 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)) @@ -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 { - Ok(DeviceId(crate::platform::HostId::Alsa, self.pcm_id.clone())) + Ok(DeviceId::new(crate::platform::HostId::Alsa, &self.pcm_id)) } fn supported_configs( @@ -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"), diff --git a/src/host/asio/device.rs b/src/host/asio/device.rs index 925a142dd..713192cd0 100644 --- a/src/host/asio/device.rs +++ b/src/host/asio/device.rs @@ -25,7 +25,7 @@ pub struct Device { buffer_size_max: FrameCount, input_sample_format: Option, output_sample_format: Option, - supported_sample_rates: Vec, + supported_sample_rates: Box<[SampleRate]>, // Input and/or Output stream. // A driver can only have one of each. @@ -48,14 +48,14 @@ impl Device { Some(self.channels_out), ); - Ok(DeviceDescriptionBuilder::new(self.name.clone()) - .driver(self.name.clone()) + Ok(DeviceDescriptionBuilder::new(&self.name) + .driver(&self.name) .direction(direction) .build()) } pub fn id(&self) -> Result { - Ok(DeviceId(crate::platform::HostId::Asio, self.name.clone())) + Ok(DeviceId::new(crate::platform::HostId::Asio, &self.name)) } /// Gets the supported input configs. @@ -207,7 +207,7 @@ impl Iterator for Devices { .ok() .and_then(|t| convert_data_type(&t)); - let supported_sample_rates: Vec = crate::COMMON_SAMPLE_RATES + let supported_sample_rates: Box<[SampleRate]> = crate::COMMON_SAMPLE_RATES .iter() .copied() .filter(|&r| driver.can_sample_rate(r.into()).unwrap_or(false)) diff --git a/src/host/audioworklet/mod.rs b/src/host/audioworklet/mod.rs index 2fa05dfb1..1b6491596 100644 --- a/src/host/audioworklet/mod.rs +++ b/src/host/audioworklet/mod.rs @@ -119,15 +119,15 @@ impl DeviceTrait for Device { type Stream = Stream; fn description(&self) -> Result { - Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + Ok(DeviceDescriptionBuilder::new("Default Device") .direction(DeviceDirection::Output) .build()) } fn id(&self) -> Result { - Ok(DeviceId( + Ok(DeviceId::new( crate::platform::HostId::AudioWorklet, - "default".to_string(), + "default", )) } diff --git a/src/host/coreaudio/ios/mod.rs b/src/host/coreaudio/ios/mod.rs index 50b75b9dd..cc54c2dcf 100644 --- a/src/host/coreaudio/ios/mod.rs +++ b/src/host/coreaudio/ios/mod.rs @@ -87,16 +87,13 @@ impl Device { crate::device_description::direction_from_counts(input_channels, output_channels) }; - Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + Ok(DeviceDescriptionBuilder::new("Default Device") .direction(direction) .build()) } fn id(&self) -> Result { - Ok(DeviceId( - crate::platform::HostId::CoreAudio, - "default".to_string(), - )) + Ok(DeviceId::new(crate::platform::HostId::CoreAudio, "default")) } fn supported_input_configs(&self) -> Result { diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs index 0df7d3406..077019abb 100644 --- a/src/host/coreaudio/macos/device.rs +++ b/src/host/coreaudio/macos/device.rs @@ -456,7 +456,10 @@ impl Device { // We now check if the returned uid is non-null before use. if !uid.is_null() { let uid_string = unsafe { CFString::wrap_under_create_rule(uid).to_string() }; - Ok(DeviceId(crate::platform::HostId::CoreAudio, uid_string)) + Ok(DeviceId::new( + crate::platform::HostId::CoreAudio, + uid_string, + )) } else { Err(ErrorKind::DeviceNotAvailable.into()) } diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 411040f98..3dac8451e 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -43,7 +43,7 @@ impl Device { Ok(Self { // The name given to the client by JACK, could potentially be different from the name // supplied e.g. if there is a name collision - name: client.name().to_string(), + name: client.name().to_owned(), sample_rate: client.sample_rate(), buffer_size: SupportedBufferSize::Range { min: client.buffer_size(), @@ -56,7 +56,7 @@ impl Device { } fn id(&self) -> Result { - Ok(DeviceId(crate::platform::HostId::Jack, self.name.clone())) + Ok(DeviceId::new(crate::platform::HostId::Jack, &self.name)) } pub fn default_output_device( diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index fb05d63c6..de6ea0f46 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -38,8 +38,8 @@ pub struct Stream { state: Arc, async_client: jack::AsyncClient, // Port names are stored in order to connect them to other ports in jack automatically - input_port_names: Vec, - output_port_names: Vec, + input_port_names: Box<[String]>, + output_port_names: Box<[String]>, } // Compile-time assertion that Stream is Send and Sync @@ -98,8 +98,8 @@ impl Stream { Ok(Self { state, async_client, - input_port_names: port_names, - output_port_names: vec![], + input_port_names: port_names.into_boxed_slice(), + output_port_names: Default::default(), }) } @@ -154,8 +154,8 @@ impl Stream { Ok(Self { state, async_client, - input_port_names: vec![], - output_port_names: port_names, + input_port_names: Box::default(), + output_port_names: port_names.into_boxed_slice(), }) } diff --git a/src/host/null/mod.rs b/src/host/null/mod.rs index c8acbddf1..37eb1c650 100644 --- a/src/host/null/mod.rs +++ b/src/host/null/mod.rs @@ -62,7 +62,7 @@ impl DeviceTrait for Device { } fn id(&self) -> Result { - Ok(DeviceId(crate::platform::HostId::Null, String::new())) + Ok(DeviceId::new(crate::platform::HostId::Null, "")) } fn supported_input_configs(&self) -> Result { diff --git a/src/host/pipewire/device.rs b/src/host/pipewire/device.rs index 44c9785c0..dd67af326 100644 --- a/src/host/pipewire/device.rs +++ b/src/host/pipewire/device.rs @@ -71,7 +71,7 @@ pub struct Device { direction: DeviceDirection, channels: ChannelCount, rate: SampleRate, - allow_rates: Vec, + allow_rates: Arc<[SampleRate]>, quantum: FrameCount, min_quantum: FrameCount, max_quantum: FrameCount, @@ -208,7 +208,7 @@ impl DeviceTrait for Device { type SupportedOutputConfigs = SupportedOutputConfigs; fn id(&self) -> Result { - Ok(DeviceId(HostId::PipeWire, self.node_name.clone())) + Ok(DeviceId::new(HostId::PipeWire, &self.node_name)) } fn description(&self) -> Result { @@ -246,10 +246,10 @@ impl DeviceTrait for Device { if !self.supports_input() { return Ok(vec![].into_iter()); } - let rates = if self.allow_rates.is_empty() { - vec![self.rate] + let rates: &[SampleRate] = if self.allow_rates.is_empty() { + &[self.rate] } else { - self.allow_rates.clone() + &self.allow_rates }; Ok(rates .iter() @@ -274,10 +274,10 @@ impl DeviceTrait for Device { if !self.supports_output() { return Ok(vec![].into_iter()); } - let rates = if self.allow_rates.is_empty() { - vec![self.rate] + let rates: &[SampleRate] = if self.allow_rates.is_empty() { + &[self.rate] } else { - self.allow_rates.clone() + &self.allow_rates }; Ok(rates .iter() @@ -671,7 +671,7 @@ impl DeviceTrait for Device { #[derive(Debug, Clone, Default)] struct Settings { rate: SampleRate, - allow_rates: Vec, + allow_rates: Box<[SampleRate]>, quantum: FrameCount, min_quantum: FrameCount, max_quantum: FrameCount, @@ -798,7 +798,8 @@ pub fn init_devices(connect_automatically: Arc) -> Option { let Ok(quantum) = quantum.parse() else { @@ -1027,9 +1028,10 @@ pub fn init_devices(connect_automatically: Arc) -> Option = Arc::from(settings.allow_rates.as_ref()); for device in devices.iter_mut() { device.rate = settings.rate; - device.allow_rates = settings.allow_rates.clone(); + device.allow_rates = Arc::clone(&shared_rates); device.quantum = settings.quantum; device.min_quantum = settings.min_quantum; device.max_quantum = settings.max_quantum; @@ -1044,7 +1046,7 @@ pub fn init_devices(connect_automatically: Arc) -> Option) -> Option Option> { - let list: Vec<&str> = list - .trim() + list.trim() .strip_prefix("[")? .strip_suffix("]")? - .split(' ') - .flat_map(|s| s.split(',')) + .split([' ', ',']) .filter(|s| !s.is_empty()) - .collect(); - let mut allow_rates = vec![]; - for rate in list { - let rate = rate.parse().ok()?; - allow_rates.push(rate); - } - Some(allow_rates) + .map(|s| s.parse().ok()) + .collect() } #[cfg(test)] diff --git a/src/host/pulseaudio/mod.rs b/src/host/pulseaudio/mod.rs index bf1187d52..e440465a8 100644 --- a/src/host/pulseaudio/mod.rs +++ b/src/host/pulseaudio/mod.rs @@ -476,7 +476,7 @@ impl DeviceTrait for Device { Device::Source { info, .. } => info.index, }; - Ok(DeviceId(HostId::PulseAudio, id.to_string())) + Ok(DeviceId::new(HostId::PulseAudio, id.to_string())) } } diff --git a/src/host/wasapi/device.rs b/src/host/wasapi/device.rs index 6e7ae7359..f54096aae 100644 --- a/src/host/wasapi/device.rs +++ b/src/host/wasapi/device.rs @@ -507,7 +507,7 @@ impl Device { unsafe { match device.GetId() { Ok(pwstr) => match pwstr.to_string() { - Ok(id_str) => Ok(DeviceId(crate::platform::HostId::Wasapi, id_str)), + Ok(id_str) => Ok(DeviceId::new(crate::platform::HostId::Wasapi, id_str)), Err(e) => Err(Error::with_message( ErrorKind::BackendError, format!("Failed to convert device ID to string: {e}"), diff --git a/src/host/webaudio/mod.rs b/src/host/webaudio/mod.rs index ecb312209..cb05372c3 100644 --- a/src/host/webaudio/mod.rs +++ b/src/host/webaudio/mod.rs @@ -113,16 +113,13 @@ impl Devices { impl Device { fn description(&self) -> Result { - Ok(DeviceDescriptionBuilder::new("Default Device".to_string()) + Ok(DeviceDescriptionBuilder::new("Default Device") .direction(DeviceDirection::Output) .build()) } fn id(&self) -> Result { - Ok(DeviceId( - crate::platform::HostId::WebAudio, - "default".to_string(), - )) + Ok(DeviceId::new(crate::platform::HostId::WebAudio, "default")) } fn supported_input_configs(&self) -> Result { diff --git a/src/lib.rs b/src/lib.rs index 6fc871597..de042cfc9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -249,11 +249,35 @@ pub type FrameCount = u32; /// } /// ``` #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct DeviceId(pub crate::platform::HostId, pub String); +pub struct DeviceId(crate::platform::HostId, Box); + +impl DeviceId { + /// Creates a `DeviceId` from a host identifier and a device-specific identifier string. + /// + /// This constructor is used by backend implementations. Application code should obtain + /// `DeviceId` values through [`DeviceTrait::id`] and persist them via [`Display`]/[`FromStr`]. + /// + /// [`DeviceTrait::id`]: crate::traits::DeviceTrait::id + /// [`Display`]: std::fmt::Display + /// [`FromStr`]: std::str::FromStr + pub fn new(host: crate::platform::HostId, id: impl AsRef) -> Self { + Self(host, Box::from(id.as_ref())) + } + + /// Returns the host this device belongs to. + pub fn host(&self) -> crate::platform::HostId { + self.0 + } + + /// Returns the backend-specific device identifier string. + pub fn id(&self) -> &str { + &self.1 + } +} impl std::fmt::Display for DeviceId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", self.0, self.1) + write!(f, "{}:{}", self.host(), self.id()) } } @@ -283,7 +307,7 @@ impl std::str::FromStr for DeviceId { let host_id = crate::platform::HostId::from_str(host_str)?; - Ok(Self(host_id, device_str.to_string())) + Ok(Self::new(host_id, device_str)) } }