Second round of correctness fixes on multiple backends - #1335
Second round of correctness fixes on multiple backends#1335roderickvd wants to merge 13 commits into
Conversation
|
sure, but might have to wait until monday |
|
I think it's great if we can share knowledge, absolutely no objections from me! |
No worries, let me know when it's done, many of these things we can still backport to |
LastExceed
left a comment
There was a problem hiding this comment.
Static review only so far, haven't tested anything yet. Also can't speak on the changes to non-windows backends, as I am not familiar with them
| // ASIOStart() plays buffer half 1 immediately, before the first bufferSwitch can fill it; | ||
| // half 0 is covered by that first callback. | ||
| fn prefill_output_silence(driver: &sys::Driver, asio_streams: &Mutex<sys::AsioStreams>) { |
There was a problem hiding this comment.
Shouldn't this use the data callback instead of generating silence? Also I think this isn't actually necessary at all:
ASIO 2.3 specification, secion II.5:
The audio streaming starts after the
ASIOStart()call. Prior to starting the hardware streaming the driver will issue one or morebufferSwitch()orbufferSwitchTimeInfo()callbacks to fill its first output buffer(s). Since the hardware did not provide any input data yet the input channels' buffers should be filled with silence by the driver.
To me this clearly states that pre-filling is the driver's responsibility, not the host's. In fact, if the initial callback invocations happen within ASIOCreateBuffers() (on the calling thread), then the silence ends up overwriting actual stream data.
There was a problem hiding this comment.
What I based it on is https://github.com/dechamps/ASIOUtil/blob/master/BUFFERS.md which also states this and then goes on with two more pieces of the spec and conludes that:
the host should fill buffer 1 prior to calling
ASIOStart()...
It does not seem filled via bufferSwitch callbacks.
Is this synthesis incorrect? I thought that this source was pretty authoritative but it'd be great if you could verify.
There was a problem hiding this comment.
That is an awesome resource, and I have a thing or two to say about it (will create an issue there). But it doesn't actually seem to contain the conclusion you quoted, Ctrl + F "it is not filled" yields 0 results. Is this paraphrased?
My interpretation is still that the driver should invoke the callback right after buffer creation to populate the buffers, but it is indeed a bit ambiguous
There was a problem hiding this comment.
There was a problem hiding this comment.
Fixed the quote for what was my deduction.
There was a problem hiding this comment.
I left a thorough comment there, I recommend you read it too. Bottom like is that the spec contradicts itself, but it can be somewhat shoehorned into what I said earlier
| - **PipeWire**: Fix capture reading from the wrong offset in the buffer on some devices. | ||
| - **WASAPI**: Device enumeration no longer panics if the COM enumerator fails to initialize. | ||
| - **WASAPI**: Output streams now start with silence instead of undefined content in the render buffer. | ||
| - **WASAPI**: Fix `I64` and `F64` incorrectly reported as supported output formats. |
There was a problem hiding this comment.
Do you have a source for the claim that this was incorrect? I can't find anything that forbids WASAPI devices from using these formats
There was a problem hiding this comment.
I don't, the MSDN docs are woefully quiet about what's supported and what's not. I believe that 64-bit isn't a thing in WASAPI at all. As proof of the contrary I've not found any implementations of it either. Could you test on a Windows box?
There was a problem hiding this comment.
Scratch that, I just realized that we're specifically talking about formats supported by AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM, not by the device itself. Your commit is correct, i64 and f64 are not supported (IAudioClient::Initialize() returns E_INVALIDARG). See also #1343
| let frames = get_available_frames(&run_context.stream)?; | ||
| if frames > 0 { | ||
| write_silence(render_client, &run_context.stream, frames)?; | ||
| } |
There was a problem hiding this comment.
Here too, shouldn't we use the data callback instead of generating silence?
There was a problem hiding this comment.
Yeah, not sure if I got this right. As matter of fact, CamillaDSP doesn't write silence at all, but moves start_stream() to after the first pass of the fill loop. This proposal of mine writes silence because process_commands() has no access to the data callback, at the cost of one period of silence at the start. That's smaller than the real fix would be, mirroring what CamillaDSP did. What do you think?
There was a problem hiding this comment.
I guess it is fine for now, not worth refactoring everything just for this. But probably worth creating an issue for future reference, I could imagine similar situations existing on the other backends as well (I haven't checked)
| // Given the audio client and format, returns whether the audio engine supports it natively in | ||
| // shared mode without format conversion. | ||
| // the given share mode without format conversion. | ||
| pub unsafe fn is_format_supported( | ||
| client: &Audio::IAudioClient, | ||
| waveformatex_ptr: *const Audio::WAVEFORMATEX, | ||
| share_mode: Audio::AUDCLNT_SHAREMODE, | ||
| ) -> Result<bool, Error> { |
There was a problem hiding this comment.
I'd generally discourage implementing things before we need them (YAGNI principle). In this case it is probably fine, but in general you never know how requirements might change, or when a planned feature ends up getting scrapped entirely, potentially resulting in code that ends up never being used
There was a problem hiding this comment.
Native DSD support on WASAPI is one of the v0.19 design goals, so it was easy to rig up.
There was a problem hiding this comment.
Err I meant exclusive mode.
|
|
||
| // There is 0% chance of lock contention the host only locks when recreating streams. | ||
| let stream_lock = asio_streams.lock().unwrap(); | ||
| let Ok(stream_lock) = asio_streams.lock() else { | ||
| return; | ||
| }; |
There was a problem hiding this comment.
The commit message says "do not block", but .lock() is a blocking function, no?
There was a problem hiding this comment.
Indeed, I should have written "do not panic".
|
ran a couple examples on hardware via ASIO and WASAPI, no problems to report |
This is the result from a second pass comparing cpal against CamillaDSP's backends for correctness and robustness
gaps (first one landed in 0.18.0). Their backends moved on since (
next5), so I went through it again.In full transparency of licensing: CamillaDSP is MPL-2.0, cpal is Apache 2.0. That should be OK: MPL's copyleft covers licensed source files but not ideas or bugs found while reading them. Nothing was copied, rather, every fix was reimplemented against cpal's conventions. @HEnquist let me know if you have any objections.
@LastExceed would you verify the ASIO and WASAPI changes? You'll note that in WASAPI I made some small changes to pave the way for exclusive mode later.