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

JACK buffer size fix. #11121

Merged
merged 21 commits into from
Feb 4, 2023
Merged

Conversation

daschuer
Copy link
Member

@daschuer daschuer commented Dec 8, 2022

This PR disposes all stored buffer size values and uses consequently the value passed as callback parameter.
This is required because the JACK buffer size is externally controlled and can change at any time.
Current main uses the crazed out buffer size form hardware preferences.

Because if that the timing compensation with two soundcards or the network clock fails and is now grayed out in the hardware preferences.

Note: Multi soundcard setup is working, but because if the joint stream in JACK and the split-up in Portaudio. It adds an extra buffer latency for the non main device. It is also known that the clock synchronisation in native JACK is broken and the situation is not clear in case in pipewire.

So if you want a minimum latency setup, use ALSA directly.

This PR is on top of #11092

Many thanks to @robbert-vdh for finding the original issues and for the initial PR

robbert-vdh and others added 14 commits December 6, 2022 20:11
`m_deviceInfo->hostApi` is an index in the list of supported APIs on
this platform. So on Linux 0 is ALSA, 2 is JACK. This is also mentioned
in the comment on the `hostApi` field. We should be checking the device
type ID instead, as is done elsewhere in the code.

This fixes the buffer size when using JACK clients. With JACK the buffer
size should be coming from the server (and PortAudio chops it up into
1024 sample blocks if it's larger). Before this change it would use the
now grayed out latency value set for the previous audio backend before
switching to JACK (defaulting to 1024 samples).
In any operation that works with audio buffers passed from PortAudio.
This is needed because PortAudio may pass us buffers that are smaller or
larger than the size we request. And when using JACK the requested size
is 0, which will cause PortAudio to pick a buffer size matching the JACK
server.
The network backend already does this.
to clarify that thisis not the actually used value. The actual value is
passied via every audi callback.
This clarifies that the value is a subject of change in case of JACK
This allows following the server buffer size that is not always const.
Copy link
Member

@Swiftb0y Swiftb0y left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A couple preliminary comments

m_audioBufferTime = mixxx::Duration::fromSeconds(
const auto requestedBufferTime = mixxx::Duration::fromSeconds(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the use of const auto here 👍

Comment on lines +486 to 488
m_targetTime += static_cast<qint64>(framesPerBuffer / m_dSampleRate * 1000000);
double callbackEntrytoDacSecs = (m_targetTime - currentTime) / 1000000.0;
callbackEntrytoDacSecs = math_max(callbackEntrytoDacSecs, 0.0001);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't we have an abstraction for sample and frame related time calculations?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not aware of something for this special case.

src/engine/enginemaster.cpp Outdated Show resolved Hide resolved
src/engine/enginemaster.cpp Show resolved Hide resolved
src/soundio/sounddeviceportaudio.cpp Outdated Show resolved Hide resolved
src/soundio/sounddeviceportaudio.cpp Outdated Show resolved Hide resolved
@@ -45,45 +42,47 @@ void VisualPlayPosition::set(double playPos, double rate, double positionStep,
data.m_positionStep = positionStep;
data.m_slipPosition = slipPosition;
data.m_tempoTrackSeconds = tempoTrackSeconds;
data.m_audioBufferMicroS = audioBufferMicroS;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does the waveform need to know about the buffer size??

// PortAudio's JACK back end has its own buffering to split or merge the buffer
// received from JACK to the desired size.
// However, we use here paFramesPerBufferUnspecified to use the JACK buffer size
// which offers the best responds time without additional jitter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue here is extra latency, not extra jitter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The jitter is introduced if Portaudio composes new buffers from the native JACK buffer. In this case we have two callbacks in a row and than the pause. I will improve the comment.

// For every sound hardware. Clock drift can not happen, but the buffers
// are processed: In 1 - Out 1 - In 2 - Out 2
// This causes even with the "Experimental (no delay)" setting
// one extra buffer delay for the non clock reference device
Copy link
Contributor

@Be-ing Be-ing Dec 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no way to avoid this extra latency when using PortAudio with multiple JACK clients? If so, I think that's a good justification to not use PortAudio for JACK. (That's beyond the scope of this bugfix, of course.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no way. And yes that is only one reason why Mixxx should become a native JACK client.

@daschuer
Copy link
Member Author

daschuer commented Dec 9, 2022

Done.

@Swiftb0y
Copy link
Member

Swiftb0y commented Dec 9, 2022

My general worry with this PR is that we're disconnecting the buffer size from the actual buffer, this dramatically increases the chance for programmer error resulting in out-of-bounds accesses.

@robbert-vdh
Copy link
Contributor

Isn't it the other way around? This PR (and #11092 leading up to it) causes the actual buffer size to be used for computations working on the audio buffer, instead of the configured buffer size (which may or may not match the actual buffer size).

@Swiftb0y
Copy link
Member

Swiftb0y commented Dec 9, 2022

I guess it depends on what buffer we're talking about. I'm not familiar enough with the surrounding code. It just worries me in general to see the size disconnected from the buffer (or rather vice versa).

@daschuer
Copy link
Member Author

daschuer commented Dec 9, 2022

Also I'm more worried that we're using iBufferSize to access a foreign buffer thats not allocated with MAX_BUFFER_LEN.

That is the essence of this PR, using the buffer size that is provided along with the pointer to the buffer. In the current main branch this is not the case, we rely on Portaudio that we receive the requested buffer size, which is luckily reliable the case.

Also, we don't use iBufferSize to access the buffets allocated with MAX_BUFFER_LEN in this PR. We use them to pass sample across the two streams in case of two soundcards.

int outChunkSize = framesPerBuffer * m_outputParams.channelCount;

The FIFOs have there own save read and write size values. In the unlikely case the FIFO buffer is to small, the buffer from Portaudio is padded with zeros.

@Swiftb0y
Copy link
Member

Swiftb0y commented Dec 9, 2022

I'm sorry, I can't comment on this, I don't understand how your explanation relates to the code you linked.

@Swiftb0y Swiftb0y dismissed their stale review December 9, 2022 20:54

I'm distancing myself from this PR

@daschuer
Copy link
Member Author

daschuer commented Dec 9, 2022

That is the code where the FIFO of MAX_BUFFER_LEN used. You can see that it does not relate to iBufferSize.

@daschuer
Copy link
Member Author

Ready

@daschuer
Copy link
Member Author

@robbert-vdh can you confirm that this is working for you and do a brief review of my code?
With that in place we can ask another team member to merge.

Copy link
Contributor

@robbert-vdh robbert-vdh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@daschuer I haven't tested it too extensively, but the JACK seems to still function the same as #11092. And while the waveforms from multiple tracks still aren't entirely in sync, they at least seem to move around less than they did before.

I'm not too familiar with these parts of Mixxx but it looks good to me. I grepped for m_iBufferSize to find more remaining uses and I stumbled upon src/analyzer/analyzergain.h. That class has a m_iBufferSize field that's initialized to 0 and then used in a comparison that always returns true if the audio buffer being analyzed isn't empty. That's probably a remnant of some older behavior and can also be inlined.

@@ -126,6 +125,9 @@ class SoundManager : public QObject {
void closeDevices(bool sleepAfterClosing);

void setJACKName() const;
bool jackApiUsed() const {
return m_jackSampleRate.isValid();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct? m_jackSampleRate seems to be set in SoundManager::queryDevicesPortaudio() when the JACK API is available. So it sounds like it will also be set when Mixxx uses ALSA, WasAPI, CoreAudio, etc. while a JACK server is available. I have not yet checked if this is actually the case here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ups, good catch. Fixed.

@daschuer
Copy link
Member Author

@robbert-vdh Is there anything left to do or can you formally approve this PR?

@robbert-vdh
Copy link
Contributor

It's been a hot minute since I looked at it but it looked good to me! I tested this before and it seemed to work fine. I've been using the original waveform change from #11092 in my own branch since rebasing this PR seemed a bit more involved so I have not tested it super extensively beyond that.

Did you take a look at the removing that unused m_iBufferSize field I mentioned yet? That was the last remaining reference to m_iBufferSize. #11121 (review)

@daschuer
Copy link
Member Author

Yes, the fix has already been merged via eb0339f

ControlObject::set(ConfigKey("[Master]", "samplerate"), m_dSampleRate);
ControlObject::set(ConfigKey("[Master]", "audio_buffer_size"),
m_audioBufferTime.toDoubleMillis());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it's highly unlikely that this CO is used somewhere in mappings or skins - and therefore can be removed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to mention that it also needs to be removed from the Mixxx Controls section in the manual, but that CO does not seem to be documented anyway. 🤷

m_clkRefTimer.start();
qint64 currentTime = m_pNetworkStream->getInputStreamTimeUs();
m_targetTime += m_audioBufferTime.toIntegerMicros();
// This deadline for the next buffer in microseconds since the Unix epoch
m_targetTime += static_cast<qint64>(framesPerBuffer / m_dSampleRate * 1000000);
double callbackEntrytoDacSecs = (m_targetTime - currentTime) / 1000000.0;
callbackEntrytoDacSecs = math_max(callbackEntrytoDacSecs, 0.0001);
VisualPlayPosition::setCallbackEntryToDacSecs(callbackEntrytoDacSecs, m_clkRefTimer);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that for rate syncing with the frequency of the network clock framesPerBuffer is enough. But to show the visual waveform, not only rate, but also latency is needed.
I don't understand this calculation. Shouldn't the waveform be in sync with the sound that the DJ hears (headphones or booth speakers) even if he's broadcasting a network stream?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case the latency of the main DAC is lost. It would be an extra feature to compensate this.

This calculation is only to compensate the jitter that happens due to the different cycles of the audio and video thread.

At least this is unrelated to this PR. Do we want to file a bug for that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, please create a bug for this. It would be great, if you could add some pointers to this report, how a possible fix could look like.
I agree, that this is not related to this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it is: #11219

@daschuer daschuer added this to the 2.3.4 milestone Jan 30, 2023
@daschuer
Copy link
Member Author

@Swiftb0y I consider this a 2.3.4 fix. Is it now good to merge?

@Swiftb0y
Copy link
Member

I'm sorry, I can't approve this PR

I'm sorry, I can't comment on this, I don't understand how your explanation relates to the code you linked.

@JoergAtGithub
Copy link
Member

I read through the code and it looks clean to me.
The change to pass framesPerBuffer as function argument instead of using the member variable doesn't help readability, but ensures, that the code runs always with the correct buffer size, which can change with every callback in case of JACK.
I can't test it myself, but since it solves the problem for @daschuer and @robbert-vdh, I rate this as properly tested.
Removing the undocumented CO [Master],audio_buffer_size which is not used in any official mapping or skin, seems to be Ok for me, if it's mentioned in the changelog.

If nobody has objections, I plan to merge this PR therefore.

@JoergAtGithub JoergAtGithub added changelog This PR should be included in the changelog and removed ui labels Jan 30, 2023
@JoergAtGithub JoergAtGithub merged commit 0f53f91 into mixxxdj:2.3 Feb 4, 2023
@JoergAtGithub
Copy link
Member

Thank you @robbert-vdh and @daschuer !

JoergAtGithub added a commit to JoergAtGithub/mixxx that referenced this pull request Feb 11, 2023
@daschuer daschuer deleted the jack-api-check-rebase branch May 4, 2023 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
changelog This PR should be included in the changelog code quality engine soundio
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants