Skip to main content

rustriff_lib/commands/
latency_testing.rs

1//! Tauri commands that expose latency measurement to the frontend.
2//!
3//! Each command in this module is a thin adapter over
4//! [`AudioLatencyMeasurementService`].  They are responsible for:
5//!
6//! - Locking (or releasing) the [`AudioService`] mutex at the right time.
7//! - Logging results via `tracing` so they appear in the backend console.
8//! - Returning serialisable DTOs across the IPC boundary.
9//!
10//! # Command overview
11//!
12//! | Command | Tauri invoke name | What it measures |
13//! |---|---|---|
14//! | [`test_gain_latency`] | `test_gain_latency` | Gain processor CPU cost (logs only, no return value) |
15//! | [`measure_all_dsp_cpu_timings`] | `measure_all_dsp_cpu_timings` | CPU cost of all DSP processors |
16//! | [`measure_all_dsp_algorithmic_latency`] | `measure_all_dsp_algorithmic_latency` | Algorithmic (design) delay per processor |
17//! | [`measure_buffer_latency`] | `measure_buffer_latency` | I/O buffer delay from stream config |
18//! | [`measure_round_trip_latency`] | `measure_round_trip_latency` | True end-to-end hardware round-trip |
19//!
20//! [`AudioLatencyMeasurementService`]: crate::services::audio_latency_measurement_service::AudioLatencyMeasurementService
21//! [`AudioService`]: crate::services::audio_service::AudioService
22
23use crate::domain::dto::algorithmic_latency_dto::AlgorithmicLatencyDto;
24use crate::domain::dto::buffer_latency_dto::BufferLatencyDto;
25use crate::domain::dto::execution_timing_dto::ExecutionTimingDto;
26use crate::domain::dto::round_trip_latency_dto::RoundTripLatencyDto;
27use crate::services::audio_latency_measurement_service::AudioLatencyMeasurementService;
28use crate::services::audio_service::AudioService;
29use std::sync::Mutex;
30use tracing::info;
31
32/// Measures the CPU execution impact of the [`GainProcessor`] and logs the result.
33///
34/// This command is intended for quick developer diagnostics — it prints the measurement
35/// to the backend log but does **not** return a value to the frontend.  Use
36/// [`measure_all_dsp_cpu_timings`] when you need a structured result.
37///
38/// The measurement uses a block size of 2 048 samples, which is large enough to
39/// suppress timer-call overhead while completing in well under a second.
40///
41/// # Errors
42///
43/// Returns `Err` if the [`AudioService`] mutex cannot be locked.
44///
45/// [`GainProcessor`]: crate::services::processors::gain::gain_processor::GainProcessor
46/// [`AudioService`]: crate::services::audio_service::AudioService
47#[tauri::command]
48pub fn test_gain_latency(
49    audio_service: tauri::State<'_, Mutex<AudioService>>,
50) -> Result<(), String> {
51    let service = audio_service
52        .lock()
53        .map_err(|_| "Failed to lock audio service".to_string())?;
54
55    let added_us_per_sample = AudioLatencyMeasurementService::measure_gain_latency(&service, 2048);
56
57    info!(
58        "Gain processor execution impact: {:.6} µs/sample",
59        added_us_per_sample
60    );
61    println!(
62        "Gain processor execution impact: {:.6} µs/sample",
63        added_us_per_sample
64    );
65
66    Ok(())
67}
68
69/// Measures the CPU execution cost of every processor in the active DSP chain.
70///
71/// Benchmarks the Gain, Tone Stack, and Master Volume processors in signal-chain order
72/// using a block size of 2 048 samples.  Each result is the *net* added cost relative
73/// to a zero-work passthrough, clamped to ≥ 0 µs/sample.
74///
75/// Results are both logged at `info` level and returned to the frontend as a
76/// `Vec<ExecutionTimingDto>`.
77///
78/// # Returns
79///
80/// `Ok(timings)` — a vector of exactly three [`ExecutionTimingDto`] entries in
81/// signal-chain order: Gain → Tone Stack → Master Volume.
82///
83/// # Errors
84///
85/// Returns `Err` if the [`AudioService`] mutex cannot be locked.
86///
87/// [`ExecutionTimingDto`]: crate::domain::dto::execution_timing_dto::ExecutionTimingDto
88/// [`AudioService`]: crate::services::audio_service::AudioService
89#[tauri::command]
90pub fn measure_all_dsp_cpu_timings(
91    audio_service: tauri::State<'_, Mutex<AudioService>>,
92) -> Result<Vec<ExecutionTimingDto>, String> {
93    let service = audio_service
94        .lock()
95        .map_err(|_| "Failed to lock audio service".to_string())?;
96
97    let timings = AudioLatencyMeasurementService::measure_all_dsp_timings(&service, 2048);
98
99    for timing in &timings {
100        info!(
101            processor = timing.processor_name,
102            execution_us_per_sample = timing.execution_us_per_sample,
103            "DSP chain processor timing"
104        );
105    }
106
107    Ok(timings)
108}
109
110/// Returns the algorithmic (design-inherent) delay for every processor in the DSP chain.
111///
112/// Algorithmic latency is the sample delay an effect introduces by design — e.g. a
113/// look-ahead limiter adds a fixed number of samples regardless of CPU speed.
114///
115/// For the current chain (Gain → Tone Stack → Master Volume) all values are **zero**
116/// because no processor uses a delay line or look-ahead buffer.  This command still
117/// exists to provide the correct data shape for the developer UI and to remain correct
118/// when future processors with non-zero delay are added.
119///
120/// Results are both logged at `info` level and returned to the frontend.
121///
122/// # Returns
123///
124/// `Ok(latency)` — a vector of exactly three [`AlgorithmicLatencyDto`] entries:
125/// Gain → Tone Stack → Master Volume, each with `latency_samples = 0`.
126///
127/// # Errors
128///
129/// Returns `Err` if the [`AudioService`] mutex cannot be locked.
130///
131/// [`AlgorithmicLatencyDto`]: crate::domain::dto::algorithmic_latency_dto::AlgorithmicLatencyDto
132/// [`AudioService`]: crate::services::audio_service::AudioService
133#[tauri::command]
134pub fn measure_all_dsp_algorithmic_latency(
135    audio_service: tauri::State<'_, Mutex<AudioService>>,
136) -> Result<Vec<AlgorithmicLatencyDto>, String> {
137    let service = audio_service
138        .lock()
139        .map_err(|_| "Failed to lock audio service".to_string())?;
140
141    let latency = AudioLatencyMeasurementService::measure_all_dsp_algorithmic_latency(&service);
142
143    for item in &latency {
144        info!(
145            processor = item.processor_name,
146            latency_samples = item.latency_samples,
147            latency_ms = item.latency_ms,
148            "DSP chain processor algorithmic latency"
149        );
150    }
151
152    Ok(latency)
153}
154
155/// Estimates the I/O buffer latency from the current CPAL stream configuration.
156///
157/// Reads the configured frame count for both the input and output streams and converts
158/// them to milliseconds using `(frames / sample_rate) × 1000`.  If either stream uses
159/// [`BufferSize::Default`], a fallback of 256 frames is used.
160///
161/// The result is logged at `info` level and returned to the frontend.
162///
163/// # Returns
164///
165/// `Ok(latency)` — a [`BufferLatencyDto`] with `input_buffer_latency_ms`,
166/// `output_buffer_latency_ms`, and `total_buffer_latency_ms`.
167///
168/// # Errors
169///
170/// Returns `Err` if the [`AudioService`] mutex cannot be locked.
171///
172/// [`BufferSize::Default`]: cpal::BufferSize::Default
173/// [`BufferLatencyDto`]: crate::domain::dto::buffer_latency_dto::BufferLatencyDto
174/// [`AudioService`]: crate::services::audio_service::AudioService
175#[tauri::command]
176pub fn measure_buffer_latency(
177    audio_service: tauri::State<'_, Mutex<AudioService>>,
178) -> Result<BufferLatencyDto, String> {
179    let service = audio_service
180        .lock()
181        .map_err(|_| "Failed to lock audio service".to_string())?;
182
183    let latency = AudioLatencyMeasurementService::measure_buffer_latency(&service);
184
185    info!(
186        input_buffer_latency_ms = latency.input_buffer_latency_ms,
187        output_buffer_latency_ms = latency.output_buffer_latency_ms,
188        total_buffer_latency_ms = latency.total_buffer_latency_ms,
189        "I/O buffer latency"
190    );
191
192    Ok(latency)
193}
194
195/// Measures true end-to-end round-trip latency using dedicated CPAL streams.
196///
197/// This is the only latency command that performs a **real hardware measurement** rather
198/// than an analytical estimate.  The procedure is:
199///
200/// 1. The [`AudioService`] mutex is locked just long enough to clone the handler arc,
201///    then **released** so the main loopback and UI remain unblocked.
202/// 2. A dedicated OS thread is spawned that calls
203///    [`AudioLatencyMeasurementService::measure_round_trip_latency`], which in turn
204///    opens its own CPAL input/output streams, runs calibration and impulse detection,
205///    and returns the averaged result.
206/// 3. The command blocks until the thread finishes (typically 3–15 s depending on
207///    warmup and timeout settings), then logs and returns the result.
208///
209/// # Physical requirement
210///
211/// The audio interface output must be physically (or virtually) connected back to its
212/// input.  Without this loopback the impulse can never be detected and the measurement
213/// will time out.
214///
215/// # Returns
216///
217/// `Ok(result)` — a [`RoundTripLatencyDto`] with:
218/// - `is_valid = true` and `latency_ms` set on success.
219/// - `is_valid = false` and a human-readable `error` on failure (e.g. timeout or no echo).
220///
221/// # Errors
222///
223/// Returns `Err` only if the [`AudioService`] mutex cannot be locked or the
224/// measurement thread panics unexpectedly.
225///
226/// [`AudioService`]: crate::services::audio_service::AudioService
227/// [`AudioLatencyMeasurementService::measure_round_trip_latency`]: crate::services::audio_latency_measurement_service::AudioLatencyMeasurementService::measure_round_trip_latency
228/// [`RoundTripLatencyDto`]: crate::domain::dto::round_trip_latency_dto::RoundTripLatencyDto
229#[tauri::command]
230pub fn measure_round_trip_latency(
231    audio_service: tauri::State<'_, Mutex<AudioService>>,
232) -> Result<RoundTripLatencyDto, String> {
233    let handler = {
234        let service = audio_service
235            .lock()
236            .map_err(|_| "Failed to lock audio service".to_string())?;
237        service.audio_handler().clone()
238    };
239
240    let latency = std::thread::spawn(move || {
241        AudioLatencyMeasurementService::measure_round_trip_latency(handler.as_ref())
242    })
243    .join()
244    .map_err(|_| "Round-trip measurement thread panicked".to_string())?;
245
246    if latency.is_valid {
247        info!(
248            round_trip_latency_ms = latency.latency_ms,
249            "Round-trip latency measurement"
250        );
251    } else {
252        info!(
253            error = latency.error.clone(),
254            "Round-trip latency measurement failed"
255        );
256    }
257
258    Ok(latency)
259}