Skip to content
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

Audio glitches with default settings (DirectSound gets stuck) #29

Closed
andyc56-mso opened this issue Nov 29, 2018 · 10 comments
Closed

Audio glitches with default settings (DirectSound gets stuck) #29

andyc56-mso opened this issue Nov 29, 2018 · 10 comments
Assignees
Labels

Comments

@andyc56-mso
Copy link

I had the same problem as AustinJerry did in the REW thread at AVS forum. He stated it as such:

"After installing and selecting FlexASIO in the REW Settings panel, the only output options in the drop-down are “out 1” and “out 2”. When I selected out 1 and initiated a signal using REW’s tone generator, there was a brief tone from the left speaker which then changed to a distorted tone which I could not stop without exiting REW completely."

I created a log file per your request to Jerry and have attached it here.
FlexASIOlog.zip

@dechamps
Copy link
Owner

Ok, I managed to reproduce it using REW 5.19. Indeed it's a pretty obvious bug, sorry about that; I'm sad that I didn't catch it earlier.

I will investigate further, but I'm not sure I'll have time to fully resolve this before next week. In the mean time, I noticed the following configuration seems to solve this problem, so feel free to use that as a workaround:

[input]
suggestedLatencySeconds = 0.100

[output]
suggestedLatencySeconds = 0.100

This would make this a regression from 8cfb77e, first released in FlexASIO 1.0. It looks like my rationale was wrong in this change.

@dechamps dechamps changed the title Distorted tone in REW signal generator that doesn't turn off Audio glitches with default settings Nov 29, 2018
@dechamps dechamps added the bug label Nov 29, 2018
@dechamps dechamps self-assigned this Nov 29, 2018
@AustinJerry
Copy link

flexasiotest.txt
Portaudiodevices.txt
FlexASIO.log

Three diagnostic files attached

@dechamps
Copy link
Owner

For some reason, only the first 20 stream callbacks (i.e. the first ~400 ms) occur. Then the stream callbacks just stop firing. This is completely consistent with the issue we're observing, including the fact that the audio doesn't stop (the same buffer is being played indefinitely). I can even reproduce this in FlexASIOTest if I increase the number of buffer switches it's waiting for.

This should never happen no matter what FlexASIO does and no matter what the settings are. This is starting to smell like a PortAudio bug. It happens with PortAudio pa_stable_v190600_20161030 as well as a recent master.

With the MME backend there appears to be glitching as well, but it's still firing stream callbacks, so that's probably a different issue. Filed #30 to track that.

Next step is to figure out where in PortAudio code the streaming loop gets stuck.

@AustinJerry
Copy link

AustinJerry commented Nov 29, 2018 via email

@dechamps dechamps changed the title Audio glitches with default settings Audio glitches with default settings (DirectSound gets stuck) Dec 1, 2018
@dechamps
Copy link
Owner

dechamps commented Dec 2, 2018

Another interesting observation is that this only occurs in full-duplex mode. If either the input or the output is disabled, I can't reproduce.

Also this doesn't seem to be an actual deadlock within PortAudio. The internal PortAudio polling event is firing, but PortAudio decides not to call the stream callback. In pa_win_ds.c, we make it to TimeSlice() and then all the way to AdaptingProcess() in pa_process.c, which ends up never calling the stream callback.

Looking at the business logic involved, it looks like one way this could happen is if the input or output buffer fill level never reaches the user buffer size. Specifically, it might be that there is not enough valid data (i.e. at least 20 ms, with the default settings) in the DirectSound host buffer when polling occurs. My understanding of the logic is that this is expected to happen from time to time, and that the buffer fill level should eventually reach the point where stream processing can continue. But in practice that doesn't seem to happen, and as a result stream processing grinds to a halt.

This is further complicated by the fact that the DirectSound host buffer is a circular buffer, and PortAudio doesn't seem to insert any intermediate buffer - it's a straight copy from the DirectSound buffers and the user (ASIO) buffers. This means that we can get into a situation where, if it takes too long to consume/produce data from/to these buffers, it might be too late and we've wrapped around the circular buffer again. (Looking at TimeSlice() code it doesn't look like PortAudio would be smart enough to notice that failure mode.) If this just keeps happening then we basically enter an infinite loop.

This is made even more precarious by the fact that this is full duplex operation, which means both the input and output buffers need to get to the correct fill level at the same time for things to work.

