Skip to content

Commit e32ee65

Browse files
authored
feat: implement Hash/Eq for host Device types (#1068)
Add Eq/Hash implementations for Device across ALSA, WASAPI, WebAudio, CoreAudio (macOS/iOS), AudioWorklet, and Null. Hashing is based on backend device identifiers and, for ALSA, PCM ID + direction. WASAPI hashes the COM device ID string obtained via GetId.
1 parent a8b016c commit e32ee65

File tree

8 files changed

+69
-12
lines changed

8 files changed

+69
-12
lines changed

CHANGELOG.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
- Add `Display` and `FromStr` implementations for `HostId`.
66
- Add support for custom `Host`s, `Device`s, and `Stream`s.
77
- Add `Sample::bits_per_sample` method.
8-
- Add `Copy` impl to `InputCallbackInfo` and `OutputCallbackInfo`.
8+
- Add `Copy` implementation to `InputCallbackInfo` and `OutputCallbackInfo`.
99
- Add `StreamError::StreamInvalidated` variant for when stream must be rebuilt.
1010
- Add `StreamError::BufferUnderrun` variant for buffer underrun/overrun notifications.
11+
- Add `Hash` implementation to `Device` for all backends.
1112
- Change `SampleRate` from struct to `u32` type alias.
1213
- Update `audio_thread_priority` to 0.34.
1314
- AAudio: Configure buffer to ensure consistent callback buffer sizes.
14-
- AAudio: Make `Stream` implement `Send` and `Sync`.
15+
- AAudio: Add `Send` and `Sync` implementations to `Stream`.
1516
- AAudio: Fix the buffer size range detection by querying the AudioService property correctly.
1617
- AAudio: Add support for 12 and 24 kHz sample rates.
1718
- ALSA: Improve `BufferSize::Fixed` precision and audio callback performance.
@@ -23,6 +24,7 @@
2324
- ALSA: Update `alsa` to 0.10.
2425
- ALSA: Pass `silent=true` to `PCM.try_recover`, so it doesn't write to stderr.
2526
- ALSA: Report buffer underruns/overruns via `StreamError::BufferUnderrun`.
27+
- ALSA: Add `Eq` and `PartialEq` implementations to `Device`.
2628
- ASIO: Fix linker flags for MinGW cross-compilation.
2729
- ASIO: Add packed(4) to representation of ASIO time structs in bindings.
2830
- ASIO: Add handling for `kAsioResetRequest` message to prevent driver UI becoming unresponsive.
@@ -38,8 +40,8 @@
3840
- CoreAudio: Update `mach2` to 0.6.
3941
- CoreAudio: Configure device buffer to ensure predictable callback buffer sizes.
4042
- CoreAudio: Fix timestamp accuracy.
41-
- CoreAudio: Make `Stream` implement `Send`.
42-
- CoreAudio: Remove `Clone` impl from `Stream`.
43+
- CoreAudio: Add `Send` implementation to `Stream`.
44+
- CoreAudio: Remove `Clone` implementation from `Stream`.
4345
- CoreAudio: Fix segfaults when enumerating devices.
4446
- CoreAudio: Fix undefined behavior related to null pointers and aligned reads.
4547
- Emscripten: Add `BufferSize::Fixed` validation against supported range.
@@ -52,9 +54,9 @@
5254
- WASAPI: Expose `IMMDevice` from WASAPI host Device.
5355
- WASAPI: Add `I24` and `U24` sample format support (24-bit samples stored in 4 bytes).
5456
- WASAPI: Update `windows` to >= 0.59, <= 0.62.
55-
- WASAPI: Make `Stream` implement `Send` and `Sync`.
56-
- Wasm: Removed optional `wee-alloc` feature for security reasons.
57-
- Wasm: Make `Stream` implement `Send` and `Sync`.
57+
- WASAPI: Add `Send` and `Sync` implementations to `Stream`.
58+
- WebAudio: Removed optional `wee-alloc` feature for security reasons.
59+
- WebAudio: Add `Send` and `Sync` implementations to `Stream`.
5860
- WebAudio: Add `BufferSize::Fixed` validation against supported range.
5961

6062
# Version 0.16.0 (2025-06-07)

src/host/alsa/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,29 @@ pub struct Device {
314314
handles: Arc<Mutex<DeviceHandles>>,
315315
}
316316

317+
impl PartialEq for Device {
318+
fn eq(&self, other: &Self) -> bool {
319+
// Devices are equal if they have the same PCM ID and direction.
320+
// The handles field is not part of device identity.
321+
self.pcm_id == other.pcm_id && self.direction == other.direction
322+
}
323+
}
324+
325+
impl Eq for Device {}
326+
327+
impl std::hash::Hash for Device {
328+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
329+
// Hash based on PCM ID and direction for consistency with PartialEq
330+
self.pcm_id.hash(state);
331+
// Manually hash direction since alsa::Direction doesn't implement Hash
332+
match self.direction {
333+
Some(alsa::Direction::Capture) => 0u8.hash(state),
334+
Some(alsa::Direction::Playback) => 1u8.hash(state),
335+
None => 2u8.hash(state),
336+
}
337+
}
338+
}
339+
317340
impl Device {
318341
fn build_stream_inner(
319342
&self,

src/host/audioworklet/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use std::time::Duration;
2323
/// Content is false if the iterator is empty.
2424
pub struct Devices(bool);
2525

26-
#[derive(Clone, Debug, PartialEq, Eq)]
26+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2727
pub struct Device;
2828

2929
pub struct Host;

src/host/coreaudio/ios/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub mod enumerate;
3232
// These days the default of iOS is now F32 and no longer I16
3333
const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
3434

35-
#[derive(Clone, Debug, PartialEq, Eq)]
35+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3636
pub struct Device;
3737

3838
pub struct Host;

src/host/coreaudio/macos/device.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ impl DeviceTrait for Device {
337337
}
338338
}
339339

340-
#[derive(Clone, Eq, PartialEq)]
340+
#[derive(Clone, Eq, Hash, PartialEq)]
341341
pub struct Device {
342342
pub(crate) audio_device_id: AudioDeviceID,
343343
}

src/host/null/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
#[derive(Default)]
1616
pub struct Devices;
1717

18-
#[derive(Clone, Debug, PartialEq, Eq)]
18+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1919
pub struct Device;
2020

2121
pub struct Host;

src/host/wasapi/device.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,38 @@ impl PartialEq for Device {
940940

941941
impl Eq for Device {}
942942

943+
impl std::hash::Hash for Device {
944+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
945+
// Hash the device ID for consistency with PartialEq
946+
// SAFETY: GetId only fails with E_OUTOFMEMORY, which is unrecoverable.
947+
// We need consistent hash/eq behavior.
948+
unsafe {
949+
use windows::Win32::System::Com;
950+
951+
struct IdRAII(windows::core::PWSTR);
952+
impl Drop for IdRAII {
953+
fn drop(&mut self) {
954+
unsafe { Com::CoTaskMemFree(Some(self.0 .0 as *mut _)) }
955+
}
956+
}
957+
958+
let id = self.device.GetId().expect("cpal: GetId failure");
959+
let id = IdRAII(id);
960+
961+
// Hash the 16-bit null-terminated string
962+
let mut offset = 0;
963+
loop {
964+
let w: u16 = *(id.0).0.offset(offset);
965+
if w == 0 {
966+
break;
967+
}
968+
w.hash(state);
969+
offset += 1;
970+
}
971+
}
972+
}
973+
}
974+
943975
impl fmt::Debug for Device {
944976
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
945977
f.debug_struct("Device")

src/host/webaudio/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type ClosureHandle = Arc<RwLock<Option<Closure<dyn FnMut()>>>>;
2727
/// Content is false if the iterator is empty.
2828
pub struct Devices(bool);
2929

30-
#[derive(Clone, Debug, PartialEq, Eq)]
30+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
3131
pub struct Device;
3232

3333
pub struct Host;

0 commit comments

Comments
 (0)