xinput/functions/get_audio_device_ids_.rs
1use crate::*;
2
3use std::ffi::OsString;
4use std::os::windows::ffi::*;
5
6
7
8/// \[[microsoft.com](https://learn.microsoft.com/en-us/windows/win32/api/xinput/nf-xinput-xinputgetaudiodeviceids)\]
9/// XInputGetAudioDeviceIds
10/// <span style="opacity: 50%">(1.4+)</span>
11///
12/// Get XAudio2 / Windows Core Audio Device Names.
13///
14/// **NOTE:** This tends to succeed, even when no gamepad is connected, with empty/None paths.
15///
16/// ### Arguments
17/// * `user_index` — The controller to get headset/microphone ids for (<code>0 .. [xuser::MAX_COUNT]</code>.)
18///
19/// ### Example
20/// ```rust
21/// let audio = xinput::get_audio_device_ids(0).unwrap_or_default();
22/// println!("{audio:#?}");
23/// ```
24///
25/// ### Output
26/// ```text
27/// AudioDeviceIds {
28/// render_device_id: None,
29/// capture_device_id: None,
30/// }
31/// ```
32///
33/// ### Errors
34/// * [error::BAD_ARGUMENTS] - Invalid `user_index` (expected <code>0 .. [xuser::MAX_COUNT]</code>)
35/// * [error::BUFFER_TOO_SMALL] - Audio device paths exceedingly large (doesn't fit in e.g. `[wchar_t; 4096]`.)
36/// * [error::DEVICE_NOT_CONNECTED] - **Unreliably.**
37/// * [error::INVALID_FUNCTION] - API unavailable: requires XInput 1.4 or later
38///
39/// | System | Windows `ver` | Windows SKU | Behavior |
40/// | ----------------- | ----------------- | --------------------- | -------- |
41/// | Github Actions | 10.0.17763.2366 | Windows 2019 Server | [error::DEVICE_NOT_CONNECTED] observed.
42/// | "SACRILEGE" | 10.0.19041.1415 | Windows 10 Pro | Succeeds when called on missing gamepads
43/// | "NECROMANCY" | 10.0.19045.3930 | Windows 10 Pro | [error::DEVICE_NOT_CONNECTED] on a valid XB1 gamepad <br> connected via XB1 wireless dongle <br> (would USB work better? XB360 controllers?)
44///
45/// ### See Also
46/// * [Getting Audio Device Identifiers](https://learn.microsoft.com/en-us/windows/win32/xinput/getting-started-with-xinput#getting-audio-device-identifiers)
47pub fn get_audio_device_ids(user_index: impl TryInto<u32>) -> Result<AudioDeviceIds, Error> {
48 fn_context!(xinput::get_audio_device_ids => XInputGetAudioDeviceIds);
49 #[allow(non_snake_case)] let XInputGetAudioDeviceIds = imports::XInputGetAudioDeviceIds.load(core::sync::atomic::Ordering::Relaxed);
50 let user_index = user_index.try_into().map_err(|_| fn_param_error!(user_index, error::BAD_ARGUMENTS))?;
51
52 let mut render_id = [0u16; 4096];
53 let mut capture_id = [0u16; 4096];
54 let mut render_len = 4096;
55 let mut capture_len = 4096;
56
57 // SAFETY: ⚠️ Needs testing with real audio devices
58 // * fuzzed in `tests/fuzz-xinput.rs`
59 // * `user_index` is well tested
60 // * `*_ptr` is never null, should only be accessed during XInputGetAudioDeviceIds's scope
61 // * `*_len` are in/out, properly initialized.
62 let code = unsafe { XInputGetAudioDeviceIds(user_index, render_id.as_mut_ptr(), &mut render_len, capture_id.as_mut_ptr(), &mut capture_len) };
63 // a dynamic alloc fallback might be appropriate...? what error is returned? experiment, as it's not documented? XInput's own docs show only 256 byte buffers, surely 16x that (4096) is enough?
64 check_success!(code)?;
65 let render_device_id = OsString::from_wide(render_id .get(..render_len as usize).ok_or(fn_param_error!(render_device_id, error::BUFFER_TOO_SMALL))?.split(|c| *c==0).next().unwrap_or(&[]));
66 let capture_device_id = OsString::from_wide(capture_id.get(..capture_len as usize).ok_or(fn_param_error!(capture_device_id, error::BUFFER_TOO_SMALL))?.split(|c| *c==0).next().unwrap_or(&[]));
67 Ok(AudioDeviceIds {
68 render_device_id: if render_device_id .is_empty() { None } else { Some(render_device_id.into() ) },
69 capture_device_id: if capture_device_id.is_empty() { None } else { Some(capture_device_id.into()) },
70 })
71}
72
73
74#[test] fn test_valid_args() {
75 for user_index in 0 .. 4 {
76 if let Err(err) = get_audio_device_ids(user_index) {
77 assert!(matches!(err.kind(), error::DEVICE_NOT_CONNECTED | error::INVALID_FUNCTION | error::CO_E_NOTINITIALIZED), "unexpected error type: {err:?}");
78 }
79 }
80}
81
82#[test] fn test_bad_user_index() {
83 for user_index in xuser::invalids().chain(Some(xuser::INDEX_ANY)) {
84 let err = get_audio_device_ids(user_index).expect_err("get_audio_devices_ids should return an error for invalid users");
85 assert!(matches!(err.kind(), error::BAD_ARGUMENTS | error::INVALID_FUNCTION | error::CO_E_NOTINITIALIZED), "unexpected error type: {err:?}");
86 }
87}