use std::path::PathBuf; use std::sync::atomic::{AtomicU64, Ordering}; use tauri::Manager; use crate::commands::security::ensure_main_window; use lumotia_audio::{DeviceInfo, MicrophoneCapture}; /// Enumerate every input device available to cpal, with metadata for the /// Settings device-picker UI. Includes a flag for likely PulseAudio / /// PipeWire monitor sources so the UI can warn the user. #[tauri::command] pub async fn list_audio_devices(window: tauri::WebviewWindow) -> Result, String> { ensure_main_window(&window)?; tokio::task::spawn_blocking(MicrophoneCapture::list_devices) .await .map_err(|e| format!("join error: {e}"))? .map_err(|e| e.to_string()) } /// Resolve the destination path for a new live-capture recording, /// ensuring the parent directory exists. Used by /// `start_live_transcription_session` to hand the path to the progressive /// WAV writer before any samples arrive (brief item #19). pub fn resolve_recording_path( app: &tauri::AppHandle, output_folder: Option<&str>, ) -> Result { let recordings_dir = match output_folder.map(str::trim).filter(|s| !s.is_empty()) { Some(folder) => PathBuf::from(folder), None => app .path() .app_local_data_dir() .map_err(|e: tauri::Error| e.to_string())? .join("recordings"), }; std::fs::create_dir_all(&recordings_dir) .map_err(|e| format!("Failed to create recordings dir: {e}"))?; Ok(recordings_dir.join(recording_filename())) } /// Deterministic recording filename generator. Combines three fields /// for absolute uniqueness across rapid calls: /// /// - wall-clock seconds since the epoch — human-readable and /// sortable; /// - the sub-second nanosecond component — defeats same-second /// collisions; /// - a process-lifetime atomic counter — defeats even same-nanosecond /// collisions, which `SystemTime::now()` alone cannot guarantee /// (two calls in the same clock tick can return identical nanos). /// /// Format: `magnotia---.wav`, e.g. /// `magnotia-1776828000-123456789-0000.wav`. fn recording_filename() -> String { let duration = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default(); let secs = duration.as_secs(); let nanos = duration.subsec_nanos(); let counter = RECORDING_COUNTER.fetch_add(1, Ordering::SeqCst); format!("magnotia-{secs}-{nanos:09}-{counter:04}.wav") } /// Process-lifetime monotonic counter for `recording_filename`. Starts /// at 0 on each Magnotia launch; wall-clock secs/nanos still advance across /// restarts, so cross-launch collisions are already impossible — the /// counter is the last-mile guarantee against within-launch same-tick /// collisions. static RECORDING_COUNTER: AtomicU64 = AtomicU64::new(0); #[cfg(test)] mod tests { use super::recording_filename; #[test] fn recording_filenames_are_unique_across_rapid_calls() { // Regression for the 2026-04-22 review AND the review-of- // review MINOR: SystemTime::now() alone cannot guarantee // uniqueness under tight loops on every OS clock resolution, // so the filename now includes a process-lifetime atomic // counter. With the counter, uniqueness is absolute across // any number of in-process calls. let mut names = std::collections::HashSet::new(); for _ in 0..1024 { names.insert(recording_filename()); } assert_eq!( names.len(), 1024, "every filename must be unique (counter-backed guarantee)" ); } #[test] fn recording_filename_has_expected_shape() { let name = recording_filename(); assert!(name.starts_with("magnotia-")); assert!(name.ends_with(".wav")); // Shape: magnotia--<9 digits>-<>=4 digits>.wav let rest = name .strip_prefix("magnotia-") .and_then(|s| s.strip_suffix(".wav")) .expect("shape prefix/suffix"); let parts: Vec<&str> = rest.split('-').collect(); assert_eq!( parts.len(), 3, "expected three '-' separated parts, got {parts:?}" ); assert!( parts[0].chars().all(|c| c.is_ascii_digit()), "secs is digits" ); assert_eq!( parts[1].len(), 9, "nanos component is zero-padded to 9 digits" ); assert!(parts[1].chars().all(|c| c.is_ascii_digit())); assert!( parts[2].len() >= 4, "counter component is zero-padded to >=4 digits" ); assert!(parts[2].chars().all(|c| c.is_ascii_digit())); } }