Skip to main content

rustriff_lib/commands/
analyzer.rs

1use crate::domain::dto::spectrum_contract_dto::SpectrumContractDto;
2use crate::domain::dto::spectrum_snapshot_dto::SpectrumSnapshotDto;
3use crate::services::analyzers::spectrum_analyzer_service::SpectrumAnalyzerService;
4use crate::services::audio_service::AudioService;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::{Arc, Mutex};
7use tauri::Emitter;
8use tokio::time::{interval, Duration};
9
10/// Tauri event name emitted by the backend when a new spectrum frame is available.
11const LIVE_SPECTRUM_EVENT: &str = "live-spectrum";
12/// Target interval for push-streamed spectrum frames (about 60 FPS).
13const STREAM_INTERVAL_MS: u64 = 16;
14
15/// Runtime handle for one live-spectrum stream task.
16///
17/// Each task owns its own shutdown flag to avoid races when a new stream starts
18/// before the previous task has observed cancellation.
19struct StreamTask {
20    handle: tauri::async_runtime::JoinHandle<()>,
21    shutdown: Arc<AtomicBool>,
22}
23
24/// Shared state for the analyzer stream task.
25///
26/// The task is started by `start_live_spectrum_stream` and stopped by either
27/// `stop_live_spectrum_stream` or when the target window can no longer receive events.
28#[derive(Default)]
29pub struct SpectrumStreamState {
30    task: Mutex<Option<StreamTask>>,
31}
32
33/// Lower bound for analyzer frequencies in Hz, shared with frontend chart config.
34const MIN_ANALYZER_FREQ_HZ: f32 = 20.0;
35/// Upper frequency bound pushed to frontend chart config.
36const MAX_ANALYZER_FREQ_HZ: f32 = 20_000.0;
37/// Lower clamp for displayed magnitudes (dBFS), shared with frontend chart config.
38const MIN_DB: f32 = -90.0;
39/// Upper clamp for displayed magnitudes (dBFS), shared with frontend chart config.
40const MAX_DB: f32 = 6.0;
41
42/// Returns a single, immediate spectrum snapshot.
43///
44/// This command is useful for first paint / fallback reads before the push stream
45/// starts delivering `live-spectrum` events.
46///
47/// FFT analysis is offloaded to a blocking task so the async command handler never
48/// stalls the Tauri runtime thread.
49#[tauri::command]
50pub async fn get_live_spectrum(
51    audio_service: tauri::State<'_, Mutex<AudioService>>,
52) -> Result<SpectrumSnapshotDto, String> {
53    let tap = {
54        let service = audio_service
55            .lock()
56            .map_err(|_| "Failed to lock audio service".to_string())?;
57        service.spectrum_tap().clone()
58    };
59
60    tauri::async_runtime::spawn_blocking(move || SpectrumAnalyzerService::analyze_tap(tap.as_ref()))
61        .await
62        .map_err(|e| format!("FFT analysis task failed: {e}"))
63}
64
65/// Returns static analyzer metadata consumed by frontend chart/state code.
66#[tauri::command]
67pub fn get_spectrum_contract() -> SpectrumContractDto {
68    SpectrumContractDto {
69        live_spectrum_event: LIVE_SPECTRUM_EVENT.to_string(),
70        min_db: MIN_DB,
71        max_db: MAX_DB,
72        min_frequency_hz: MIN_ANALYZER_FREQ_HZ,
73        max_frequency_hz: MAX_ANALYZER_FREQ_HZ,
74    }
75}
76
77/// Starts (or restarts) push-based live spectrum streaming for the calling window.
78///
79/// Behavior:
80/// - Captures the current shared `SpectrumTap` from `AudioService`.
81/// - Signals any previously running stream task to shut down using that task's own
82///   cancellation flag, then replaces it.
83/// - Spawns a background loop that analyzes the tap and emits `live-spectrum`
84///   events at `STREAM_INTERVAL_MS` cadence.
85/// - Automatically exits when event emission fails (for example, when window closes).
86#[tauri::command]
87pub fn start_live_spectrum_stream(
88    window: tauri::Window,
89    audio_service: tauri::State<'_, Mutex<AudioService>>,
90    stream_state: tauri::State<'_, SpectrumStreamState>,
91) -> Result<(), String> {
92    let tap: Arc<_> = {
93        let service = audio_service
94            .lock()
95            .map_err(|_| "Failed to lock audio service".to_string())?;
96        service.spectrum_tap().clone()
97    };
98
99    let shutdown = Arc::new(AtomicBool::new(false));
100
101    {
102        let mut guard = stream_state
103            .task
104            .lock()
105            .map_err(|_| "Failed to lock spectrum stream state".to_string())?;
106
107        if let Some(previous) = guard.take() {
108            previous.shutdown.store(true, Ordering::Relaxed);
109            previous.handle.abort();
110        }
111
112        let task_shutdown = Arc::clone(&shutdown);
113        let handle = tauri::async_runtime::spawn(async move {
114            let mut ticker = interval(Duration::from_millis(STREAM_INTERVAL_MS));
115            loop {
116                ticker.tick().await;
117                if task_shutdown.load(Ordering::Relaxed) {
118                    break;
119                }
120
121                let tap_ref = Arc::clone(&tap);
122                let snapshot = tauri::async_runtime::spawn_blocking(move || {
123                    SpectrumAnalyzerService::analyze_tap(tap_ref.as_ref())
124                })
125                .await;
126
127                match snapshot {
128                    Ok(data) => {
129                        if window.emit(LIVE_SPECTRUM_EVENT, data).is_err() {
130                            break;
131                        }
132                    }
133                    Err(_) => break,
134                }
135            }
136        });
137
138        guard.replace(StreamTask { handle, shutdown });
139    }
140
141    Ok(())
142}
143
144/// Stops the active live spectrum stream task, if one exists.
145///
146/// This is safe to call repeatedly; when no task is active it becomes a no-op.
147/// The active task is signaled to stop using its own cancellation flag.
148#[tauri::command]
149pub fn stop_live_spectrum_stream(
150    stream_state: tauri::State<'_, SpectrumStreamState>,
151) -> Result<(), String> {
152    if let Some(task) = stream_state
153        .task
154        .lock()
155        .map_err(|_| "Failed to lock spectrum stream state".to_string())?
156        .take()
157    {
158        task.shutdown.store(true, Ordering::Relaxed);
159        task.handle.abort();
160    }
161
162    Ok(())
163}