Skip to content

Commit 7851f42

Browse files
committed
Fixing issues with audio capture for windows and linux
1 parent ec37f01 commit 7851f42

7 files changed

Lines changed: 193 additions & 53 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- Add audio file path to meetings table
2+
-- This allows tracking of recorded audio files for each meeting
3+
4+
ALTER TABLE meetings ADD COLUMN audio_file_path TEXT;
5+
6+
-- Index for searching meetings with audio files
7+
CREATE INDEX idx_meetings_audio_file ON meetings(audio_file_path) WHERE audio_file_path IS NOT NULL;

apps/desktop/src-tauri/src/adapters/audio/linux.rs

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,29 @@ use std::sync::{Arc, Mutex};
1111
use std::time::Duration;
1212

1313
/// Linux PulseAudio capture implementation
14+
///
15+
/// Captures system audio output using PulseAudio monitor sources.
16+
/// Uses @DEFAULT_MONITOR@ to capture what's playing through the speakers.
17+
///
18+
/// Audio format: 44100 Hz, 2 channels (stereo), 16-bit signed little-endian
1419
pub struct PulseAudioCapture {
1520
is_capturing: Arc<Mutex<bool>>,
1621
audio_buffer: Arc<Mutex<Vec<f32>>>,
22+
/// Audio format - placeholder until capture starts, then set to 44.1kHz stereo 16-bit
1723
format: AudioFormat,
1824
capture_handle: Option<tokio::task::JoinHandle<()>>,
1925
}
2026

