|
37 | 37 | //! ).expect("failed to build duplex stream"); |
38 | 38 | //! ``` |
39 | 39 |
|
40 | | -use crate::{PauseStreamError, PlayStreamError, SampleRate, StreamInstant}; |
41 | | - |
42 | | -/// Hardware timestamp information from the audio device. |
43 | | -/// |
44 | | -/// This provides precise timing information from the audio hardware, essential for |
45 | | -/// sample-accurate synchronization between input and output, and for correlating |
46 | | -/// audio timing with other system events. |
47 | | -/// |
48 | | -/// # Detecting Xruns |
49 | | -/// |
50 | | -/// Applications can detect xruns (buffer underruns/overruns) by tracking the |
51 | | -/// `sample_time` field across callbacks. Under normal operation, `sample_time` |
52 | | -/// advances by exactly the buffer size each callback. A larger jump indicates |
53 | | -/// missed buffers: |
54 | | -/// |
55 | | -/// ```ignore |
56 | | -/// let mut last_sample_time: Option<f64> = None; |
57 | | -/// |
58 | | -/// // In your callback: |
59 | | -/// if let Some(last) = last_sample_time { |
60 | | -/// let expected = last + buffer_size as f64; |
61 | | -/// let discontinuity = (info.timestamp.sample_time - expected).abs(); |
62 | | -/// if discontinuity > 1.0 { |
63 | | -/// println!("Xrun detected: {} samples missed", discontinuity); |
64 | | -/// } |
65 | | -/// } |
66 | | -/// last_sample_time = Some(info.timestamp.sample_time); |
67 | | -/// ``` |
68 | | -#[derive(Clone, Copy, Debug, PartialEq)] |
69 | | -pub struct AudioTimestamp { |
70 | | - /// Hardware sample counter from the device clock. |
71 | | - /// |
72 | | - /// This is the authoritative position from the device's clock and increments |
73 | | - /// by the buffer size each callback. Use this for xrun detection by tracking |
74 | | - /// discontinuities. |
75 | | - /// |
76 | | - /// This is an f64 to allow for sub-sample precision in rate-adjusted scenarios. |
77 | | - /// For most purposes, cast to i64 for an integer value. |
78 | | - pub sample_time: f64, |
79 | | - |
80 | | - /// System host time reference (platform-specific high-resolution timer). |
81 | | - /// |
82 | | - /// Can be used to correlate audio timing with other system events or for |
83 | | - /// debugging latency issues. |
84 | | - pub host_time: u64, |
85 | | - |
86 | | - /// Clock rate scalar (1.0 = nominal rate). |
87 | | - /// |
88 | | - /// Indicates if the hardware clock is running faster or slower than nominal. |
89 | | - /// Useful for applications that need to compensate for clock drift when |
90 | | - /// synchronizing with external sources. |
91 | | - pub rate_scalar: f64, |
92 | | - |
93 | | - /// Callback timestamp from cpal's existing timing system. |
94 | | - /// |
95 | | - /// This provides compatibility with cpal's existing `StreamInstant` timing |
96 | | - /// infrastructure. |
97 | | - pub callback_instant: StreamInstant, |
98 | | -} |
99 | | - |
100 | | -impl AudioTimestamp { |
101 | | - /// Create a new AudioTimestamp. |
102 | | - pub fn new( |
103 | | - sample_time: f64, |
104 | | - host_time: u64, |
105 | | - rate_scalar: f64, |
106 | | - callback_instant: StreamInstant, |
107 | | - ) -> Self { |
108 | | - Self { |
109 | | - sample_time, |
110 | | - host_time, |
111 | | - rate_scalar, |
112 | | - callback_instant, |
113 | | - } |
114 | | - } |
115 | | - |
116 | | - /// Get the sample position as an integer. |
117 | | - /// |
118 | | - /// This rounds the hardware sample time to the nearest integer. The result |
119 | | - /// is suitable for use as a timeline position or for sample-accurate event |
120 | | - /// scheduling. |
121 | | - #[inline] |
122 | | - pub fn sample_position(&self) -> i64 { |
123 | | - self.sample_time.round() as i64 |
124 | | - } |
125 | | - |
126 | | - /// Check if the clock is running at nominal rate. |
127 | | - /// |
128 | | - /// Returns `true` if `rate_scalar` is very close to 1.0 (within 0.0001). |
129 | | - #[inline] |
130 | | - pub fn is_nominal_rate(&self) -> bool { |
131 | | - (self.rate_scalar - 1.0).abs() < 0.0001 |
132 | | - } |
133 | | -} |
134 | | - |
135 | | -impl Default for AudioTimestamp { |
136 | | - fn default() -> Self { |
137 | | - Self { |
138 | | - sample_time: 0.0, |
139 | | - host_time: 0, |
140 | | - rate_scalar: 1.0, |
141 | | - callback_instant: StreamInstant::new(0, 0), |
142 | | - } |
143 | | - } |
144 | | -} |
| 40 | +use crate::{SampleRate, StreamInstant}; |
145 | 41 |
|
146 | 42 | /// Information passed to duplex callbacks. |
147 | 43 | /// |
148 | | -/// This contains timing information and metadata about the current audio buffer, |
149 | | -/// including latency-adjusted timestamps for input capture and output playback. |
150 | | -#[derive(Clone, Copy, Debug)] |
| 44 | +/// This contains timing information for the current audio buffer, combining |
| 45 | +/// both input and output timing similar to [`InputCallbackInfo`](crate::InputCallbackInfo) |
| 46 | +/// and [`OutputCallbackInfo`](crate::OutputCallbackInfo). |
| 47 | +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] |
151 | 48 | pub struct DuplexCallbackInfo { |
152 | | - /// Hardware timestamp for this callback. |
153 | | - pub timestamp: AudioTimestamp, |
| 49 | + /// The instant the stream's data callback was invoked. |
| 50 | + pub callback: StreamInstant, |
154 | 51 |
|
155 | | - /// Estimated time when the input audio was captured. |
| 52 | + /// The instant that input data was captured from the device. |
156 | 53 | /// |
157 | | - /// This is calculated by subtracting the device latency from the callback time, |
158 | | - /// representing when the input samples were actually captured by the hardware. |
| 54 | + /// This is calculated by subtracting the input device latency from the callback time, |
| 55 | + /// representing when the input samples were actually captured by the hardware (e.g., by an ADC). |
159 | 56 | pub capture: StreamInstant, |
160 | 57 |
|
161 | | - /// Estimated time when the output audio will be played. |
| 58 | + /// The predicted instant that output data will be delivered to the device for playback. |
162 | 59 | /// |
163 | | - /// This is calculated by adding the device latency to the callback time, |
164 | | - /// representing when the output samples will actually reach the hardware. |
| 60 | + /// This is calculated by adding the output device latency to the callback time, |
| 61 | + /// representing when the output samples will actually be played by the hardware (e.g., by a DAC). |
165 | 62 | pub playback: StreamInstant, |
166 | 63 | } |
167 | 64 |
|
168 | 65 | impl DuplexCallbackInfo { |
169 | 66 | /// Create a new DuplexCallbackInfo. |
170 | | - pub fn new(timestamp: AudioTimestamp, capture: StreamInstant, playback: StreamInstant) -> Self { |
| 67 | + pub fn new(callback: StreamInstant, capture: StreamInstant, playback: StreamInstant) -> Self { |
171 | 68 | Self { |
172 | | - timestamp, |
| 69 | + callback, |
173 | 70 | capture, |
174 | 71 | playback, |
175 | 72 | } |
@@ -254,104 +151,19 @@ impl DuplexStreamConfig { |
254 | 151 | } |
255 | 152 | } |
256 | 153 |
|
257 | | -/// A placeholder duplex stream type for backends that don't yet support duplex. |
258 | | -/// |
259 | | -/// This type implements `StreamTrait` but all operations return errors. |
260 | | -/// Backend implementations should replace this with their own type once |
261 | | -/// duplex support is implemented. |
262 | | -#[derive(Default)] |
263 | | -pub struct UnsupportedDuplexStream; |
264 | | - |
265 | | -impl UnsupportedDuplexStream { |
266 | | - /// Create a new unsupported duplex stream marker. |
267 | | - /// |
268 | | - /// This should not normally be called - it exists only to satisfy |
269 | | - /// type requirements for backends without duplex support. |
270 | | - pub fn new() -> Self { |
271 | | - Self |
272 | | - } |
273 | | -} |
274 | | - |
275 | | -impl crate::traits::StreamTrait for UnsupportedDuplexStream { |
276 | | - fn play(&self) -> Result<(), PlayStreamError> { |
277 | | - Err(PlayStreamError::BackendSpecific { |
278 | | - err: crate::BackendSpecificError { |
279 | | - description: "Duplex streams are not yet supported on this backend".to_string(), |
280 | | - }, |
281 | | - }) |
282 | | - } |
283 | | - |
284 | | - fn pause(&self) -> Result<(), PauseStreamError> { |
285 | | - Err(PauseStreamError::BackendSpecific { |
286 | | - err: crate::BackendSpecificError { |
287 | | - description: "Duplex streams are not yet supported on this backend".to_string(), |
288 | | - }, |
289 | | - }) |
290 | | - } |
291 | | -} |
292 | | - |
293 | | -// Safety: UnsupportedDuplexStream contains no mutable state |
294 | | -unsafe impl Send for UnsupportedDuplexStream {} |
295 | | -unsafe impl Sync for UnsupportedDuplexStream {} |
296 | | - |
297 | 154 | #[cfg(test)] |
298 | 155 | mod tests { |
299 | 156 | use super::*; |
300 | 157 |
|
301 | | - #[test] |
302 | | - fn test_audio_timestamp_sample_position() { |
303 | | - let ts = AudioTimestamp::new(1234.5, 0, 1.0, StreamInstant::new(0, 0)); |
304 | | - assert_eq!(ts.sample_position(), 1235); // rounds up |
305 | | - |
306 | | - let ts = AudioTimestamp::new(1234.4, 0, 1.0, StreamInstant::new(0, 0)); |
307 | | - assert_eq!(ts.sample_position(), 1234); // rounds down |
308 | | - |
309 | | - let ts = AudioTimestamp::new(-100.0, 0, 1.0, StreamInstant::new(0, 0)); |
310 | | - assert_eq!(ts.sample_position(), -100); // negative values work |
311 | | - } |
312 | | - |
313 | | - #[test] |
314 | | - fn test_audio_timestamp_nominal_rate() { |
315 | | - let ts = AudioTimestamp::new(0.0, 0, 1.0, StreamInstant::new(0, 0)); |
316 | | - assert!(ts.is_nominal_rate()); |
317 | | - |
318 | | - let ts = AudioTimestamp::new(0.0, 0, 1.00005, StreamInstant::new(0, 0)); |
319 | | - assert!(ts.is_nominal_rate()); // within tolerance |
320 | | - |
321 | | - let ts = AudioTimestamp::new(0.0, 0, 1.001, StreamInstant::new(0, 0)); |
322 | | - assert!(!ts.is_nominal_rate()); // outside tolerance |
323 | | - } |
324 | | - |
325 | | - #[test] |
326 | | - fn test_audio_timestamp_default() { |
327 | | - let ts = AudioTimestamp::default(); |
328 | | - assert_eq!(ts.sample_time, 0.0); |
329 | | - assert_eq!(ts.host_time, 0); |
330 | | - assert_eq!(ts.rate_scalar, 1.0); |
331 | | - assert_eq!(ts.sample_position(), 0); |
332 | | - assert!(ts.is_nominal_rate()); |
333 | | - } |
334 | | - |
335 | | - #[test] |
336 | | - fn test_audio_timestamp_equality() { |
337 | | - let ts1 = AudioTimestamp::new(1000.0, 12345, 1.0, StreamInstant::new(0, 0)); |
338 | | - let ts2 = AudioTimestamp::new(1000.0, 12345, 1.0, StreamInstant::new(0, 0)); |
339 | | - let ts3 = AudioTimestamp::new(1000.0, 12346, 1.0, StreamInstant::new(0, 0)); |
340 | | - |
341 | | - assert_eq!(ts1, ts2); |
342 | | - assert_ne!(ts1, ts3); |
343 | | - } |
344 | | - |
345 | 158 | #[test] |
346 | 159 | fn test_duplex_callback_info() { |
347 | 160 | let callback = StreamInstant::new(1, 0); |
348 | 161 | let capture = StreamInstant::new(0, 500_000_000); // 500ms before callback |
349 | 162 | let playback = StreamInstant::new(1, 500_000_000); // 500ms after callback |
350 | 163 |
|
351 | | - let ts = AudioTimestamp::new(512.0, 1000, 1.0, callback); |
352 | | - let info = DuplexCallbackInfo::new(ts, capture, playback); |
| 164 | + let info = DuplexCallbackInfo::new(callback, capture, playback); |
353 | 165 |
|
354 | | - assert_eq!(info.timestamp.sample_time, 512.0); |
| 166 | + assert_eq!(info.callback, callback); |
355 | 167 | assert_eq!(info.capture, capture); |
356 | 168 | assert_eq!(info.playback, playback); |
357 | 169 | } |
@@ -421,30 +233,4 @@ mod tests { |
421 | 233 | let config3 = DuplexStreamConfig::new(2, 4, 44100, crate::BufferSize::Fixed(512)); |
422 | 234 | assert_ne!(config1, config3); |
423 | 235 | } |
424 | | - |
425 | | - #[test] |
426 | | - fn test_unsupported_duplex_stream() { |
427 | | - use crate::traits::StreamTrait; |
428 | | - |
429 | | - let stream = UnsupportedDuplexStream::new(); |
430 | | - |
431 | | - // play() should return an error |
432 | | - let play_result = stream.play(); |
433 | | - assert!(play_result.is_err()); |
434 | | - |
435 | | - // pause() should return an error |
436 | | - let pause_result = stream.pause(); |
437 | | - assert!(pause_result.is_err()); |
438 | | - } |
439 | | - |
440 | | - #[test] |
441 | | - fn test_unsupported_duplex_stream_default() { |
442 | | - let _stream = UnsupportedDuplexStream::default(); |
443 | | - } |
444 | | - |
445 | | - #[test] |
446 | | - fn test_unsupported_duplex_stream_send_sync() { |
447 | | - fn assert_send_sync<T: Send + Sync>() {} |
448 | | - assert_send_sync::<UnsupportedDuplexStream>(); |
449 | | - } |
450 | 236 | } |
0 commit comments