use std::sync::{Arc, Mutex};
use anyhow::{anyhow, Result};
use tokio::sync::mpsc;
use core_foundation::base::TCFType;
use core_media_rs::cm_format_description::CMFormatDescriptionRef;
use core_media_rs::cm_sample_buffer::CMSampleBuffer;
use screencapturekit::{
shareable_content::SCShareableContent,
stream::{
configuration::SCStreamConfiguration, content_filter::SCContentFilter,
output_trait::SCStreamOutputTrait, output_type::SCStreamOutputType, SCStream,
},
};
use crate::audio::capture::{CaptureHandle, CaptureInfo};
#[repr(C)]
#[derive(Debug, Default, Copy, Clone)]
#[allow(non_snake_case)]
struct AudioStreamBasicDescription {
mSampleRate: f64,
mFormatID: u32,
mFormatFlags: u32,
mBytesPerPacket: u32,
mFramesPerPacket: u32,
mBytesPerFrame: u32,
mChannelsPerFrame: u32,
mBitsPerChannel: u32,
mReserved: u32,
}
const K_AUDIO_FORMAT_FLAG_IS_FLOAT: u32 = 1 << 0;
const K_AUDIO_FORMAT_FLAG_IS_NON_INTERLEAVED: u32 = 1 << 5;
extern "C" {
fn CMAudioFormatDescriptionGetStreamBasicDescription(
desc: CMFormatDescriptionRef,
) -> *const AudioStreamBasicDescription;
}
pub struct LoopbackKeepAlive {
stream: Option<SCStream>,
}
impl Drop for LoopbackKeepAlive {
fn drop(&mut self) {
if let Some(s) = self.stream.take() {
if let Err(e) = s.stop_capture() {
log::warn!("loopback: stop_capture error: {:?}", e);
}
drop(s);
}
}
}
struct AudioOutput {
tx: mpsc::Sender<Vec<f32>>,
format: Arc<Mutex<Option<ObservedFormat>>>,
}
#[derive(Clone, Copy)]
#[allow(dead_code)] struct ObservedFormat {
sample_rate: u32,
channels: u16,
is_float: bool,
is_non_interleaved: bool,
bits_per_channel: u32,
}
impl SCStreamOutputTrait for AudioOutput {
fn did_output_sample_buffer(&self, sample_buffer: CMSampleBuffer, of_type: SCStreamOutputType) {
if !matches!(of_type, SCStreamOutputType::Audio) {
return;
}
if self.format.lock().map(|g| g.is_none()).unwrap_or(false) {
if let Ok(fd) = sample_buffer.get_format_description() {
let ptr = fd.as_concrete_TypeRef();
unsafe {
let asbd_ptr = CMAudioFormatDescriptionGetStreamBasicDescription(ptr);
if !asbd_ptr.is_null() {
let asbd = *asbd_ptr;
let observed = ObservedFormat {
sample_rate: asbd.mSampleRate as u32,
channels: asbd.mChannelsPerFrame as u16,
is_float: (asbd.mFormatFlags & K_AUDIO_FORMAT_FLAG_IS_FLOAT) != 0,
is_non_interleaved: (asbd.mFormatFlags
& K_AUDIO_FORMAT_FLAG_IS_NON_INTERLEAVED)
!= 0,
bits_per_channel: asbd.mBitsPerChannel,
};
if let Ok(mut slot) = self.format.lock() {
*slot = Some(observed);
}
log::info!(
"loopback: audio format observed — rate={} ch={} float={} non_interleaved={} bits={}",
observed.sample_rate,
observed.channels,
observed.is_float,
observed.is_non_interleaved,
observed.bits_per_channel
);
}
}
}
}
let observed = match self.format.lock().ok().and_then(|g| *g) {
Some(o) => o,
None => return, };
let abl = match sample_buffer.get_audio_buffer_list() {
Ok(a) => a,
Err(e) => {
log::warn!("loopback: get_audio_buffer_list: {:?}", e);
return;
}
};
let interleaved = match decode_buffer_list(&abl, &observed) {
Some(v) => v,
None => return,
};
let _ = self.tx.try_send(interleaved);
}
}
fn decode_buffer_list(
abl: &core_audio_types_rs::audio_buffer_list::AudioBufferList,
fmt: &ObservedFormat,
) -> Option<Vec<f32>> {
if !fmt.is_float || fmt.bits_per_channel != 32 {
log::warn!(
"loopback: unsupported audio format (float={} bits={}); dropping frame",
fmt.is_float,
fmt.bits_per_channel
);
return None;
}
let channels = fmt.channels.max(1) as usize;
if !fmt.is_non_interleaved {
let buf = abl.get(0)?;
let bytes = buf.data();
let sample_count = bytes.len() / 4;
let mut out = Vec::<f32>::with_capacity(sample_count);
for chunk in bytes.chunks_exact(4) {
let bits = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
out.push(f32::from_bits(bits));
}
Some(out)
} else {
if abl.num_buffers() < channels {
return None;
}
let first = abl.get(0)?;
let frames_per_channel = (first.data_bytes_size as usize) / 4;
let mut out = vec![0.0_f32; frames_per_channel * channels];
for ch in 0..channels {
let buf = abl.get(ch)?;
let bytes = buf.data();
for (i, chunk) in bytes.chunks_exact(4).enumerate() {
if i >= frames_per_channel {
break;
}
let bits = u32::from_ne_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
out[i * channels + ch] = f32::from_bits(bits);
}
}
Some(out)
}
}
pub fn capture(name: Option<&str>) -> Result<CaptureHandle> {
log::info!(
"loopback: capturing system audio (requested device: {:?})",
name
);
let content = sck_shareable_content()?;
let display = first_display(&content)?;
let filter = SCContentFilter::new().with_display_excluding_windows(&display, &[]);
start_sck_capture(filter)
}
pub fn capture_app(bundle_id: &str) -> Result<CaptureHandle> {
log::info!("loopback: capturing per-app audio for {:?}", bundle_id);
let content = sck_shareable_content()?;
let display = first_display(&content)?;
let apps = content.applications();
let target = apps
.iter()
.find(|a| {
let bid = a.bundle_identifier();
!bid.is_empty() && bid == bundle_id
})
.or_else(|| {
apps.iter().find(|a| a.application_name() == bundle_id)
})
.ok_or_else(|| {
anyhow!(
"application '{}' not found among {} running apps — try refreshing ([r])",
bundle_id,
apps.len()
)
})?;
log::info!(
"loopback: per-app target = {} (bundle={}, pid={})",
target.application_name(),
target.bundle_identifier(),
target.process_id()
);
let filter = SCContentFilter::new().with_display_including_application_excepting_windows(
&display,
&[target],
&[],
);
start_sck_capture(filter)
}
fn sck_shareable_content() -> Result<screencapturekit::shareable_content::SCShareableContent> {
SCShareableContent::get()
.map_err(|e| anyhow!("SCShareableContent::get failed: {:?} — if this is a permission error, grant 'Screen & System Audio Recording' in System Settings > Privacy & Security > Screen Recording and restart voice-bird", e))
}
#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
fn CGPreflightScreenCaptureAccess() -> bool;
}
pub fn screen_recording_permission_granted() -> bool {
unsafe { CGPreflightScreenCaptureAccess() }
}
fn first_display(
content: &screencapturekit::shareable_content::SCShareableContent,
) -> Result<screencapturekit::shareable_content::SCDisplay> {
let mut displays = content.displays();
if displays.is_empty() {
return Err(anyhow!(
"no displays available for ScreenCaptureKit (screen recording permission may be denied)"
));
}
Ok(displays.remove(0))
}
fn start_sck_capture(filter: SCContentFilter) -> Result<CaptureHandle> {
let config = SCStreamConfiguration::new()
.set_captures_audio(true)
.map_err(|e| anyhow!("set_captures_audio: {:?}", e))?
.set_excludes_current_process_audio(true)
.map_err(|e| anyhow!("set_excludes_current_process_audio: {:?}", e))?
.set_width(2)
.map_err(|e| anyhow!("set_width: {:?}", e))?
.set_height(2)
.map_err(|e| anyhow!("set_height: {:?}", e))?;
let (tx, rx) = mpsc::channel::<Vec<f32>>(64);
let format_slot: Arc<Mutex<Option<ObservedFormat>>> = Arc::new(Mutex::new(None));
let mut stream = SCStream::new(&filter, &config);
let audio_output = AudioOutput {
tx,
format: format_slot.clone(),
};
if stream
.add_output_handler(audio_output, SCStreamOutputType::Audio)
.is_none()
{
return Err(anyhow!("failed to add audio output handler to SCStream"));
}
stream.start_capture().map_err(|e| {
anyhow!(
"screen recording permission denied — grant it in System Settings > Privacy & Security > Screen Recording, then restart voice-bird (underlying error: {:?})",
e
)
})?;
let info = CaptureInfo {
sample_rate: 48_000,
channels: 2,
};
Ok(CaptureHandle {
frames_rx: rx,
info,
stream: crate::audio::capture::CaptureKeepAlive::Sck(LoopbackKeepAlive {
stream: Some(stream),
}),
})
}