2127
impl PulseAudioCapture {
2228
/// Creates a new PulseAudio capture instance
29+
///
30+
/// The format field is initialized to a default placeholder.
31+
/// Actual format (44.1kHz stereo 16-bit) is set when `start_capture()` is called.
2332
pub fn new() -> Self {
2433
Self {
2534
is_capturing: Arc::new(Mutex::new(false)),
2635
audio_buffer: Arc::new(Mutex::new(Vec::new())),
27-
format: AudioFormat::default(),
36+
format: AudioFormat::default(), // Placeholder, updated during start_capture()
2837
capture_handle: None,
2938
}
3039
}
@@ -49,20 +58,29 @@ impl AudioCapturePort for PulseAudioCapture {
4958
Ok(vec!["Default Monitor Source".to_string()])
5059
}
5160

52-
async fn start_capture(&mut self, _device_name: Option<String>) -> Result<()> {
53-
let mut is_capturing = self.is_capturing.lock().unwrap();
54-
if *is_capturing {
55-
return Err(AppError::AudioCapture(
56-
"Capture already in progress".to_string(),
57-
));
58-
}
61+
async fn start_capture(&mut self, device_name: Option<String>) -> Result<()> {
62+
{
63+
let mut is_capturing = self.is_capturing.lock().unwrap();
64+
if *is_capturing {
65+
return Err(AppError::AudioCapture(
66+
"Capture already in progress".to_string(),
67+
));
68+
}
5969

60-
*is_capturing = true;
61-
drop(is_capturing);
70+
*is_capturing = true;
71+
} // Drop is_capturing guard here
6272

6373
let is_capturing_clone = Arc::clone(&self.is_capturing);
6474
let audio_buffer_clone = Arc::clone(&self.audio_buffer);
6575

76+
// Determine which device to use for capture
77+
// Default to system monitor source if not specified
78+
let device = device_name.unwrap_or_else(|| "@DEFAULT_MONITOR@".to_string());
79+
80+
// Store format info to be updated after detection
81+
let format_info = Arc::new(Mutex::new(AudioFormat::default()));
82+
let format_info_clone = Arc::clone(&format_info);
83+
6684
// Spawn background task for audio capture
6785
let handle = tokio::task::spawn_blocking(move || {
6886
// Set up PulseAudio sample specification
@@ -72,13 +90,20 @@ impl AudioCapturePort for PulseAudioCapture {
7290
rate: 44100, // 44.1 kHz
7391
};
7492

93+
// Store the format
94+
*format_info_clone.lock().unwrap() = AudioFormat {
95+
sample_rate: spec.rate,
96+
channels: spec.channels,
97+
bits_per_sample: 16, // S16LE is 16-bit
98+
};
99+
75100
// Create a simple recording connection
76-
// Using None for device name uses the default monitor source
101+
// Use monitor source to capture system audio output
77102
let simple = match Simple::new(
78103
None, // Use default server
79104
"Meet-Scribe", // Application name
80105
Direction::Record, // Recording
81-
None, // Use default monitor device
106+
Some(&device), // Monitor source for system audio
82107
"Audio Capture", // Stream description
83108
&spec, // Sample spec
84109
None, // Use default channel map
@@ -93,6 +118,7 @@ impl AudioCapturePort for PulseAudioCapture {
93118
};
94119

95120
log::info!("PulseAudio capture initialized successfully");
121+
log::info!("Device: {}", device);
96122
log::info!("Format: {} Hz, {} channels, 16-bit", spec.rate, spec.channels);
97123

98124
// Buffer for reading samples (1024 frames at a time)
@@ -137,7 +163,16 @@ impl AudioCapturePort for PulseAudioCapture {
137163
});
138164

139165
self.capture_handle = Some(handle);
140-
log::info!("Audio capture started");
166+
167+
// Wait for format initialization to complete
168+
// Format is set to 44100 Hz, stereo, 16-bit in the background thread
169+
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
170+
171+
// Update our format from the initialized format
172+
self.format = format_info.lock().unwrap().clone();
173+
174+
log::info!("Audio capture started with format: {} Hz, {} channels, {} bits",
175+
self.format.sample_rate, self.format.channels, self.format.bits_per_sample);
141176
Ok(())
142177
}
143178

@@ -197,9 +232,11 @@ mod tests {
197232
fn test_default_format() {
198233
let capture = PulseAudioCapture::new();
199234
let format = capture.get_format();
200-
assert_eq!(format.sample_rate, 16000);
201-
assert_eq!(format.channels, 1);
202-
assert_eq!(format.bits_per_sample, 16);
235+
// Before capture starts, format is the default placeholder
236+
// Actual format is set during start_capture() to: 44100 Hz, stereo, 16-bit
237+
assert_eq!(format.sample_rate, 16000); // Placeholder before capture
238+
assert_eq!(format.channels, 1); // Placeholder before capture
239+
assert_eq!(format.bits_per_sample, 16); // Placeholder before capture
203240
}
204241

205242
#[tokio::test]

apps/desktop/src-tauri/src/adapters/audio/windows.rs

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,30 @@ use windows::Win32::System::Com::{
1818
};
1919

2020
/// Windows WASAPI audio capture implementation
21+
///
22+
/// Captures system audio output using WASAPI loopback mode.
23+
/// The audio format (sample rate, channels, bit depth) is auto-detected
24+
/// from the system's default audio device during `start_capture()`.
25+
///
26+
/// Typical Windows audio format: 48000 Hz, 2 channels, 32-bit float
2127
pub struct WasapiAudioCapture {
2228
is_capturing: Arc<Mutex<bool>>,
2329
audio_buffer: Arc<Mutex<Vec<f32>>>,
30+
/// Audio format - placeholder until capture starts, then auto-detected
2431
format: AudioFormat,
2532
capture_handle: Option<tokio::task::JoinHandle<()>>,
2633
}
2734

2835
impl WasapiAudioCapture {
2936
/// Creates a new WASAPI audio capture instance
37+
///
38+
/// The format field is initialized to a default placeholder.
39+
/// Actual format is detected when `start_capture()` is called.
3040
pub fn new() -> Self {
3141
Self {
3242
is_capturing: Arc::new(Mutex::new(false)),
3343
audio_buffer: Arc::new(Mutex::new(Vec::new())),
34-
format: AudioFormat::default(),
44+
format: AudioFormat::default(), // Placeholder, updated during start_capture()
3545
capture_handle: None,
3646
}
3747
}
@@ -61,9 +71,15 @@ impl WasapiAudioCapture {
6171
}
6272

6373
/// Initialize the audio client with the desired format
64-
fn initialize_audio_client(audio_client: &IAudioClient) -> Result<(WAVEFORMATEX, u16, u16)> {
74+
///
75+
/// Queries the WASAPI device for its mix format and initializes the audio client
76+
/// for loopback capture. Returns the detected format parameters which are used
77+
/// to update the WasapiAudioCapture.format field.
78+
///
79+
/// Returns: (WAVEFORMATEX, sample_rate, bits_per_sample)
80+
fn initialize_audio_client(audio_client: &IAudioClient) -> Result<(WAVEFORMATEX, u32, u16)> {
6581
unsafe {
66-
// Get the device's mix format
82+
// Get the device's mix format (auto-detected from system)
6783
let mix_format_ptr = audio_client
6884
.GetMixFormat()
6985
.map_err(|e| AppError::AudioCapture(format!("Failed to get mix format: {}", e)))?;
@@ -73,8 +89,8 @@ impl WasapiAudioCapture {
7389
}
7490

7591
let mix_format = *mix_format_ptr;
76-
let sample_rate = mix_format.nSamplesPerSec as u16;
77-
let bits_per_sample = mix_format.wBitsPerSample;
92+
let sample_rate = mix_format.nSamplesPerSec; // Actual system sample rate
93+
let bits_per_sample = mix_format.wBitsPerSample; // Actual bit depth
7894

7995
// Initialize the audio client for loopback capture
8096
let buffer_duration = 10_000_000; // 1 second in 100-nanosecond units
@@ -236,19 +252,24 @@ impl AudioCapturePort for WasapiAudioCapture {
236252
}
237253

238254
async fn start_capture(&mut self, _device_name: Option<String>) -> Result<()> {
239-
let mut is_capturing = self.is_capturing.lock().unwrap();
240-
if *is_capturing {
241-
return Err(AppError::AudioCapture(
242-
"Capture already in progress".to_string(),
243-
));
244-
}
255+
{
256+
let mut is_capturing = self.is_capturing.lock().unwrap();
257+
if *is_capturing {
258+
return Err(AppError::AudioCapture(
259+
"Capture already in progress".to_string(),
260+
));
261+
}
245262

246-
*is_capturing = true;
247-
drop(is_capturing);
263+
*is_capturing = true;
264+
} // Drop is_capturing guard here
248265

249266
let is_capturing_clone = Arc::clone(&self.is_capturing);
250267
let audio_buffer_clone = Arc::clone(&self.audio_buffer);
251268

269+
// Store format info to be updated after detection
270+
let format_info = Arc::new(Mutex::new(AudioFormat::default()));
271+
let format_info_clone = Arc::clone(&format_info);
272+
252273
// Spawn background task for audio capture
253274
let handle = tokio::task::spawn_blocking(move || {
254275
// Initialize COM for this thread
@@ -282,7 +303,8 @@ impl AudioCapturePort for WasapiAudioCapture {
282303
}
283304
};
284305

285-
// Initialize the audio client
306+
// Initialize the audio client and get the actual device format
307+
// This is where the format is detected from the WASAPI device
286308
let (format, sample_rate, bits_per_sample) = match Self::initialize_audio_client(&audio_client) {
287309
Ok(f) => f,
288310
Err(e) => {
@@ -293,6 +315,15 @@ impl AudioCapturePort for WasapiAudioCapture {
293315
}
294316
};
295317

318+
// IMPORTANT: Update format with actual detected values from the device
319+
// This replaces the default placeholder values with the real audio format
320+
let channels = format.nChannels;
321+
*format_info_clone.lock().unwrap() = AudioFormat {
322+
sample_rate, // e.g., 48000 Hz (detected from device)
323+
channels, // e.g., 2 (stereo, detected from device)
324+
bits_per_sample, // e.g., 32 bits (float, detected from device)
325+
};
326+
296327
// Get the capture client
297328
let capture_client: IAudioCaptureClient = match unsafe {
298329
audio_client.GetService::<IAudioCaptureClient>()
@@ -307,7 +338,7 @@ impl AudioCapturePort for WasapiAudioCapture {
307338
};
308339

309340
log::info!("WASAPI audio capture initialized successfully");
310-
log::info!("Format: {} Hz, {} bits", sample_rate, bits_per_sample);
341+
log::info!("Format: {} Hz, {} channels, {} bits", sample_rate, channels, bits_per_sample);
311342

312343
// Run the capture loop
313344
Self::capture_loop(
@@ -324,7 +355,17 @@ impl AudioCapturePort for WasapiAudioCapture {
324355
});
325356

326357
self.capture_handle = Some(handle);
327-
log::info!("Audio capture started");
358+
359+
// Wait for format detection to complete
360+
// The background thread detects the system's audio format and stores it in format_info
361+
// Typical Windows audio: 48000 Hz, stereo, 32-bit float
362+
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
363+
364+
// Update our format from the auto-detected format
365+
self.format = format_info.lock().unwrap().clone();
366+
367+
log::info!("Audio capture started with format: {} Hz, {} channels, {} bits",
368+
self.format.sample_rate, self.format.channels, self.format.bits_per_sample);
328369
Ok(())
329370
}
330371

@@ -384,9 +425,12 @@ mod tests {
384425
fn test_default_format() {
385426
let capture = WasapiAudioCapture::new();
386427
let format = capture.get_format();
387-
assert_eq!(format.sample_rate, 16000);
388-
assert_eq!(format.channels, 1);
389-
assert_eq!(format.bits_per_sample, 16);
428+
// Before capture starts, format is the default placeholder
429+
// Actual format is detected during start_capture() and varies by system
430+
// Typical Windows audio: 48000 Hz, 2 channels, 32 bits (float)
431+
assert_eq!(format.sample_rate, 16000); // Placeholder before capture
432+
assert_eq!(format.channels, 1); // Placeholder before capture
433+
assert_eq!(format.bits_per_sample, 16); // Placeholder before capture
390434
}
391435

392436
#[tokio::test]

apps/desktop/src-tauri/src/adapters/storage/sqlite.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ impl SqliteStorage {
3333
pub fn run_migrations(&self) -> Result<()> {
3434
use rusqlite_migration::{Migrations, M};
3535

36-
let migrations = Migrations::new(vec![M::up(include_str!(
37-
"../../../migrations/001_initial.sql"
38-
))]);
36+
let migrations = Migrations::new(vec![
37+
M::up(include_str!("../../../migrations/001_initial.sql")),
38+
M::up(include_str!("../../../migrations/002_add_audio_file_path.sql")),
39+
]);
3940

4041
let mut conn = self.conn.lock().unwrap();
4142
migrations.to_latest(&mut conn).map_err(|e| {
@@ -51,14 +52,15 @@ impl StoragePort for SqliteStorage {
5152
async fn create_meeting(&self, meeting: &Meeting) -> Result<i64> {
5253
let conn = self.conn.lock().unwrap();
5354
conn.execute(
54-
"INSERT INTO meetings (platform, title, start_time, end_time, participant_count, created_at)
55-
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
55+
"INSERT INTO meetings (platform, title, start_time, end_time, participant_count, audio_file_path, created_at)
56+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
5657
params![
5758
meeting.platform.to_string(),
5859
meeting.title,
5960
meeting.start_time,
6061
meeting.end_time,
6162
meeting.participant_count,
63+
meeting.audio_file_path,
6264
meeting.created_at,
6365
],
6466
)?;
@@ -68,7 +70,7 @@ impl StoragePort for SqliteStorage {
6870
async fn get_meeting(&self, id: i64) -> Result<Option<Meeting>> {
6971
let conn = self.conn.lock().unwrap();
7072
let mut stmt = conn.prepare(
71-
"SELECT id, platform, title, start_time, end_time, participant_count, created_at
73+
"SELECT id, platform, title, start_time, end_time, participant_count, audio_file_path, created_at
7274
FROM meetings WHERE id = ?1",
7375
)?;
7476

@@ -90,7 +92,8 @@ impl StoragePort for SqliteStorage {
9092
start_time: row.get(3)?,
9193
end_time: row.get(4)?,
9294
participant_count: row.get(5)?,
93-
created_at: row.get(6)?,
95+
audio_file_path: row.get(6)?,
96+
created_at: row.get(7)?,
9497
}))
9598
} else {
9699
Ok(None)
@@ -100,7 +103,7 @@ impl StoragePort for SqliteStorage {
100103
async fn list_meetings(&self, limit: Option<i32>, offset: Option<i32>) -> Result<Vec<Meeting>> {
101104
let conn = self.conn.lock().unwrap();
102105
let query = format!(
103-
"SELECT id, platform, title, start_time, end_time, participant_count, created_at
106+
"SELECT id, platform, title, start_time, end_time, participant_count, audio_file_path, created_at
104107
FROM meetings ORDER BY start_time DESC LIMIT ?1 OFFSET ?2"
105108
);
106109

@@ -121,7 +124,8 @@ impl StoragePort for SqliteStorage {
121124
start_time: row.get(3)?,
122125
end_time: row.get(4)?,
123126
participant_count: row.get(5)?,
124-
created_at: row.get(6)?,
127+
audio_file_path: row.get(6)?,
128+
created_at: row.get(7)?,
125129
})
126130
})?;
127131

@@ -137,13 +141,14 @@ impl StoragePort for SqliteStorage {
137141
let conn = self.conn.lock().unwrap();
138142
conn.execute(
139143
"UPDATE meetings SET platform = ?1, title = ?2, start_time = ?3, end_time = ?4,
140-
participant_count = ?5 WHERE id = ?6",
144+
participant_count = ?5, audio_file_path = ?6 WHERE id = ?7",
141145
params![
142146
meeting.platform.to_string(),
143147
meeting.title,
144148
meeting.start_time,
145149
meeting.end_time,
146150
meeting.participant_count,
151+
meeting.audio_file_path,
147152
meeting.id,
148153
],
149154
)?;

0 commit comments

Comments
 (0)