use std::sync::mpsc::{Receiver, SyncSender};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use windows::Win32::Media::Audio::*;
use windows::Win32::System::Com::{
CoCreateInstance, CoInitializeEx, CLSCTX_ALL, COINIT_MULTITHREADED,
};
use crate::{AudioBuffer, AudioSource, Result};
pub struct AudioCapture {
stop: Arc<AtomicBool>,
handle: Option<std::thread::JoinHandle<()>>,
pub sample_rate: u32,
pub channels: u16,
}
impl Drop for AudioCapture {
fn drop(&mut self) {
self.stop.store(true, Ordering::SeqCst);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
pub fn start(source: AudioSource, bound: usize) -> Result<(AudioCapture, Receiver<AudioBuffer>)> {
let (sample_rate, channels) = probe_format(source)?;
let (tx, rx) = std::sync::mpsc::sync_channel::<AudioBuffer>(bound);
let stop = Arc::new(AtomicBool::new(false));
let stop_thread = stop.clone();
let handle = std::thread::spawn(move || {
if let Err(e) = capture_loop(source, tx, stop_thread) {
tracing::error!("audio capture loop ended: {e}");
}
});
Ok((
AudioCapture {
stop,
handle: Some(handle),
sample_rate,
channels,
},
rx,
))
}
fn probe_format(source: AudioSource) -> Result<(u32, u16)> {
unsafe {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
let client = activate_client(source)?;
let fmt = client.GetMixFormat()?;
let sr = (*fmt).nSamplesPerSec;
let ch = (*fmt).nChannels;
windows::Win32::System::Com::CoTaskMemFree(Some(fmt as *const _));
Ok((sr, ch))
}
}
unsafe fn activate_client(source: AudioSource) -> Result<IAudioClient> {
let enumerator: IMMDeviceEnumerator =
CoCreateInstance(&MMDeviceEnumerator, None, CLSCTX_ALL)?;
let (data_flow, _role) = match source {
AudioSource::SystemLoopback => (eRender, eConsole),
AudioSource::Microphone => (eCapture, eConsole),
};
let device = enumerator.GetDefaultAudioEndpoint(data_flow, eConsole)?;
let client = device.Activate::<IAudioClient>(CLSCTX_ALL, None)?;
Ok(client)
}
fn capture_loop(
source: AudioSource,
tx: SyncSender<AudioBuffer>,
stop: Arc<AtomicBool>,
) -> Result<()> {
unsafe {
let _ = CoInitializeEx(None, COINIT_MULTITHREADED);
let client = activate_client(source)?;
let fmt_ptr = client.GetMixFormat()?;
let fmt = *fmt_ptr;
let sample_rate = fmt.nSamplesPerSec;
let channels = fmt.nChannels;
let is_float = format_is_float(fmt_ptr);
let bits = fmt.wBitsPerSample;
let stream_flags = match source {
AudioSource::SystemLoopback => AUDCLNT_STREAMFLAGS_LOOPBACK,
AudioSource::Microphone => 0,
};
let buffer_duration_hns = 2_000_000i64; client.Initialize(
AUDCLNT_SHAREMODE_SHARED,
stream_flags,
buffer_duration_hns,
0,
fmt_ptr,
None,
)?;
let capture: IAudioCaptureClient = client.GetService()?;
client.Start()?;
windows::Win32::System::Com::CoTaskMemFree(Some(fmt_ptr as *const _));
let start_qpc = std::time::Instant::now();
let poll = std::time::Duration::from_millis(10);
while !stop.load(Ordering::SeqCst) {
let packet_len = capture.GetNextPacketSize()?;
if packet_len == 0 {
std::thread::sleep(poll);
continue;
}
let mut data_ptr: *mut u8 = std::ptr::null_mut();
let mut num_frames: u32 = 0;
let mut flags: u32 = 0;
capture.GetBuffer(
&mut data_ptr,
&mut num_frames,
&mut flags,
None,
None,
)?;
let silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT.0 as u32) != 0;
let frame_bytes = (bits / 8) as usize * channels as usize;
let byte_len = num_frames as usize * frame_bytes;
let pcm16 = if silent {
vec![0u8; num_frames as usize * 2 * channels as usize]
} else {
let raw = std::slice::from_raw_parts(data_ptr, byte_len);
to_pcm16(raw, is_float, bits, channels)
};
capture.ReleaseBuffer(num_frames)?;
let timestamp = start_qpc.elapsed();
let _ = tx.try_send(AudioBuffer {
data: pcm16,
timestamp,
sample_rate,
channels,
});
}
client.Stop()?;
Ok(())
}
}
unsafe fn format_is_float(fmt: *const WAVEFORMATEX) -> bool {
const WAVE_FORMAT_IEEE_FLOAT: u16 = 0x0003;
const WAVE_FORMAT_EXTENSIBLE: u16 = 0xFFFE;
match (*fmt).wFormatTag {
WAVE_FORMAT_IEEE_FLOAT => true,
WAVE_FORMAT_EXTENSIBLE => {
use windows::Win32::Media::Multimedia::KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
let ext = fmt as *const WAVEFORMATEXTENSIBLE;
let subformat = std::ptr::addr_of!((*ext).SubFormat).read_unaligned();
subformat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT
}
_ => false,
}
}
fn to_pcm16(raw: &[u8], is_float: bool, bits: u16, _channels: u16) -> Vec<u8> {
if is_float && bits == 32 {
let n = raw.len() / 4;
let mut out = Vec::with_capacity(n * 2);
for i in 0..n {
let f = f32::from_le_bytes([
raw[i * 4],
raw[i * 4 + 1],
raw[i * 4 + 2],
raw[i * 4 + 3],
]);
let clamped = f.clamp(-1.0, 1.0);
let s = (clamped * 32767.0) as i16;
out.extend_from_slice(&s.to_le_bytes());
}
out
} else if bits == 16 {
raw.to_vec() } else if bits == 32 {
let n = raw.len() / 4;
let mut out = Vec::with_capacity(n * 2);
for i in 0..n {
out.push(raw[i * 4 + 2]);
out.push(raw[i * 4 + 3]);
}
out
} else {
raw.to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn float_to_pcm16_maps_range() {
let mut raw = Vec::new();
for f in [1.0f32, 0.0, -1.0] {
raw.extend_from_slice(&f.to_le_bytes());
}
let pcm = to_pcm16(&raw, true, 32, 1);
let s: Vec<i16> = pcm
.chunks(2)
.map(|c| i16::from_le_bytes([c[0], c[1]]))
.collect();
assert_eq!(s[0], 32767);
assert_eq!(s[1], 0);
assert_eq!(s[2], -32767);
}
#[test]
fn pcm16_passthrough() {
let raw = vec![0x11, 0x22, 0x33, 0x44];
assert_eq!(to_pcm16(&raw, false, 16, 1), raw);
}
}