use std::panic::{catch_unwind, AssertUnwindSafe};
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::{Error, ProcessMode, Result};
use windows::core::{implement, Interface, HRESULT, PROPVARIANT};
use windows::Win32::Foundation::{CloseHandle, HANDLE};
use windows::Win32::Media::Audio::{
ActivateAudioInterfaceAsync, IActivateAudioInterfaceAsyncOperation,
IActivateAudioInterfaceCompletionHandler, IActivateAudioInterfaceCompletionHandler_Impl,
IAudioClient, AUDIOCLIENT_ACTIVATION_PARAMS, AUDIOCLIENT_ACTIVATION_PARAMS_0,
AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK, AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS,
PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE,
PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE, VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK,
WAVEFORMATEX,
};
use windows::Win32::Media::Multimedia::WAVE_FORMAT_IEEE_FLOAT;
use windows::Win32::System::Threading::{CreateEventW, SetEvent};
use crate::common::{capture_loop, init_loopback_capture, map_hr, wait_event_signaled, ComThread};
use windows::core::PCWSTR;
const NATIVE_RATE: u32 = 48_000;
const NATIVE_CHANNELS: u16 = 2;
#[repr(C)]
#[derive(Clone, Copy)]
struct RawPropVariant {
vt: u16,
w_reserved1: u16,
w_reserved2: u16,
w_reserved3: u16,
blob_cb_size: u32,
_pad: u32,
blob_p_data: *mut u8,
}
const VT_BLOB_U16: u16 = 65;
const _: () = {
assert!(core::mem::size_of::<RawPropVariant>() == 24);
assert!(core::mem::align_of::<RawPropVariant>() == 8);
assert!(core::mem::size_of::<PROPVARIANT>() == core::mem::size_of::<RawPropVariant>());
assert!(core::mem::align_of::<PROPVARIANT>() == 8);
};
#[allow(clippy::missing_transmute_annotations)]
unsafe fn make_blob_propvariant(
params: *mut AUDIOCLIENT_ACTIVATION_PARAMS,
) -> core::mem::ManuallyDrop<PROPVARIANT> {
let raw = RawPropVariant {
vt: VT_BLOB_U16,
w_reserved1: 0,
w_reserved2: 0,
w_reserved3: 0,
blob_cb_size: core::mem::size_of::<AUDIOCLIENT_ACTIVATION_PARAMS>() as u32,
_pad: 0,
blob_p_data: params as *mut u8,
};
core::mem::ManuallyDrop::new(PROPVARIANT::from_raw(core::mem::transmute::<
RawPropVariant,
_,
>(raw)))
}
#[implement(IActivateAudioInterfaceCompletionHandler)]
struct ActivationHandler {
done: HANDLE,
}
impl IActivateAudioInterfaceCompletionHandler_Impl for ActivationHandler {
fn ActivateCompleted(
&self,
_operation: Option<&IActivateAudioInterfaceAsyncOperation>,
) -> windows::core::Result<()> {
let _ = catch_unwind(AssertUnwindSafe(|| unsafe {
let _ = SetEvent(self.done);
}));
Ok(())
}
}
pub struct WasapiProcessBackend {
target_pid: u32,
mode: ProcessMode,
stop_flag: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
native: (u32, u16),
}
impl WasapiProcessBackend {
pub fn new(target_pid: u32, mode: ProcessMode) -> Self {
Self {
target_pid,
mode,
stop_flag: Arc::new(AtomicBool::new(false)),
handle: None,
native: (NATIVE_RATE, NATIVE_CHANNELS),
}
}
pub fn target_pid(&self) -> u32 {
self.target_pid
}
pub fn mode(&self) -> ProcessMode {
self.mode
}
}
impl CaptureBackend for WasapiProcessBackend {
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 (ready_tx, ready_rx) = mpsc::channel::<Result<()>>();
let target_pid = self.target_pid;
let mode = self.mode;
let handle = thread::Builder::new()
.name("flexaudio-wasapi-process".into())
.spawn(move || {
run_process_thread(target_pid, mode, sink, stop_flag, ready_tx);
})
.map_err(|e| Error::Backend(format!("spawn wasapi process 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 process 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 WasapiProcessBackend {
fn drop(&mut self) {
self.stop();
}
}
fn fixed_process_format() -> WAVEFORMATEX {
WAVEFORMATEX {
wFormatTag: WAVE_FORMAT_IEEE_FLOAT as u16, nChannels: NATIVE_CHANNELS, nSamplesPerSec: NATIVE_RATE, wBitsPerSample: 32,
nBlockAlign: 8, nAvgBytesPerSec: NATIVE_RATE * 8, cbSize: 0,
}
}
fn run_process_thread(
target_pid: u32,
mode: ProcessMode,
sink: RawSink,
stop_flag: Arc<AtomicBool>,
ready_tx: mpsc::Sender<Result<()>>,
) {
let _com = ComThread::new();
let setup = unsafe { setup_process_loopback(target_pid, mode) };
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)]
pub(crate) unsafe fn setup_process_loopback(
target_pid: u32,
mode: ProcessMode,
) -> Result<(
IAudioClient,
windows::Win32::Media::Audio::IAudioCaptureClient,
HANDLE,
u16,
)> {
let loopback_mode = match mode {
ProcessMode::Include => PROCESS_LOOPBACK_MODE_INCLUDE_TARGET_PROCESS_TREE,
ProcessMode::Exclude => PROCESS_LOOPBACK_MODE_EXCLUDE_TARGET_PROCESS_TREE,
};
let mut params = AUDIOCLIENT_ACTIVATION_PARAMS {
ActivationType: AUDIOCLIENT_ACTIVATION_TYPE_PROCESS_LOOPBACK,
Anonymous: AUDIOCLIENT_ACTIVATION_PARAMS_0 {
ProcessLoopbackParams: AUDIOCLIENT_PROCESS_LOOPBACK_PARAMS {
TargetProcessId: target_pid,
ProcessLoopbackMode: loopback_mode,
},
},
};
let prop = make_blob_propvariant(&mut params as *mut _);
let done_event = CreateEventW(None, true, false, PCWSTR::null())
.map_err(|e| map_hr("CreateEventW(activation done)", e))?;
let handler: IActivateAudioInterfaceCompletionHandler =
ActivationHandler { done: done_event }.into();
let op: IActivateAudioInterfaceAsyncOperation = match ActivateAudioInterfaceAsync(
VIRTUAL_AUDIO_DEVICE_PROCESS_LOOPBACK,
&IAudioClient::IID,
Some(&*prop as *const _),
&handler,
) {
Ok(op) => op,
Err(e) => {
let _ = CloseHandle(done_event);
return Err(map_process_activation_err("ActivateAudioInterfaceAsync", e));
}
};
if !wait_event_signaled(done_event, 5000) {
let _ = CloseHandle(done_event);
return Err(Error::Backend(
"process loopback activation timed out".into(),
));
}
let _ = CloseHandle(done_event);
let mut hr = HRESULT(0);
let mut unknown: Option<windows::core::IUnknown> = None;
op.GetActivateResult(&mut hr, &mut unknown)
.map_err(|e| map_hr("GetActivateResult", e))?;
if let Err(e) = hr.ok() {
return Err(map_process_activation_err("activation result HRESULT", e));
}
let unknown =
unknown.ok_or_else(|| Error::Backend("activation returned null interface".into()))?;
let client: IAudioClient = unknown
.cast()
.map_err(|e| map_hr("cast activated IUnknown to IAudioClient", e))?;
let wfx = fixed_process_format();
let (capture, event) = init_loopback_capture(&client, &wfx as *const WAVEFORMATEX)?;
drop(op);
drop(handler);
let _ = prop; let _ = params;
Ok((client, capture, event, NATIVE_CHANNELS))
}
fn map_process_activation_err(ctx: &str, e: windows::core::Error) -> Error {
const E_NOTIMPL: i32 = 0x80004001u32 as i32;
const E_NOINTERFACE: i32 = 0x80004002u32 as i32;
let code = e.code().0;
if code == E_NOTIMPL || code == E_NOINTERFACE {
Error::UnsupportedOsVersion
} else {
map_hr(ctx, e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use flexaudio_core::raw_ring;
#[test]
fn new_and_native_format_are_fixed() {
let backend = WasapiProcessBackend::new(1234, ProcessMode::Include);
assert_eq!(backend.native_format(), (48_000, 2));
assert_eq!(backend.target_pid(), 1234);
assert_eq!(backend.mode(), ProcessMode::Include);
}
#[test]
fn raw_propvariant_layout_matches_sdk() {
assert_eq!(core::mem::size_of::<RawPropVariant>(), 24);
assert_eq!(core::mem::align_of::<RawPropVariant>(), 8);
assert_eq!(core::mem::size_of::<PROPVARIANT>(), 24);
}
#[test]
fn start_then_stop_tolerates_missing_target() {
let mut backend = WasapiProcessBackend::new(0xFFFF_FFFE, ProcessMode::Include);
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]
#[ignore = "requires a real process playing audio; run with `FLEXAUDIO_TEST_PID=<pid> cargo test -p flexaudio-os-windows -- --ignored` on a Windows machine"]
fn end_to_end_captures_real_audio() {
use std::time::Duration;
let pid: u32 = std::env::var("FLEXAUDIO_TEST_PID")
.ok()
.and_then(|s| s.parse().ok())
.expect("set FLEXAUDIO_TEST_PID to the PID of a process playing audio");
let mut backend = WasapiProcessBackend::new(pid, ProcessMode::Include);
let (rate, channels) = backend.native_format();
let cap = rate as usize * channels as usize * 2; let (prod, mut cons) = raw_ring(cap);
let sink = RawSink::new(prod, rate, channels);
backend.start(sink).expect("start should succeed");
thread::sleep(Duration::from_millis(800));
backend.stop();
let mut buf = vec![0.0f32; cap];
let got = cons.pop_slice(&mut buf);
assert!(got > 0, "expected captured samples, got none");
}
}