use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use flexaudio_core::backend::{CaptureBackend, RawSink};
use flexaudio_core::types::{DeviceInfo, Error, ProcessMode, Result, SourceKind};
use windows::core::Interface;
use windows::Win32::Devices::FunctionDiscovery::PKEY_Device_FriendlyName;
use windows::Win32::Media::Audio::{
eConsole, eRender, IAudioClient, IMMDevice, IMMDeviceEnumerator, MMDeviceEnumerator,
DEVICE_STATE_ACTIVE, WAVEFORMATEX,
};
use windows::Win32::System::Com::{CoCreateInstance, CoTaskMemFree, CLSCTX_ALL, STGM_READ};
use crate::common::{capture_loop, init_loopback_capture, map_hr, parse_mix_format, ComThread};
const FALLBACK_FORMAT: (u32, u16) = (48_000, 2);
const PROCESS_LOOPBACK_FORMAT: (u32, u16) = (48_000, 2);
pub struct WasapiSystemBackend {
exclude_self: bool,
device_id: Option<String>,
stop_flag: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
native: (u32, u16),
}
impl WasapiSystemBackend {
pub fn new(exclude_self: bool, device_id: Option<String>) -> Self {
let native = if exclude_self {
PROCESS_LOOPBACK_FORMAT
} else {
query_native_format(device_id.as_deref()).unwrap_or(FALLBACK_FORMAT)
};
Self {
exclude_self,
device_id,
stop_flag: Arc::new(AtomicBool::new(false)),
handle: None,
native,
}
}
}
impl Default for WasapiSystemBackend {
fn default() -> Self {
Self::new(false, None)
}
}
fn query_native_format(device_id: Option<&str>) -> Option<(u32, u16)> {
let _com = ComThread::new();
unsafe {
let enumerator: IMMDeviceEnumerator =
CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL).ok()?;
let device = resolve_render_endpoint(&enumerator, device_id).ok()?;
let client: IAudioClient = device.Activate(CLSCTX_ALL, None).ok()?;
let pwfx = client.GetMixFormat().ok()?;
if pwfx.is_null() {
return None;
}
let rate = core::ptr::addr_of!((*pwfx).nSamplesPerSec).read_unaligned();
let channels = core::ptr::addr_of!((*pwfx).nChannels).read_unaligned();
CoTaskMemFree(Some(pwfx as *const _ as *const _));
Some((rate, channels))
}
}
unsafe fn resolve_render_endpoint(
enumerator: &IMMDeviceEnumerator,
device_id: Option<&str>,
) -> Result<IMMDevice> {
let Some(id) = device_id else {
return enumerator
.GetDefaultAudioEndpoint(eRender, eConsole)
.map_err(|_e| Error::DeviceNotFound);
};
let collection = enumerator
.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE)
.map_err(|e| map_hr("IMMDeviceEnumerator::EnumAudioEndpoints", e))?;
let count = collection
.GetCount()
.map_err(|e| map_hr("IMMDeviceCollection::GetCount", e))?;
for i in 0..count {
let device = match collection.Item(i) {
Ok(d) => d,
Err(_e) => continue,
};
if endpoint_friendly_name(&device).as_deref() == Some(id) {
return Ok(device);
}
}
Err(Error::DeviceNotFound)
}
unsafe fn endpoint_friendly_name(device: &IMMDevice) -> Option<String> {
let store = device.OpenPropertyStore(STGM_READ).ok()?;
let value = store.GetValue(&PKEY_Device_FriendlyName).ok()?;
let name = value.to_string();
if name.is_empty() {
None
} else {
Some(name)
}
}
pub fn list_output_devices() -> Result<Vec<DeviceInfo>> {
let _com = ComThread::new();
unsafe {
let enumerator: IMMDeviceEnumerator =
CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)
.map_err(|e| map_hr("CoCreateInstance(MMDeviceEnumerator)", e))?;
let default_name = enumerator
.GetDefaultAudioEndpoint(eRender, eConsole)
.ok()
.and_then(|d| endpoint_friendly_name(&d));
let collection = enumerator
.EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE)
.map_err(|e| map_hr("IMMDeviceEnumerator::EnumAudioEndpoints", e))?;
let count = collection
.GetCount()
.map_err(|e| map_hr("IMMDeviceCollection::GetCount", e))?;
let mut out = Vec::with_capacity(count as usize);
for i in 0..count {
let device = match collection.Item(i) {
Ok(d) => d,
Err(_e) => continue,
};
let Some(name) = endpoint_friendly_name(&device) else {
continue;
};
let (sample_rate, channels) = endpoint_mix_format(&device).unwrap_or(FALLBACK_FORMAT);
let is_default = default_name.as_deref() == Some(name.as_str());
out.push(DeviceInfo {
id: name.clone(),
name,
source_kind: SourceKind::SystemLoopback,
sample_rate,
channels,
is_loopback: true,
is_default,
});
}
Ok(out)
}
}
unsafe fn endpoint_mix_format(device: &IMMDevice) -> Option<(u32, u16)> {
let client: IAudioClient = device.Activate(CLSCTX_ALL, None).ok()?;
let pwfx = client.GetMixFormat().ok()?;
if pwfx.is_null() {
return None;
}
let rate = core::ptr::addr_of!((*pwfx).nSamplesPerSec).read_unaligned();
let channels = core::ptr::addr_of!((*pwfx).nChannels).read_unaligned();
CoTaskMemFree(Some(pwfx as *const _ as *const _));
Some((rate, channels))
}
impl CaptureBackend for WasapiSystemBackend {
fn native_format(&self) -> (u32, u16) {
self.native
}
fn start(&mut self, sink: RawSink) -> Result<()> {
if self.handle.is_some() {
return Ok(());
}
self.stop_flag.store(false, Ordering::SeqCst);
let stop_flag = self.stop_flag.clone();
let exclude_self = self.exclude_self;
let device_id = self.device_id.clone();
let (ready_tx, ready_rx) = mpsc::channel::<Result<()>>();
let handle = thread::Builder::new()
.name("flexaudio-wasapi-system".into())
.spawn(move || {
run_system_thread(exclude_self, device_id, sink, stop_flag, ready_tx);
})
.map_err(|e| Error::Backend(format!("spawn wasapi system thread: {e}")))?;
match ready_rx.recv() {
Ok(Ok(())) => {
self.handle = Some(handle);
Ok(())
}
Ok(Err(e)) => {
self.stop_flag.store(false, Ordering::SeqCst);
let _ = handle.join();
Err(e)
}
Err(_) => {
self.stop_flag.store(false, Ordering::SeqCst);
let _ = handle.join();
Err(Error::Backend(
"wasapi system thread exited before reporting readiness".into(),
))
}
}
}
fn stop(&mut self) {
self.stop_flag.store(true, Ordering::SeqCst);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
impl Drop for WasapiSystemBackend {
fn drop(&mut self) {
self.stop();
}
}
fn run_system_thread(
exclude_self: bool,
device_id: Option<String>,
sink: RawSink,
stop_flag: Arc<AtomicBool>,
ready_tx: mpsc::Sender<Result<()>>,
) {
let _com = ComThread::new();
let setup = if exclude_self {
unsafe { crate::process::setup_process_loopback(std::process::id(), ProcessMode::Exclude) }
} else {
unsafe { setup_system_loopback(device_id.as_deref(), &sink) }
};
let (client, capture, event, channels) = match setup {
Ok(t) => t,
Err(e) => {
let _ = ready_tx.send(Err(e));
return;
}
};
if ready_tx.send(Ok(())).is_err() {
return;
}
unsafe { capture_loop(&client, &capture, event, channels, sink, &stop_flag) };
}
#[allow(clippy::type_complexity)]
unsafe fn setup_system_loopback(
device_id: Option<&str>,
_sink: &RawSink,
) -> Result<(
IAudioClient,
windows::Win32::Media::Audio::IAudioCaptureClient,
windows::Win32::Foundation::HANDLE,
u16,
)> {
let enumerator: IMMDeviceEnumerator =
CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)
.map_err(|e| map_hr("CoCreateInstance(MMDeviceEnumerator)", e))?;
let device: IMMDevice = resolve_render_endpoint(&enumerator, device_id)?;
let client: IAudioClient = device
.Activate(CLSCTX_ALL, None)
.map_err(|e| map_hr("IMMDevice::Activate(IAudioClient)", e))?;
let pwfx: *mut WAVEFORMATEX = client
.GetMixFormat()
.map_err(|e| map_hr("IAudioClient::GetMixFormat", e))?;
let parsed = parse_mix_format(pwfx as *const WAVEFORMATEX);
let (_rate, channels) = match parsed {
Ok(v) => v,
Err(e) => {
CoTaskMemFree(Some(pwfx as *const _ as *const _));
return Err(e);
}
};
let init = init_loopback_capture(&client, pwfx as *const WAVEFORMATEX);
CoTaskMemFree(Some(pwfx as *const _ as *const _));
let (capture, event) = init?;
let _ = client.as_raw();
Ok((client, capture, event, channels))
}
#[cfg(test)]
mod tests {
use super::*;
use flexaudio_core::raw_ring;
#[test]
fn new_and_native_format_do_not_panic() {
let backend = WasapiSystemBackend::new(false, None);
let (rate, channels) = backend.native_format();
assert!(rate > 0);
assert!(channels > 0);
}
#[test]
fn new_with_device_id_does_not_panic() {
let backend = WasapiSystemBackend::new(false, Some("no-such-endpoint".into()));
let (rate, channels) = backend.native_format();
assert!(rate > 0);
assert!(channels > 0);
}
#[test]
fn new_exclude_self_native_is_fixed() {
let backend = WasapiSystemBackend::new(true, Some("ignored".into()));
assert_eq!(backend.native_format(), (48_000, 2));
}
#[test]
fn list_output_devices_does_not_panic() {
if let Ok(devices) = list_output_devices() {
for d in &devices {
assert!(d.is_loopback);
assert_eq!(d.source_kind, SourceKind::SystemLoopback);
}
}
}
#[test]
fn start_then_stop_tolerates_missing_endpoint() {
let mut backend = WasapiSystemBackend::new(false, None);
let (rate, channels) = backend.native_format();
let cap = (rate as usize * channels as usize).max(1);
let (prod, _cons) = raw_ring(cap);
let sink = RawSink::new(prod, rate, channels);
match backend.start(sink) {
Ok(()) => {
backend.stop();
backend.stop(); }
Err(_e) => { }
}
}
#[test]
fn start_with_unknown_device_id_is_device_not_found() {
let mut backend = WasapiSystemBackend::new(false, Some("no-such-endpoint".into()));
let (rate, channels) = backend.native_format();
let cap = (rate as usize * channels as usize).max(1);
let (prod, _cons) = raw_ring(cap);
let sink = RawSink::new(prod, rate, channels);
match backend.start(sink) {
Ok(()) => {
backend.stop();
}
Err(e) => assert!(matches!(e, Error::DeviceNotFound)),
}
}
#[test]
fn start_then_stop_exclude_self_tolerates_failure() {
let mut backend = WasapiSystemBackend::new(true, None);
let (rate, channels) = backend.native_format();
let cap = (rate as usize * channels as usize).max(1);
let (prod, _cons) = raw_ring(cap);
let sink = RawSink::new(prod, rate, channels);
match backend.start(sink) {
Ok(()) => {
backend.stop();
backend.stop(); }
Err(_e) => {
}
}
}
}