use std::ffi::CStr;
use std::time::Duration;
use rodio::Source;
use rodio::buffer::SamplesBuffer;
use rodio::cpal::traits::{DeviceTrait, HostTrait};
use super::synth::OnePole;
const HAPTIC_CUTOFF_HZ: f32 = 120.0;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum PadKey {
Container(u128),
Ordinal(usize),
}
impl std::fmt::Debug for PadKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Container(id) => write!(f, "Container({id:032x})"),
Self::Ordinal(n) => write!(f, "Ordinal({n})"),
}
}
}
impl PadKey {
pub fn of_hid_path(path: &CStr) -> Option<Self> {
container_of_hid_path(path).map(Self::Container)
}
}
pub fn container_of_hid_path(path: &CStr) -> Option<u128> {
platform::container_of_hid_path(path)
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Endpoint {
pub index: usize,
pub id: String,
pub friendly_name: String,
pub container: Option<u128>,
}
pub fn endpoints() -> Vec<Endpoint> {
platform::endpoints()
}
pub fn is_pad_name(name: &str) -> bool {
name.contains("Wireless Controller")
}
pub fn guid_to_u128(data1: u32, data2: u16, data3: u16, data4: [u8; 8]) -> u128 {
(data1 as u128) << 96
| (data2 as u128) << 80
| (data3 as u128) << 64
| u64::from_be_bytes(data4) as u128
}
pub struct PadSpeaker {
_device: rodio::MixerDeviceSink,
mixer: rodio::mixer::Mixer,
channels: rodio::ChannelCount,
sample_rate: rodio::SampleRate,
pub key: PadKey,
pub name: String,
}
impl std::fmt::Debug for PadSpeaker {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PadSpeaker")
.field("key", &self.key)
.field("name", &self.name)
.field("channels", &self.channels)
.field("sample_rate", &self.sample_rate)
.finish()
}
}
impl PadSpeaker {
pub fn sample_rate(&self) -> rodio::SampleRate {
self.sample_rate
}
pub fn channels(&self) -> rodio::ChannelCount {
self.channels
}
pub fn play(&self, clip: SamplesBuffer, speaker: f32, haptic: f32) {
self.add(clip, speaker, haptic);
}
pub fn add<S>(&self, source: S, speaker: f32, haptic: f32)
where
S: Source<Item = f32> + Send + 'static,
{
if self.channels.get() >= 4 {
self.mixer.add(Quad::new(source, speaker, haptic));
} else {
self.mixer.add(Stereo::new(source, speaker, haptic));
}
}
}
pub fn open_pad_speakers() -> Vec<PadSpeaker> {
let endpoints = endpoints();
let devices = match rodio::cpal::default_host().output_devices() {
Ok(devices) => devices,
Err(error) => {
log::warn!("could not list audio outputs ({error}); no pad speakers");
return Vec::new();
}
};
let mut pads = Vec::new();
let mut seen = 0;
for device in devices {
let Ok(config) = device.default_output_config() else {
continue;
};
let endpoint = device
.id()
.ok()
.and_then(|id| endpoints.iter().find(|endpoint| endpoint.id == id.1));
let is_pad = match endpoint {
Some(endpoint) => is_pad_name(&endpoint.friendly_name),
None => config.channels() == 4,
};
if !is_pad {
continue;
}
let name = match endpoint {
Some(endpoint) => endpoint.friendly_name.clone(),
None => device
.description()
.map(|description| description.name().to_owned())
.unwrap_or_else(|_| "unnamed output".to_owned()),
};
let key = match endpoint.and_then(|endpoint| endpoint.container) {
Some(container) => PadKey::Container(container),
None => PadKey::Ordinal(seen),
};
seen += 1;
let opened =
rodio::DeviceSinkBuilder::from_device(device).and_then(|builder| builder.open_stream());
let mut device = match opened {
Ok(device) => device,
Err(error) => {
log::warn!("could not open pad speaker {name}: {error}");
continue;
}
};
device.log_on_drop(false);
let channels = device.config().channel_count();
let sample_rate = device.config().sample_rate();
log::info!("pad speaker: {name}, {channels} ch {sample_rate} Hz");
pads.push(PadSpeaker {
mixer: device.mixer().clone(),
_device: device,
channels,
sample_rate,
key,
name,
});
}
pads
}
pub struct Spread<S, const N: usize> {
inner: S,
speaker: f32,
haptic: f32,
filter: OnePole,
frame: [f32; N],
left: usize,
}
pub type Quad<S> = Spread<S, 4>;
pub type Stereo<S> = Spread<S, 2>;
impl<S: Source<Item = f32>, const N: usize> Spread<S, N> {
pub fn new(inner: S, speaker: f32, haptic: f32) -> Self {
const {
assert!(N == 2 || N == 4, "a pad has two lanes or four");
}
let filter = OnePole::new(inner.sample_rate().get() as f32, HAPTIC_CUTOFF_HZ);
Self {
inner,
speaker,
haptic,
filter,
frame: [0.0; N],
left: 0,
}
}
fn mono(&mut self) -> Option<f32> {
let channels = self.inner.channels().get() as usize;
let mut sum = self.inner.next()?;
for _ in 1..channels {
sum += self.inner.next()?;
}
Some(sum / channels as f32)
}
}
impl<S: Source<Item = f32>, const N: usize> Iterator for Spread<S, N> {
type Item = f32;
fn next(&mut self) -> Option<f32> {
if self.left == 0 {
let sample = self.mono()?;
let felt = self.filter.step(sample) * self.haptic;
let heard = sample * self.speaker;
self.frame = [heard; N];
if N == 4 {
self.frame[2] = felt;
self.frame[3] = felt;
}
self.left = N;
}
let sample = self.frame[N - self.left];
self.left -= 1;
Some(sample)
}
}
impl<S: Source<Item = f32>, const N: usize> Source for Spread<S, N> {
fn current_span_len(&self) -> Option<usize> {
let channels = self.inner.channels().get() as usize;
self.inner
.current_span_len()
.map(|span| span / channels * N + self.left)
}
fn channels(&self) -> rodio::ChannelCount {
rodio::ChannelCount::new(N as u16).expect("N is two or four")
}
fn sample_rate(&self) -> rodio::SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
#[cfg(windows)]
mod platform {
use std::ffi::CStr;
use windows::Win32::Devices::DeviceAndDriverInstallation::{
CM_Get_DevNode_PropertyW, CM_Get_Device_Interface_PropertyW, CM_LOCATE_DEVNODE_NORMAL,
CM_Locate_DevNodeW, CR_BUFFER_SMALL, CR_SUCCESS,
};
use windows::Win32::Devices::FunctionDiscovery::PKEY_Device_FriendlyName;
use windows::Win32::Devices::Properties::{
DEVPKEY_Device_ContainerId, DEVPKEY_Device_InstanceId, DEVPROP_TYPE_GUID,
DEVPROP_TYPE_STRING, DEVPROPTYPE,
};
use windows::Win32::Foundation::{DEVPROPKEY, PROPERTYKEY, RPC_E_CHANGED_MODE};
use windows::Win32::Media::Audio::{
DEVICE_STATE_ACTIVE, IDeviceTopology, IMMDevice, IMMDeviceEnumerator, IMMEndpoint,
MMDeviceEnumerator, eAll, eRender,
};
use windows::Win32::System::Com::StructuredStorage::PropVariantClear;
use windows::Win32::System::Com::{
CLSCTX_ALL, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoTaskMemFree,
CoUninitialize, STGM_READ,
};
use windows::Win32::System::Variant::{VT_CLSID, VT_LPWSTR};
use windows::Win32::UI::Shell::PropertiesSystem::IPropertyStore;
use windows::core::{GUID, HRESULT, Interface, PCWSTR, PWSTR};
use super::{Endpoint, guid_to_u128};
const PKEY_DEVICE_CONTAINER_ID: PROPERTYKEY = PROPERTYKEY {
fmtid: DEVPKEY_Device_ContainerId.fmtid,
pid: DEVPKEY_Device_ContainerId.pid,
};
struct ComInit(HRESULT);
impl ComInit {
fn usable(&self) -> bool {
self.0.is_ok() || self.0 == RPC_E_CHANGED_MODE
}
}
impl Drop for ComInit {
fn drop(&mut self) {
if self.0.is_ok() {
unsafe { CoUninitialize() };
}
}
}
thread_local! {
static COM: ComInit = ComInit(unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) });
}
fn com_usable() -> bool {
COM.with(ComInit::usable)
}
fn wide(text: &str) -> Vec<u16> {
text.encode_utf16().chain(std::iter::once(0)).collect()
}
unsafe fn take_string(text: PWSTR) -> Option<String> {
if text.is_null() {
return None;
}
let owned = unsafe { text.to_string() }.ok();
unsafe { CoTaskMemFree(Some(text.as_ptr() as *const _)) };
owned
}
fn interface_string(path: &[u16], key: &DEVPROPKEY) -> Option<String> {
let mut kind = DEVPROPTYPE::default();
let mut size = 0u32;
let asked = unsafe {
CM_Get_Device_Interface_PropertyW(
PCWSTR(path.as_ptr()),
key,
&mut kind,
None,
&mut size,
0,
)
};
if asked != CR_BUFFER_SMALL || kind != DEVPROP_TYPE_STRING || size < 2 {
return None;
}
let mut buffer = vec![0u16; size as usize / 2];
let read = unsafe {
CM_Get_Device_Interface_PropertyW(
PCWSTR(path.as_ptr()),
key,
&mut kind,
Some(buffer.as_mut_ptr() as *mut u8),
&mut size,
0,
)
};
if read != CR_SUCCESS {
return None;
}
let end = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
Some(String::from_utf16_lossy(&buffer[..end]))
}
fn devnode_guid(devinst: u32, key: &DEVPROPKEY) -> Option<u128> {
let mut kind = DEVPROPTYPE::default();
let mut guid = GUID::default();
let mut size = std::mem::size_of::<GUID>() as u32;
let read = unsafe {
CM_Get_DevNode_PropertyW(
devinst,
key,
&mut kind,
Some(&mut guid as *mut GUID as *mut u8),
&mut size,
0,
)
};
if read != CR_SUCCESS || kind != DEVPROP_TYPE_GUID {
return None;
}
Some(guid_to_u128(guid.data1, guid.data2, guid.data3, guid.data4))
}
fn container_of_interface(path: &str) -> Option<u128> {
let instance = interface_string(&wide(path), &DEVPKEY_Device_InstanceId)?;
let instance = wide(&instance);
let mut devinst = 0u32;
let located = unsafe {
CM_Locate_DevNodeW(
&mut devinst,
PCWSTR(instance.as_ptr()),
CM_LOCATE_DEVNODE_NORMAL,
)
};
if located != CR_SUCCESS {
return None;
}
devnode_guid(devinst, &DEVPKEY_Device_ContainerId)
}
pub fn container_of_hid_path(path: &CStr) -> Option<u128> {
container_of_interface(&path.to_string_lossy())
}
unsafe fn store_string(store: &IPropertyStore, key: &PROPERTYKEY) -> Option<String> {
let mut value = unsafe { store.GetValue(key) }.ok()?;
let inner = unsafe { &value.Anonymous.Anonymous };
let text = if inner.vt == VT_LPWSTR {
unsafe { inner.Anonymous.pwszVal.to_string() }.ok()
} else {
None
};
unsafe { PropVariantClear(&mut value) }.ok();
text
}
unsafe fn store_guid(store: &IPropertyStore, key: &PROPERTYKEY) -> Option<u128> {
let mut value = unsafe { store.GetValue(key) }.ok()?;
let inner = unsafe { &value.Anonymous.Anonymous };
let guid = if inner.vt == VT_CLSID {
let pointer = unsafe { inner.Anonymous.puuid };
(!pointer.is_null()).then(|| unsafe { *pointer })
} else {
None
};
unsafe { PropVariantClear(&mut value) }.ok();
guid.map(|guid| guid_to_u128(guid.data1, guid.data2, guid.data3, guid.data4))
}
pub fn adapter_path_of(device: &IMMDevice) -> Option<String> {
unsafe {
let topology: IDeviceTopology = device.Activate(CLSCTX_ALL, None).ok()?;
let connector = topology.GetConnector(0).ok()?;
let id = take_string(connector.GetDeviceIdConnectedTo().ok()?)?;
let path = id.strip_prefix("{2}.").unwrap_or(&id);
let end = path.rfind('}')?;
Some(path[..=end].to_owned())
}
}
pub fn endpoints() -> Vec<Endpoint> {
if !com_usable() {
log::warn!("COM would not initialise; pads cannot be told apart");
return Vec::new();
}
let mut found = Vec::new();
unsafe {
let enumerator: IMMDeviceEnumerator =
match CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL) {
Ok(enumerator) => enumerator,
Err(error) => {
log::warn!("no audio endpoint enumerator ({error})");
return found;
}
};
let Ok(collection) = enumerator.EnumAudioEndpoints(eAll, DEVICE_STATE_ACTIVE) else {
return found;
};
let count = collection.GetCount().unwrap_or(0);
for item in 0..count {
let Ok(device) = collection.Item(item) else {
continue;
};
let flow = device
.cast::<IMMEndpoint>()
.and_then(|endpoint| endpoint.GetDataFlow());
if flow != Ok(eRender) {
continue;
}
let Some(id) = device.GetId().ok().and_then(|id| take_string(id)) else {
continue;
};
let store = device.OpenPropertyStore(STGM_READ).ok();
let friendly_name = store
.as_ref()
.and_then(|store| store_string(store, &PKEY_Device_FriendlyName))
.unwrap_or_default();
let container = store
.as_ref()
.and_then(|store| store_guid(store, &PKEY_DEVICE_CONTAINER_ID))
.or_else(|| {
adapter_path_of(&device).and_then(|path| container_of_interface(&path))
});
found.push(Endpoint {
index: found.len(),
id,
friendly_name,
container,
});
}
}
found
}
}
#[cfg(not(windows))]
mod platform {
use std::ffi::CStr;
use super::Endpoint;
pub fn container_of_hid_path(_path: &CStr) -> Option<u128> {
None
}
pub fn endpoints() -> Vec<Endpoint> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use std::f32::consts::TAU;
use super::*;
const RATE: u32 = 48000;
fn rate() -> rodio::SampleRate {
rodio::SampleRate::new(RATE).unwrap()
}
fn mono(samples: Vec<f32>) -> SamplesBuffer {
SamplesBuffer::new(rodio::ChannelCount::MIN, rate(), samples)
}
fn tone(hz: f32, seconds: f32) -> SamplesBuffer {
let count = (RATE as f32 * seconds) as usize;
mono(
(0..count)
.map(|i| (TAU * hz * i as f32 / RATE as f32).sin())
.collect(),
)
}
fn lane_peak(samples: &[f32], lanes: usize, lane: usize, skip: usize) -> f32 {
samples
.iter()
.skip(skip * lanes + lane)
.step_by(lanes)
.fold(0.0f32, |peak, s| peak.max(s.abs()))
}
#[test]
fn a_mono_impulse_comes_out_as_four_samples_with_the_right_gains() {
let quad = Quad::new(mono(vec![1.0, 0.0]), 0.5, 0.8);
assert_eq!(quad.channels().get(), 4);
assert_eq!(quad.sample_rate().get(), RATE);
let out: Vec<f32> = quad.collect();
assert_eq!(out.len(), 8, "four lanes per mono sample");
assert_eq!(out[0], 0.5);
assert_eq!(out[1], 0.5);
let coefficient = 1.0 - (-TAU * HAPTIC_CUTOFF_HZ / RATE as f32).exp();
assert!((out[2] - 0.8 * coefficient).abs() < 1e-6, "{}", out[2]);
assert_eq!(out[2], out[3], "both hands get the same thump");
assert!(
out[2] > 0.0 && out[2] < 0.05,
"and it is well under the speaker's"
);
assert_eq!(out[4], 0.0);
assert!(out[6] > 0.0 && out[6] < out[2]);
}
#[test]
fn the_haptic_lane_keeps_a_thump_and_drops_a_crack() {
let low: Vec<f32> = Quad::new(tone(30.0, 0.5), 1.0, 1.0).collect();
let high: Vec<f32> = Quad::new(tone(5000.0, 0.5), 1.0, 1.0).collect();
let settle = RATE as usize / 10;
let low_felt = lane_peak(&low, 4, 2, settle);
let high_felt = lane_peak(&high, 4, 2, settle);
assert!(low_felt > 0.9, "the thump got through at {low_felt}");
assert!(high_felt < 0.05, "the crack got through at {high_felt}");
assert!(lane_peak(&high, 4, 0, settle) > 0.99);
assert!(lane_peak(&low, 4, 0, settle) > 0.99);
}
#[test]
fn a_stereo_output_gets_the_speaker_lanes_and_nothing_felt() {
let stereo = Stereo::new(mono(vec![1.0, -0.5]), 0.5, 0.8);
assert_eq!(stereo.channels().get(), 2);
let out: Vec<f32> = stereo.collect();
assert_eq!(out, vec![0.5, 0.5, -0.25, -0.25]);
}
#[test]
fn a_stereo_source_is_folded_to_mono_before_it_is_spread() {
let clip = SamplesBuffer::new(
rodio::ChannelCount::new(2).unwrap(),
rate(),
vec![1.0, 0.0, 0.5, 0.5],
);
let out: Vec<f32> = Stereo::new(clip, 1.0, 0.0).collect();
assert_eq!(out, vec![0.5, 0.5, 0.5, 0.5]);
}
struct Remaining(Vec<f32>);
impl Iterator for Remaining {
type Item = f32;
fn next(&mut self) -> Option<f32> {
(!self.0.is_empty()).then(|| self.0.remove(0))
}
}
impl Source for Remaining {
fn current_span_len(&self) -> Option<usize> {
Some(self.0.len())
}
fn channels(&self) -> rodio::ChannelCount {
rodio::ChannelCount::MIN
}
fn sample_rate(&self) -> rodio::SampleRate {
rate()
}
fn total_duration(&self) -> Option<Duration> {
Some(Duration::from_millis(7))
}
}
#[test]
fn the_span_is_counted_in_output_samples() {
let mut quad = Quad::new(Remaining(vec![0.1, 0.2, 0.3]), 1.0, 1.0);
assert_eq!(quad.current_span_len(), Some(12));
quad.next();
assert_eq!(
quad.current_span_len(),
Some(11),
"three of the frame left, and two frames"
);
let rest = quad.by_ref().count();
assert_eq!(rest, 11);
assert_eq!(quad.current_span_len(), Some(0));
assert_eq!(quad.total_duration(), Some(Duration::from_millis(7)));
let quad = Quad::new(mono(vec![0.1, 0.2, 0.3]), 1.0, 1.0);
assert_eq!(quad.current_span_len(), Some(12));
assert_eq!(quad.count(), 12);
}
#[test]
fn a_loop_with_no_spans_reports_none() {
struct Endless;
impl Iterator for Endless {
type Item = f32;
fn next(&mut self) -> Option<f32> {
Some(0.0)
}
}
impl Source for Endless {
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
rodio::ChannelCount::MIN
}
fn sample_rate(&self) -> rodio::SampleRate {
rate()
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
let quad = Quad::new(Endless, 1.0, 1.0);
assert_eq!(quad.current_span_len(), None);
assert_eq!(quad.total_duration(), None);
}
#[test]
fn a_key_is_its_container_and_nothing_else() {
assert_eq!(PadKey::Container(7), PadKey::Container(7));
assert_ne!(PadKey::Container(7), PadKey::Container(8));
assert_ne!(PadKey::Container(1), PadKey::Ordinal(1));
assert_eq!(PadKey::Ordinal(0), PadKey::Ordinal(0));
assert_eq!(
format!(
"{:?}",
PadKey::Container(0x8c7ed206_3f8a_4827_b3ab_ae9e1faefc6c)
),
"Container(8c7ed2063f8a4827b3abae9e1faefc6c)",
"a log line shows the GUID's digits, not a decimal",
);
assert_eq!(format!("{:?}", PadKey::Ordinal(2)), "Ordinal(2)");
let mut set = std::collections::HashSet::new();
set.insert(PadKey::Container(7));
assert!(set.contains(&PadKey::Container(7)), "and it hashes");
}
#[test]
fn a_pad_is_known_by_the_name_sony_gave_the_usb_device() {
assert!(is_pad_name("Speakers (Wireless Controller)"));
assert!(is_pad_name("Speakers (2- Wireless Controller)"));
assert!(is_pad_name("Headphones (3- Wireless Controller)"));
assert!(!is_pad_name("Speakers"));
assert!(!is_pad_name("Speakers (Realtek(R) Audio)"));
assert!(!is_pad_name("DELL U4320Q"));
assert!(!is_pad_name(""));
}
#[test]
fn a_guid_reads_as_the_number_its_text_form_writes() {
let number = guid_to_u128(
0x8c7ed206,
0x3f8a,
0x4827,
[0xb3, 0xab, 0xae, 0x9e, 0x1f, 0xae, 0xfc, 0x6c],
);
assert_eq!(number, 0x8c7ed206_3f8a_4827_b3ab_ae9e1faefc6c);
assert_eq!(guid_to_u128(0, 0, 0, [0; 8]), 0);
assert_eq!(guid_to_u128(0, 0, 0, [0, 0, 0, 0, 0, 0, 0, 1]), 1);
#[cfg(windows)]
assert_eq!(
number,
windows::core::GUID::from_u128(0x8c7ed206_3f8a_4827_b3ab_ae9e1faefc6c).to_u128(),
"and it is the same number the windows crate makes",
);
}
#[test]
#[ignore]
fn probe() {
println!("cpal output devices:");
if let Ok(devices) = rodio::cpal::default_host().output_devices() {
for (index, device) in devices.enumerate() {
let name = device
.description()
.map(|description| description.name().to_owned())
.unwrap_or_else(|error| format!("<{error}>"));
let id = device
.id()
.map(|id| id.1)
.unwrap_or_else(|error| format!("<{error}>"));
match device.default_output_config() {
Ok(config) => println!(
" {index}: {name:?} {} ch {} Hz {:?} id {id}",
config.channels(),
config.sample_rate(),
config.sample_format(),
),
Err(error) => println!(" {index}: {name:?} <{error}> id {id}"),
}
}
}
println!("render endpoints:");
for endpoint in endpoints() {
println!(
" {}: {:?} container {} id {}",
endpoint.index,
endpoint.friendly_name,
endpoint
.container
.map(|c| format!("{c:032x}"))
.unwrap_or_else(|| "none".to_owned()),
endpoint.id,
);
}
println!("PlayStation HID interfaces:");
if let Ok(api) = hidapi::HidApi::new() {
for info in api.device_list().filter(|info| info.vendor_id() == 0x054C) {
println!(
" pid {:04x} interface {}: {:?} container {}",
info.product_id(),
info.interface_number(),
info.path(),
container_of_hid_path(info.path())
.map(|c| format!("{c:032x}"))
.unwrap_or_else(|| "none".to_owned()),
);
}
}
println!("pad speakers opened:");
for pad in open_pad_speakers() {
println!(" {pad:?}");
}
}
}