use tracing::{debug, info};
use super::{AudioData, AudioError, CHANNELS, SAMPLE_RATE};
#[repr(C)]
struct AXTSCKCaptureResult {
samples: *mut f32,
sample_count: i32,
sample_rate: f32,
channels: i32,
error_code: i32,
error_msg: [u8; 256],
}
extern "C" {
fn axt_sck_is_available() -> bool;
fn axt_sck_capture_system_audio(duration_secs: f32) -> AXTSCKCaptureResult;
fn axt_sck_free_result(result: *mut AXTSCKCaptureResult);
}
pub(super) fn sck_available() -> bool {
unsafe { axt_sck_is_available() }
}
pub(super) fn capture_system_audio_sck(duration_secs: f32) -> Result<AudioData, AudioError> {
info!(
duration = duration_secs,
"capturing system audio via ScreenCaptureKit (audio-only mode)"
);
let mut result = unsafe { axt_sck_capture_system_audio(duration_secs) };
if result.error_code != 0 {
let msg = error_msg_from_result(&result);
unsafe { axt_sck_free_result(&mut result) };
return Err(AudioError::Framework(msg));
}
if result.samples.is_null() || result.sample_count <= 0 {
unsafe { axt_sck_free_result(&mut result) };
debug!("SCK captured zero audio samples (no system audio playing?)");
return Ok(AudioData::silent(duration_secs));
}
let count = result.sample_count as usize;
let native_samples: Vec<f32> =
unsafe { std::slice::from_raw_parts(result.samples, count) }.to_vec();
let native_rate = result.sample_rate;
let native_channels = result.channels.max(1) as u16;
unsafe { axt_sck_free_result(&mut result) };
let mono_samples = if native_channels > 1 {
downmix_to_mono(&native_samples, native_channels)
} else {
native_samples
};
let (final_samples, final_rate) = if (native_rate - SAMPLE_RATE as f32).abs() > 1.0 {
let resampled = linear_resample(&mono_samples, native_rate as u32, SAMPLE_RATE);
(resampled, SAMPLE_RATE)
} else {
(mono_samples, native_rate as u32)
};
#[allow(clippy::cast_precision_loss)]
let actual_duration = final_samples.len() as f32 / final_rate as f32;
debug!(
native_rate,
native_channels,
final_samples = final_samples.len(),
actual_duration,
"SCK capture complete"
);
Ok(AudioData {
samples: final_samples,
sample_rate: final_rate,
channels: CHANNELS,
duration_secs: actual_duration.min(duration_secs),
})
}
fn error_msg_from_result(result: &AXTSCKCaptureResult) -> String {
let end = result
.error_msg
.iter()
.position(|&b| b == 0)
.unwrap_or(result.error_msg.len());
String::from_utf8_lossy(&result.error_msg[..end]).into_owned()
}
fn downmix_to_mono(samples: &[f32], channels: u16) -> Vec<f32> {
let ch = channels as usize;
if ch <= 1 {
return samples.to_vec();
}
samples
.chunks_exact(ch)
.map(|frame| frame.iter().sum::<f32>() / ch as f32)
.collect()
}
fn linear_resample(samples: &[f32], from_rate: u32, to_rate: u32) -> Vec<f32> {
if from_rate == to_rate || samples.is_empty() {
return samples.to_vec();
}
let ratio = f64::from(to_rate) / f64::from(from_rate);
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let out_len = (samples.len() as f64 * ratio) as usize;
let mut out = Vec::with_capacity(out_len);
for i in 0..out_len {
let src_pos = i as f64 / ratio;
let idx = src_pos as usize;
let frac = (src_pos - idx as f64) as f32;
let s0 = samples[idx.min(samples.len() - 1)];
let s1 = samples[(idx + 1).min(samples.len() - 1)];
out.push(s0 + frac * (s1 - s0));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sck_available_returns_bool() {
let _ = sck_available();
}
#[test]
fn downmix_to_mono_identity_for_mono() {
let mono = vec![0.5, -0.5, 0.25];
assert_eq!(downmix_to_mono(&mono, 1), mono);
}
#[test]
fn downmix_to_mono_averages_stereo() {
let stereo = vec![1.0, 0.0, 0.0, -1.0];
let mono = downmix_to_mono(&stereo, 2);
assert_eq!(mono.len(), 2);
assert!((mono[0] - 0.5).abs() < 1e-6);
assert!((mono[1] - (-0.5)).abs() < 1e-6);
}
#[test]
fn linear_resample_identity() {
let samples = vec![1.0, 2.0, 3.0, 4.0];
let resampled = linear_resample(&samples, 48000, 48000);
assert_eq!(resampled, samples);
}
#[test]
fn linear_resample_halves_rate() {
let samples = vec![0.0, 1.0, 0.0, -1.0];
let resampled = linear_resample(&samples, 48000, 24000);
assert_eq!(resampled.len(), 2);
}
#[test]
fn linear_resample_48k_to_16k() {
let samples: Vec<f32> = (0..48).map(|i| (i as f32) / 48.0).collect();
let resampled = linear_resample(&samples, 48000, 16000);
assert_eq!(resampled.len(), 16);
assert!(resampled[0].abs() < 0.05);
}
#[test]
fn error_msg_from_result_extracts_string() {
let mut result = AXTSCKCaptureResult {
samples: std::ptr::null_mut(),
sample_count: 0,
sample_rate: 0.0,
channels: 0,
error_code: 1,
error_msg: [0u8; 256],
};
let msg = b"test error";
result.error_msg[..msg.len()].copy_from_slice(msg);
assert_eq!(error_msg_from_result(&result), "test error");
}
}