mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 21:30:39 -07:00
Merge branch 'main' into channels
This commit is contained in:
@@ -163,25 +163,67 @@ fn extract_audio_samples(sample_buffer: CMSampleBuffer) -> Result<Vec<f32>, Stri
|
||||
.audio_buffer_list()
|
||||
.ok_or_else(|| "Failed to get audio buffer list".to_string())?;
|
||||
|
||||
let mut samples = Vec::new();
|
||||
let buffers: Vec<_> = audio_buffer_list.iter().collect();
|
||||
let num_buffers = buffers.len();
|
||||
|
||||
if num_buffers == 0 {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
// Iterate through audio buffers
|
||||
for buffer in audio_buffer_list.iter() {
|
||||
// Get raw bytes and interpret as f32 samples
|
||||
// ScreenCaptureKit on macOS provides audio in Float32 format
|
||||
// The audio can be either:
|
||||
// - Interleaved (1 buffer with L,R,L,R,... samples)
|
||||
// - Planar (2 buffers, one for L channel, one for R channel)
|
||||
|
||||
if num_buffers == 1 {
|
||||
// Interleaved stereo or mono in a single buffer
|
||||
let buffer = &buffers[0];
|
||||
let data_bytes = buffer.data();
|
||||
let num_samples = data_bytes.len() / std::mem::size_of::<f32>();
|
||||
|
||||
if num_samples > 0 {
|
||||
unsafe {
|
||||
// Interpret bytes as f32 samples
|
||||
let data_ptr = data_bytes.as_ptr() as *const f32;
|
||||
let data = std::slice::from_raw_parts(data_ptr, num_samples);
|
||||
samples.extend_from_slice(data);
|
||||
return Ok(data.to_vec());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Planar format - separate buffer for each channel
|
||||
// We need to interleave them: L0, R0, L1, R1, ...
|
||||
let mut channel_data: Vec<Vec<f32>> = Vec::new();
|
||||
let mut max_samples = 0;
|
||||
|
||||
for buffer in &buffers {
|
||||
let data_bytes = buffer.data();
|
||||
let num_samples = data_bytes.len() / std::mem::size_of::<f32>();
|
||||
|
||||
if num_samples > 0 {
|
||||
unsafe {
|
||||
let data_ptr = data_bytes.as_ptr() as *const f32;
|
||||
let data = std::slice::from_raw_parts(data_ptr, num_samples);
|
||||
channel_data.push(data.to_vec());
|
||||
max_samples = max_samples.max(num_samples);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Interleave the channels
|
||||
let mut interleaved = Vec::with_capacity(max_samples * num_buffers);
|
||||
for i in 0..max_samples {
|
||||
for channel in &channel_data {
|
||||
if i < channel.len() {
|
||||
interleaved.push(channel[i]);
|
||||
} else {
|
||||
interleaved.push(0.0); // Pad with silence if needed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(interleaved);
|
||||
}
|
||||
|
||||
Ok(samples)
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
fn samples_to_wav(samples: &[f32], sample_rate: u32, channels: u16) -> Result<Vec<u8>, String> {
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct AudioCaptureState {
|
||||
pub sample_rate: Arc<Mutex<u32>>,
|
||||
pub channels: Arc<Mutex<u16>>,
|
||||
pub stop_tx: Arc<Mutex<Option<tokio::sync::mpsc::Sender<()>>>>,
|
||||
pub error: Arc<Mutex<Option<String>>>,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub stream: Arc<Mutex<Option<SCStream>>>,
|
||||
}
|
||||
@@ -29,6 +30,7 @@ impl AudioCaptureState {
|
||||
sample_rate: Arc::new(Mutex::new(44100)),
|
||||
channels: Arc::new(Mutex::new(2)),
|
||||
stop_tx: Arc::new(Mutex::new(None)),
|
||||
error: Arc::new(Mutex::new(None)),
|
||||
#[cfg(target_os = "macos")]
|
||||
stream: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
@@ -36,5 +38,6 @@ impl AudioCaptureState {
|
||||
|
||||
pub fn reset(&self) {
|
||||
*self.samples.lock().unwrap() = Vec::new();
|
||||
*self.error.lock().unwrap() = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use std::io::Cursor;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use wasapi::*;
|
||||
use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_MULTITHREADED};
|
||||
|
||||
pub async fn start_capture(
|
||||
state: &AudioCaptureState,
|
||||
@@ -19,6 +19,7 @@ pub async fn start_capture(
|
||||
let sample_rate_arc = state.sample_rate.clone();
|
||||
let channels_arc = state.channels.clone();
|
||||
let stop_tx = state.stop_tx.clone();
|
||||
let error_arc = state.error.clone();
|
||||
|
||||
// Use AtomicBool for stop signal (works with non-Send types)
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
@@ -36,13 +37,29 @@ pub async fn start_capture(
|
||||
// Spawn capture task on a dedicated thread (WASAPI COM objects are not Send)
|
||||
// All WASAPI objects must be created and used on the same thread
|
||||
thread::spawn(move || {
|
||||
// Initialize COM for this thread
|
||||
unsafe {
|
||||
let hr = CoInitializeEx(None, COINIT_MULTITHREADED);
|
||||
if hr.is_err() {
|
||||
eprintln!("Failed to initialize COM: {:?}", hr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure COM is uninitialized when thread exits
|
||||
let _com_guard = scopeguard::guard((), |_| unsafe {
|
||||
CoUninitialize();
|
||||
});
|
||||
|
||||
// Initialize WASAPI on this thread
|
||||
let device = match DeviceEnumerator::new()
|
||||
.and_then(|enumerator| enumerator.get_default_device(&Direction::Render))
|
||||
{
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get audio device: {}", e);
|
||||
let error_msg = format!("Failed to get audio device: {}", e);
|
||||
eprintln!("{}", error_msg);
|
||||
*error_arc.lock().unwrap() = Some(error_msg);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -50,7 +67,9 @@ pub async fn start_capture(
|
||||
let mut audio_client = match device.get_iaudioclient() {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get audio client: {}", e);
|
||||
let error_msg = format!("Failed to get audio client: {}", e);
|
||||
eprintln!("{}", error_msg);
|
||||
*error_arc.lock().unwrap() = Some(error_msg);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -58,7 +77,9 @@ pub async fn start_capture(
|
||||
let mix_format = match audio_client.get_mixformat() {
|
||||
Ok(format) => format,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get mix format: {}", e);
|
||||
let error_msg = format!("Failed to get mix format: {}", e);
|
||||
eprintln!("{}", error_msg);
|
||||
*error_arc.lock().unwrap() = Some(error_msg);
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -69,27 +90,53 @@ pub async fn start_capture(
|
||||
*sample_rate_arc.lock().unwrap() = mix_format.get_samplespersec();
|
||||
*channels_arc.lock().unwrap() = mix_format.get_nchannels();
|
||||
|
||||
// Get device period
|
||||
let (_def_period, min_period) = match audio_client.get_device_period() {
|
||||
Ok(periods) => periods,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get device period: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize audio client for loopback with StreamMode
|
||||
// For loopback mode: get Render device, initialize with Capture direction
|
||||
// This triggers AUDCLNT_STREAMFLAGS_LOOPBACK in the wasapi crate
|
||||
let stream_mode = StreamMode::EventsShared {
|
||||
autoconvert: false,
|
||||
buffer_duration_hns: 0, // 0 = use default buffer size
|
||||
autoconvert: true, // Enable automatic format conversion
|
||||
buffer_duration_hns: min_period, // Use minimum period
|
||||
};
|
||||
|
||||
if let Err(e) = audio_client.initialize_client(&mix_format, &Direction::Capture, &stream_mode) {
|
||||
eprintln!("Failed to initialize audio client: {}", e);
|
||||
let error_msg = format!("Failed to initialize audio client: {}", e);
|
||||
eprintln!("{}", error_msg);
|
||||
*error_arc.lock().unwrap() = Some(error_msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up event handle for EventsShared mode
|
||||
let h_event = match audio_client.set_get_eventhandle() {
|
||||
Ok(event) => event,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to set event handle: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let capture_client = match audio_client.get_audiocaptureclient() {
|
||||
Ok(client) => client,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to get capture client: {}", e);
|
||||
let error_msg = format!("Failed to get capture client: {}", e);
|
||||
eprintln!("{}", error_msg);
|
||||
*error_arc.lock().unwrap() = Some(error_msg);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(e) = audio_client.start_stream() {
|
||||
eprintln!("Failed to start stream: {}", e);
|
||||
let error_msg = format!("Failed to start stream: {}", e);
|
||||
eprintln!("{}", error_msg);
|
||||
*error_arc.lock().unwrap() = Some(error_msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,8 +192,10 @@ pub async fn start_capture(
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep briefly to avoid busy-waiting
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
// Wait for event signal (with timeout to allow checking stop flag)
|
||||
if h_event.wait_for_event(100).is_err() {
|
||||
// Timeout is expected - just continue to check stop flag
|
||||
}
|
||||
}
|
||||
|
||||
// Stop the stream when done
|
||||
@@ -176,13 +225,18 @@ pub async fn stop_capture(state: &AudioCaptureState) -> Result<String, String> {
|
||||
// Wait a bit for capture to stop
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Check if there was an error during capture
|
||||
if let Some(error) = state.error.lock().unwrap().as_ref() {
|
||||
return Err(error.clone());
|
||||
}
|
||||
|
||||
// Get samples
|
||||
let samples = state.samples.lock().unwrap().clone();
|
||||
let sample_rate = *state.sample_rate.lock().unwrap();
|
||||
let channels = *state.channels.lock().unwrap();
|
||||
|
||||
if samples.is_empty() {
|
||||
return Err("No audio samples captured".to_string());
|
||||
return Err("No audio samples captured. Make sure audio is playing on your system during recording.".to_string());
|
||||
}
|
||||
|
||||
// Convert to WAV
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pub mod audio_capture;
|
||||
@@ -400,7 +400,10 @@ pub fn run() {
|
||||
.manage(audio_output::AudioOutputState::new())
|
||||
.setup(|app| {
|
||||
#[cfg(desktop)]
|
||||
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||
{
|
||||
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||
app.handle().plugin(tauri_plugin_process::init())?;
|
||||
}
|
||||
|
||||
// Hide title bar icon on Windows
|
||||
#[cfg(windows)]
|
||||
|
||||
Reference in New Issue
Block a user