Skip to content

Commit 463745b

Browse files
committed
refactor(duplex): Remove AudioTimestamp and all Duplex stream and error types
Simplify the duplex implementation by unifying it with existing Stream types instead of introducing separate types. This removes AudioTimestamp, DuplexStream wrappers, and the duplicate DuplexDisconnectManager, making duplex streams work the same way as regular input/output streams. DuplexCallbackInfo now uses StreamInstant fields directly, matching the existing Input/OutputCallbackInfo pattern.
1 parent 1f0fabb commit 463745b

22 files changed

Lines changed: 88 additions & 717 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- `DeviceTrait::build_duplex_stream` and `build_duplex_stream_raw` for synchronized input/output.
13-
- `duplex` module with `DuplexStreamConfig`, `AudioTimestamp`, and `DuplexCallbackInfo` types.
13+
- `duplex` module with `DuplexStreamConfig` and `DuplexCallbackInfo` types.
1414
- **CoreAudio**: Duplex stream support with hardware-synchronized input/output.
1515
- Example `duplex_feedback` demonstrating duplex stream usage.
1616
- `DeviceBusy` error variant for retriable device access errors (EBUSY, EAGAIN).
@@ -19,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919

2020
### Changed
2121

22-
- **BREAKING**: `DeviceTrait` now requires `DuplexStream` associated type and `build_duplex_stream_raw()` method. External implementations must add stubs returning `StreamConfigNotSupported`.
22+
- **BREAKING**: `DeviceTrait` now includes `build_duplex_stream()` and `build_duplex_stream_raw()` methods. The default implementation returns `StreamConfigNotSupported`, so external implementations are compatible without changes.
2323
- Overall MSRV increased to 1.78.
2424
- **ALSA**: Update `alsa` dependency from 0.10 to 0.11.
2525
- **ALSA**: MSRV increased from 1.77 to 1.82 (required by alsa-sys 0.4.0).

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ This library currently supports the following:
2828
- Enumerate known supported input and output stream formats for a device.
2929
- Get the current default input and output stream formats for a device.
3030
- Build and run input and output PCM streams on a chosen device with a given stream format.
31+
- Build and run duplex (simultaneous input/output) streams with hardware clock synchronization (macOS only, more platforms coming soon).
3132

3233
Currently, supported hosts include:
3334

@@ -209,6 +210,7 @@ CPAL comes with several examples demonstrating various features:
209210
- `beep` - Generate a simple sine wave tone
210211
- `enumerate` - List all available audio devices and their capabilities
211212
- `feedback` - Pass input audio directly to output (microphone loopback)
213+
- `duplex_feedback` - Hardware-synchronized duplex stream loopback (macOS only)
212214
- `record_wav` - Record audio from the default input device to a WAV file
213215
- `synth_tones` - Generate multiple tones simultaneously
214216

examples/custom.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ impl DeviceTrait for MyDevice {
5454
type SupportedInputConfigs = std::iter::Empty<cpal::SupportedStreamConfigRange>;
5555
type SupportedOutputConfigs = std::iter::Once<cpal::SupportedStreamConfigRange>;
5656
type Stream = MyStream;
57-
type DuplexStream = cpal::duplex::UnsupportedDuplexStream;
5857

5958
fn name(&self) -> Result<String, cpal::DeviceNameError> {
6059
Ok(String::from("custom"))
@@ -190,7 +189,7 @@ impl DeviceTrait for MyDevice {
190189
_data_callback: D,
191190
_error_callback: E,
192191
_timeout: Option<std::time::Duration>,
193-
) -> Result<Self::DuplexStream, cpal::BuildStreamError>
192+
) -> Result<Self::Stream, cpal::BuildStreamError>
194193
where
195194
D: FnMut(&cpal::Data, &mut cpal::Data, &cpal::duplex::DuplexCallbackInfo) + Send + 'static,
196195
E: FnMut(cpal::StreamError) + Send + 'static,

examples/duplex_feedback.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,17 @@ fn main() -> anyhow::Result<()> {
4040
let opt = Opt::parse();
4141
let host = cpal::default_host();
4242

43-
// Find the device.
44-
let device = if let Some(device_name) = opt.device {
45-
let id = &device_name.parse().expect("failed to parse device id");
46-
host.device_by_id(id)
43+
// Find the device by device ID or use default
44+
let device = if let Some(device_id_str) = opt.device {
45+
let device_id = device_id_str.parse().expect("failed to parse device id");
46+
host.device_by_id(&device_id)
47+
.expect(&format!("failed to find device with id: {}", device_id_str))
4748
} else {
4849
host.default_output_device()
49-
}
50-
.expect("failed to find device");
50+
.expect("no default output device")
51+
};
5152

52-
println!("Using device: \"{}\"", device.id()?);
53+
println!("Using device: \"{}\"", device.description()?.name());
5354

5455
// Create duplex stream configuration.
5556
let config = DuplexStreamConfig::new(

src/duplex.rs

Lines changed: 17 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -37,139 +37,36 @@
3737
//! ).expect("failed to build duplex stream");
3838
//! ```
3939
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};
14541

14642
/// Information passed to duplex callbacks.
14743
///
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)]
15148
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,
15451

155-
/// Estimated time when the input audio was captured.
52+
/// The instant that input data was captured from the device.
15653
///
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).
15956
pub capture: StreamInstant,
16057

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.
16259
///
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).
16562
pub playback: StreamInstant,
16663
}
16764

16865
impl DuplexCallbackInfo {
16966
/// 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 {
17168
Self {
172-
timestamp,
69+
callback,
17370
capture,
17471
playback,
17572
}
@@ -254,104 +151,19 @@ impl DuplexStreamConfig {
254151
}
255152
}
256153

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-
297154
#[cfg(test)]
298155
mod tests {
299156
use super::*;
300157

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-
345158
#[test]
346159
fn test_duplex_callback_info() {
347160
let callback = StreamInstant::new(1, 0);
348161
let capture = StreamInstant::new(0, 500_000_000); // 500ms before callback
349162
let playback = StreamInstant::new(1, 500_000_000); // 500ms after callback
350163

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);
353165

354-
assert_eq!(info.timestamp.sample_time, 512.0);
166+
assert_eq!(info.callback, callback);
355167
assert_eq!(info.capture, capture);
356168
assert_eq!(info.playback, playback);
357169
}
@@ -421,30 +233,4 @@ mod tests {
421233
let config3 = DuplexStreamConfig::new(2, 4, 44100, crate::BufferSize::Fixed(512));
422234
assert_ne!(config1, config3);
423235
}
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-
}
450236
}

src/host/aaudio/mod.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,6 @@ pub struct Host;
115115
#[derive(Clone)]
116116
pub struct Device(Option<AudioDeviceInfo>);
117117

118-
pub struct DuplexStream(pub crate::duplex::UnsupportedDuplexStream);
119-
120-
impl StreamTrait for DuplexStream {
121-
fn play(&self) -> Result<(), PlayStreamError> {
122-
StreamTrait::play(&self.0)
123-
}
124-
125-
fn pause(&self) -> Result<(), PauseStreamError> {
126-
StreamTrait::pause(&self.0)
127-
}
128-
}
129-
130118
/// Stream wraps AudioStream in Arc<Mutex<>> to provide Send + Sync semantics.
131119
///
132120
/// While the underlying ndk::audio::AudioStream is neither Send nor Sync in ndk 0.9.0
@@ -395,7 +383,6 @@ impl DeviceTrait for Device {
395383
type SupportedInputConfigs = SupportedInputConfigs;
396384
type SupportedOutputConfigs = SupportedOutputConfigs;
397385
type Stream = Stream;
398-
type DuplexStream = DuplexStream;
399386

400387
fn name(&self) -> Result<String, DeviceNameError> {
401388
match &self.0 {

0 commit comments

Comments
 (0)