In theory, PortAudio is supposed to set the polling frequency and host buffer sizes in such a way that we don't get into that kind of catch-22 situation, but it might be that the default FlexASIO settings, which result in a ~40 ms host buffer size and a ~5 ms polling frequency, might be too stringent for this to work. This might be further compounded by the fact that I suspect DirectSound doesn't actually have a 5 ms granularity when it comes to buffer span validity.

@dechamps
Copy link
Owner

dechamps commented Dec 2, 2018

Some more data from experiments: with default settings, 44.1 kHz sample rate, stereo input and output, the DirectSound capture buffer size is set to 7232 bytes (41.0 ms). Polling occurs every 5 milliseconds or so. Every time polling occurs, IDirectSoundCaptureBuffer8::GetCurrentPosition returns a read position of exactly 0 or exactly 5512 bytes (31.2 ms). DirectSound seems to randomly return either result, and the capture position is progressing normally, so it doesn't look like DirectSound itself is getting stuck - it's more likely the read cursor granularity is just overly coarse. This is consistent with the fact that DirectSound is returning zero much more often than 5512 (which is much closer to the end of the buffer).

Meanwhile, the current PortAudio read offset into the buffer is stuck at 5472 bytes (31.0 ms). If the returned read position was zero, PortAudio assumes that it can read from the last 1760 bytes (10.0 ms) of the buffer (wraparound). If the read position was 5512, PortAudio assumes it can read from a span of 40 bytes in the middle of the buffer. In both cases, the read is not large enough to fill the 20 ms user buffer, so PortAudio simply waits for the next poll for more data to become available. On the next poll, the exact same scenario repeats itself, and the whole loop gets stuck.

@dechamps
Copy link
Owner

dechamps commented Dec 3, 2018

Using a forcibly reduced polling period in PortAudio still results in IDirectSoundCaptureBuffer::GetCurrentPosition() always returning either 0 or 5512 as the read position. Furthermore, if I increase the DirectSound buffer size, the read cursor position is always returned in increments of 5512 bytes (31.25 ms). Therefore I think we can safely conclude that the root cause of the problem is that DirectSound is unable to provide good enough read granularity to support small buffer sizes.

This also occurs when disabling the output side, but then PortAudio doesn't get stuck. Presumably that's because its read offset doesn't get wedged in that case, since the output position doesn't influence it.

The read cursor granularity on the output side is around 9 ms, which is much better. Therefore it would seem that the issue is truly with the way DirectSound handles the input side internally.

The DirectSound input read cursor granularity is 31.25 ms. For things to work correctly the DirectSound host buffer size needs to be at least twice that. PortAudio sets the host buffer size to the user buffer size, plus the preferred latency (or the user buffer size, whichever is greater). With the default FlexASIO settings, that results in a 40 ms host buffer, which is too small. If the default FlexASIO buffer size was increased from 20 ms to 40 ms, that would result in a 80 ms host buffer, which would take care of the immediate problem.

@dechamps
Copy link
Owner

dechamps commented Dec 3, 2018

Observing the Windows API calls using the excellent API Monitor sheds more light on the issue. DirectSound uses WASAPI internally; it even goes through the public WASAPI API just like any other application. (That's not surprising, but it's nice to have confirmation.) When calling IAudioClient::Initialize, DirectSound always requests a ~30 ms buffer size on the capture side, and ~20 ms on the render side, no matter what DirectSound host buffer size the application uses. Attempting to use smaller buffer sizes on the application side is pointless.

@dechamps
Copy link
Owner

dechamps commented Dec 3, 2018

Strike my last - it looks like I misread the interaction between DirectSound and WASAPI. DirectSound is, in fact, transferring data between the WASAPI buffer and the DirectSound buffer in 10 ms (441 samples) chunks, in both directions. Specifically, on capture, WASAPI provides DirectSound with 441 sample buffers through AudioCaptureClient::GetBuffer(), and DirectSound is providing it with 441 sample buffers through IAudioRenderClient::GetBuffer(). (The buffer size passed to IAudioClient::Initialize() is a red herring, I think.)

With that in mind, one would expect DirectSound to provide a 10 ms cursor granularity. That's true for the render side, but for the capture side, strangely, that doesn't seem to be the case. Specifically, DirectSound doesn't seem to update the read cursor position it exposes to the application every time WASAPI provides it with an additional 10 ms buffer. This looks like a weird limitation of DirectSound, where it's incapable of providing a capture read cursor granularity of less than 30 ms even though WASAPI itself feeds it 10 ms buffers.

@dechamps
Copy link
Owner

FYI I reported the root cause upstream in PortAudio/portaudio#775 (better late than never!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants