Conversation
|
@gulbrand thanks for your contribution. I don't mind the AI usage as long as the output is of high quality and what I'd expect from a seasoned developer. Let me know when you've addressed @Decodetalkers review points, and are ready for my detailed review. Very quickly, I like having |
Agreed. I've updated the PR but need a bit of time to test in separate projects again and to double check the safety claims. I'll ping again once I'm done with testing but I think this PR is in a much better state now. |
95278cd to
a344d4c
Compare
roderickvd
left a comment
There was a problem hiding this comment.
Took a while but here's my first code review.
examples/duplex_feedback.rs
Outdated
| let device = if let Some(device_id_str) = opt.device { | ||
| let device_id = device_id_str.parse().expect("failed to parse device id"); | ||
| host.device_by_id(&device_id) | ||
| .unwrap_or_else(|| panic!("failed to find device with id: {}", device_id_str)) |
There was a problem hiding this comment.
Why not use expect here as well?
There was a problem hiding this comment.
expect can't take a format string. I've done this .unwrap_or_else(|| panic!(...)) pattern myself a few times, It's the best way I know of to custom-format the panic message.
There was a problem hiding this comment.
I like expect better. They both seem idiomatic, but to me personally, expect feels a little more "gooder" here :) . This is just the example app so I'm not sure it matters too much.
There was a problem hiding this comment.
Changed to expect this is demo code so its ok either way.
There was a problem hiding this comment.
I was wrong in my initial comment. expect(&format!("{x}")) allocates eagerly. unwrap_or_else(|| panic!("{x}")) is better.
src/host/coreaudio/macos/device.rs
Outdated
| // roles — otherwise Core Audio won't re-route both directions. | ||
| let error_callback_for_stream: super::ErrorCallback = | ||
| if is_default_input_device(self) && is_default_output_device(self) { | ||
| Box::new(|_: StreamError| {}) |
There was a problem hiding this comment.
I think we shouldn't silently swallow disconnects, and propagate the error instead. A duplex stream is broken when either direction changes device.
There was a problem hiding this comment.
I think I lost track of this one, but I blieve this was about sending an error when the device disconnect. The DeviceManager is updated to include buffer change detection. Now that I am looking at all the early returns from the callback I wonder if we need to stop the callback and invoke the error callback. This means adding another channel to pass eror flags to the DeviceManager. Should we do that?
|
@gulbrand checking in - what's your planning on this one? No pressure, just asking. |
I'll be able to focus on this PR this weekend. |
89c629b to
ca12b28
Compare
|
@roderickvd Thank you for your feedback! FYI: I squashed my branch. I had 40+ commits and rebasing was a pain. I finished a first pass at addressing your feedback--mostly focussing on the easy fixes 🤣 . I want to follow-up on:
I think those two steps would answer the remaining feedback comments and answer the question about what might move to coreaudio-rs. |
|
I made some progress today answering the question of what would belong in coreaudio-rs vs cpal. I tried a couple of prototypes and there's one more I want to look at. I'll need another round on this one before I can form a helpful opinion. On the allocation issue -- I will try to rework that so that the stream is invalidated with an error if coreaudio asks for a different buffer size during streaming. I looked at what a few pro audio apps do and they detect this and stop audio processing and some of them even ask the user what to do about it. This seems like the right approach here as well. I think that's going to be better than just blindly maxing out the allocation buffer. @roderickvd thoughts on this approach? The other change I want to make is I think this should be feature flagged behind os target macos. I haven't been able to test this anywhere else, like iOS etc. I won't be able to test those anytime soon either. |
Yes, I think that's better. With regard to "asking what to do about it" - not sure if and how we could do that in cpal beyond firing the error callback with a specific error type. I'm planning on refactoring cpal errors into one |
|
|
|
@roderickvd I plan on an update ready for your review today/tomorrow. I'm really close, I just cleaning up comments now. The files have been separated and most of the comments are addressed either with a comment here or a code change. As for what could move over to coreaudio-rs, that remains unclear to me how to best do that. The first challenge I ran into was the error handling. Keeping this code in cpal for now means we can control what to do on errors better. Moving that to coreaudio-rs may mean updating its error model. With some work, I think it would be possible, but that would be more work over in coreaudio-rs. Looking at the changes that would go over to coreaudio-rs it isn't as much. Probably just the AudioUnit setup code to create the AudioUnits for the duplex callback. I could be wrong, but that's my best read for now and my recommendation is to keep here. I'm biased toward minimal changes, and so my recommendation is based on minimizing what would need to change in coreaudio-rs--with the error model changing, that might be too much. Do you think we should try to do it anyway? |
|
@roderickvd this is ready for another review. A few things to still highlight:
Thoughts on how to proceed? |
|
I think maybe you need to do git rebase first, to resolve the conflicts |
|
Conflicts resolved. |
| /// `Some(duration)` sets a maximum wait time. Not all backends support timeouts. | ||
| fn build_duplex_stream_raw<D, E>( | ||
| &self, | ||
| _config: &DuplexStreamConfig, |
There was a problem hiding this comment.
no, we do not need & any more, it is copyable
roderickvd
left a comment
There was a problem hiding this comment.
This is starting to look great. Almost ready to merge.
|
|
||
| ### Changed | ||
|
|
||
| - **POTENTIALLY 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. |
There was a problem hiding this comment.
This landed in the wrong section, probably due to rebasing.
You don't need to include "POTENTIALLY BREAKING" - SemVer will be bumped and this is listed under "Changed" so that's good enough for me.
|
|
||
| // Timing information for a duplex callback, combining input and output timestamps. | ||
| #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] | ||
| pub struct DuplexCallbackInfo { |
There was a problem hiding this comment.
For these and other public types, please ensure that they have public Rustdoc.
There was a problem hiding this comment.
The module should probably have some short Rustdoc, as it's private. Compare with InputCallbackInfo/OutputCallbackInfo for tone and verbosity.
| pub input_channels: ChannelCount, | ||
| pub output_channels: ChannelCount, | ||
| pub sample_rate: SampleRate, | ||
| pub buffer_size: crate::BufferSize, |
There was a problem hiding this comment.
Nitpick: may be imported for consistency with the other types.
| if listen_buffer_size { | ||
| let stream_weak_clone = stream_weak.clone(); | ||
| let error_callback_clone = error_callback.clone(); | ||
| std::thread::spawn(move || { |
There was a problem hiding this comment.
Perhaps capture the relevant discussion between @Decodetalkers and you earlier:
// thread handle intentionally dropped; same pattern as disconnect handler | where | ||
| E: FnMut(StreamError) + Send, | ||
| { | ||
| callback_instant.sub(delay).unwrap_or_else(|| { |
There was a problem hiding this comment.
I'm soon going to merge #1139 which will change the StreamInstant API somewhat. Here's what you might then replace it with on CoreAudio: https://github.com/RustAudio/cpal/pull/1139/changes#diff-8e684b369b47b2adec5fdeb79b478a2533366a37c684eb9a1e686abed4939bc6
| .lock() | ||
| .map_err(|_| BuildStreamError::BackendSpecific { | ||
| err: BackendSpecificError { | ||
| description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), |
There was a problem hiding this comment.
I know it's been like this in other places, but I think that we should propagate poisoned mutexes. I'll probably submit a PR to do so in other places too.
| ## Examples | ||
|
|
||
| CPAL comes with several examples in `examples/`. | ||
| CPAL comes with several examples in `examples/`, including `duplex_feedback` for hardware-synchronized duplex streams. |
That sounds nice. Would it need another channel or could we use a single channel and send an
As you wish, though I'd be fine by doing it in |
UPDATE on AI Usage
The Rust Audio AI policy has been updated and this PR is compliant with the policy.
Add synchronized duplex stream support
Summary
This PR introduces synchronized duplex streams to cpal, starting with CoreAudio support on macOS.
Development Note
Developed with assistance from Claude Code (Anthropic's AI coding assistant).
Motivation
Currently, applications requiring synchronized input/output (like DAWs, real-time effects, or audio analysis tools) must use separate input and output streams with ring buffers for synchronization. This approach:
Duplex streams solve this by using a single device context for both input and output, guaranteeing sample-accurate alignment at the hardware level.
API Overview
Potentially Breaking Changes
build_duplex_stream and build_duplex_stream_raw have been added to
DeviceTraitwith a default impl. This shouldn't break any existing implementations, but just calling this out.