-
Notifications
You must be signed in to change notification settings - Fork 513
feat: add duplex stream API (interface only; backends to follow) #1229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| use crate::{ChannelCount, InputStreamTimestamp, OutputStreamTimestamp, SampleRate}; | ||
|
|
||
| /// Information relevant to a single call to the user's duplex stream data callback. | ||
| /// | ||
| /// Combines the input and output timestamps for the callback. Because a duplex stream's input and | ||
| /// output share a single clock, both timestamps are drawn from the same time source. | ||
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
| pub struct DuplexCallbackInfo { | ||
| input_timestamp: InputStreamTimestamp, | ||
| output_timestamp: OutputStreamTimestamp, | ||
| } | ||
|
|
||
| impl DuplexCallbackInfo { | ||
| /// Construct a `DuplexCallbackInfo` from its input and output timestamps. | ||
| pub fn new( | ||
| input_timestamp: InputStreamTimestamp, | ||
| output_timestamp: OutputStreamTimestamp, | ||
| ) -> Self { | ||
| Self { | ||
| input_timestamp, | ||
| output_timestamp, | ||
| } | ||
| } | ||
|
|
||
| /// The timestamp for the captured input data passed to the callback. | ||
| pub fn input_timestamp(&self) -> InputStreamTimestamp { | ||
| self.input_timestamp | ||
| } | ||
|
|
||
| /// The timestamp for the output data written by the callback. | ||
| pub fn output_timestamp(&self) -> OutputStreamTimestamp { | ||
| self.output_timestamp | ||
| } | ||
| } | ||
|
|
||
| /// The configuration shared by both directions of a duplex stream. | ||
| #[derive(Clone, Copy, Debug, Eq, PartialEq)] | ||
| pub struct DuplexStreamConfig { | ||
| /// The number of input (capture) channels. | ||
| pub input_channels: ChannelCount, | ||
| /// The number of output (playback) channels. | ||
| pub output_channels: ChannelCount, | ||
| /// The sample rate driving both directions. | ||
| pub sample_rate: SampleRate, | ||
| /// The desired buffer size, in frames per callback. | ||
| pub buffer_size: crate::BufferSize, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: for consistency, you could import |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,9 +12,9 @@ use std::{ | |
| }; | ||
|
|
||
| use crate::{ | ||
| Data, DeviceDescription, DeviceId, Error, InputCallbackInfo, InputDevices, OutputCallbackInfo, | ||
| OutputDevices, SampleFormat, SizedSample, StreamConfig, StreamInstant, SupportedStreamConfig, | ||
| SupportedStreamConfigRange, | ||
| Data, DeviceDescription, DeviceId, DuplexCallbackInfo, DuplexStreamConfig, Error, ErrorKind, | ||
| InputCallbackInfo, InputDevices, OutputCallbackInfo, OutputDevices, SampleFormat, SizedSample, | ||
| StreamConfig, StreamInstant, SupportedStreamConfig, SupportedStreamConfigRange, | ||
| }; | ||
|
|
||
| /// A [`Host`] provides access to the available audio devices on the system. | ||
|
|
@@ -114,10 +114,12 @@ pub trait DeviceTrait: PartialEq + Eq + Hash + Debug + Display { | |
| type SupportedInputConfigs: Iterator<Item = SupportedStreamConfigRange>; | ||
| /// The iterator type yielding supported output stream formats. | ||
| type SupportedOutputConfigs: Iterator<Item = SupportedStreamConfigRange>; | ||
| /// The stream type created by [`build_input_stream_raw`] and [`build_output_stream_raw`]. | ||
| /// The stream type created by [`build_input_stream_raw`] and [`build_output_stream_raw`], | ||
| /// and [`build_duplex_stream_raw`]. | ||
| /// | ||
| /// [`build_input_stream_raw`]: Self::build_input_stream_raw | ||
| /// [`build_output_stream_raw`]: Self::build_output_stream_raw | ||
| /// [`build_duplex_stream_raw`]: Self::build_duplex_stream_raw | ||
| type Stream: StreamTrait; | ||
|
|
||
| /// Structured description of the device with metadata. | ||
|
|
@@ -160,6 +162,18 @@ pub trait DeviceTrait: PartialEq + Eq + Hash + Debug + Display { | |
| .is_ok_and(|mut iter| iter.next().is_some()) | ||
| } | ||
|
|
||
| /// True if the device can build a synchronized duplex stream where the captured input and | ||
| /// rendered output share a single clock. | ||
| /// | ||
| /// Returning `true` is a contract that input and output sides will run from one device-level | ||
| /// callback, or an OS driver aggregate (such as an Aggregate Device on MacOS). | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: macOS (note capitalization). Several occurrences in this file. |
||
| /// | ||
| /// The default implementation returns `false`; hosts that can guarantee a shared clock should | ||
| /// override. | ||
| fn supports_duplex(&self) -> bool { | ||
| false | ||
| } | ||
|
|
||
| /// An iterator yielding input stream configurations that are supported by the device. | ||
| /// | ||
| /// # Errors | ||
|
|
@@ -407,6 +421,102 @@ pub trait DeviceTrait: PartialEq + Eq + Hash + Debug + Display { | |
| where | ||
| D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, | ||
| E: FnMut(Error) + Send + 'static; | ||
|
|
||
| /// Create a synchronized duplex stream whose input and output share the same clock | ||
| /// or OS provided bidirectional aggregate device (MacOS). MacOS Aggregate device drift | ||
| /// compensation is not required. | ||
| /// | ||
| /// # Parameters | ||
| /// | ||
| /// * `config` - Channels, sample rate, and buffer size shared by both directions. | ||
| /// * `data_callback` - Called periodically with captured input and a mutable output buffer. | ||
| /// * `error_callback` - Called when a stream error occurs (e.g., device disconnected). | ||
| /// * `timeout` - Time to wait for the backend to initialize the stream. `None` waits | ||
| /// indefinitely. Note: not all backends honor this value. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// - [`ErrorKind::UnsupportedOperation`] if the host or device does not support duplex | ||
| /// streams. | ||
| /// - [`ErrorKind::UnsupportedConfig`] if the sample rate, channel counts, buffer size, or | ||
| /// sample format is not supported by the device. | ||
| /// - [`ErrorKind::DeviceNotAvailable`] if the device has been disconnected. | ||
| /// - [`ErrorKind::DeviceBusy`] if the device is temporarily in use by another application. | ||
| /// - [`ErrorKind::PermissionDenied`] if the process lacks permission to access the device | ||
| /// (e.g. microphone access on macOS). | ||
| /// - [`ErrorKind::InvalidInput`] if the configuration parameters are invalid. | ||
| /// - [`ErrorKind::StreamInvalidated`] if the device's sample rate or buffer size changed | ||
| /// during stream creation, or an internal lock was poisoned. | ||
| /// - [`ErrorKind::ResourceExhausted`] if the host fails to spawn an internal monitoring | ||
| /// thread. | ||
| /// - [`ErrorKind::BackendError`] for unclassified backend failures. | ||
| /// | ||
| /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation | ||
| /// [`ErrorKind::UnsupportedConfig`]: crate::ErrorKind::UnsupportedConfig | ||
| /// [`ErrorKind::DeviceNotAvailable`]: crate::ErrorKind::DeviceNotAvailable | ||
| /// [`ErrorKind::DeviceBusy`]: crate::ErrorKind::DeviceBusy | ||
| /// [`ErrorKind::PermissionDenied`]: crate::ErrorKind::PermissionDenied | ||
| /// [`ErrorKind::InvalidInput`]: crate::ErrorKind::InvalidInput | ||
| /// [`ErrorKind::StreamInvalidated`]: crate::ErrorKind::StreamInvalidated | ||
| /// [`ErrorKind::ResourceExhausted`]: crate::ErrorKind::ResourceExhausted | ||
| /// [`ErrorKind::BackendError`]: crate::ErrorKind::BackendError | ||
| fn build_duplex_stream<T, D, E>( | ||
| &self, | ||
| config: DuplexStreamConfig, | ||
| mut data_callback: D, | ||
| error_callback: E, | ||
| timeout: Option<Duration>, | ||
| ) -> Result<Self::Stream, Error> | ||
| where | ||
| T: SizedSample, | ||
| D: FnMut(&[T], &mut [T], &DuplexCallbackInfo) + Send + 'static, | ||
| E: FnMut(Error) + Send + 'static, | ||
| { | ||
| self.build_duplex_stream_raw( | ||
| config, | ||
| T::FORMAT, | ||
| move |input, output, info| { | ||
| data_callback( | ||
| input | ||
| .as_slice() | ||
| .expect("host supplied incorrect sample type"), | ||
| output | ||
| .as_slice_mut() | ||
| .expect("host supplied incorrect sample type"), | ||
| info, | ||
| ) | ||
| }, | ||
| error_callback, | ||
| timeout, | ||
| ) | ||
| } | ||
|
|
||
| /// Create a dynamically typed synchronized duplex stream. | ||
| /// | ||
| /// Hosts that support duplex streams must override this method; | ||
| /// the default implementation returns [`ErrorKind::UnsupportedOperation`]. | ||
| /// | ||
| /// See [`build_duplex_stream`](Self::build_duplex_stream) for parameter and error | ||
| /// documentation. | ||
| /// | ||
| /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation | ||
| fn build_duplex_stream_raw<D, E>( | ||
| &self, | ||
| _config: DuplexStreamConfig, | ||
| _sample_format: SampleFormat, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be possible for a duplex stream to have its halves with different sample formats? If so, how likely would that be, and would we then want to support that or document it as a limitation?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know. Maybe an aggregate device would allow for each direction to have a different sample format. Its easy to add this as a just in case. |
||
| _data_callback: D, | ||
| _error_callback: E, | ||
| _timeout: Option<Duration>, | ||
| ) -> Result<Self::Stream, Error> | ||
| where | ||
| D: FnMut(&Data, &mut Data, &DuplexCallbackInfo) + Send + 'static, | ||
| E: FnMut(Error) + Send + 'static, | ||
| { | ||
| Err(Error::with_message( | ||
| ErrorKind::UnsupportedOperation, | ||
| "duplex streams are not supported by this host", | ||
| )) | ||
| } | ||
| } | ||
|
|
||
| /// A stream created from [`Device`](DeviceTrait), with methods to control it. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For style, we're not starting the line with "Added" for the other entries. Consider something like: