MacOS: Round-Trip Latency suddenly increased in August 22

Hello,

I develop https://repetito.com, a software looper that uses Juce, and have an issue with round-trip latency.

Round-trip latency is important for a looper to be able to synchronize loops being recorded with loops being played, so I measure this latency and precisely compensate for it.

When testing Repetito with recent Juce versions, I noticed that round-trip latency suddenly increased - almost twofold!

Following is a link to an xcode 12 project that can be used to reproduce the issue. The app measures round-trip latency simply by playing a 2 second silence with a digital click in the middle, and recording the input while this audio is played. If the input picks up the audio output, the app is able to measure the round-trip latency.

https://repetito.com/RoundTripLatency/RoundTripLatency.zip

With this test app, I was able to find the commit that introduced this issue:
5ec536f13 (“CoreAudio: Forward errors to callback during device initialisation”, 2022-08-18)

Here’s the previous commit, and changing from that commit to 5ec536f13 introduces the issue:
5b355f637 (“Graph: Tidy up names”, 2022-08-12)

I’ve looked a bit at the code change but am not familiar with AudioIODeviceCombiner. Removing the ScopedErrorForwarder in AudioIODeviceCombiner fixes the issue - but that’s more of a workaround. I can’t see anything wrong in ScopedErrorForwarder so I suppose the issue is more in the interaction between ScopedErrorForwarder and AudioIODeviceCombiner.

To test: if you are using the mac’s audio device, you simply need to have a decent output volume. If you are using an external audio device, a mic must be set so it picks up the audio output (some channel config is necessary if the input channel is not the one with the mic since the app uses the first input channel).

Test Process:

  • unzip the project;
  • git clone Juce in the specified directory;
  • git checkout 5ec536f13 or more recent, run the app and check the Real-Time Latency value (the issue is present, RTL is big);
  • git checkout 5b355f637 or older, run the app and check Real-Time Latency (the issue is not present, RTL is small);

I’m testing on a M1 2020 MacBook Air running macOS 12.2.1 with the internal audio device, but I’ve also tested with other devices.

Note: the measured round-trip latency is not exactly the same when testing multiple times - but it doesn’t change much (less than a few ms with my tests).

Could someone at Juce look into this?

Thanks! -Mathieu

Thank you for the example, we will take a look. After a cursory glance, I could not figure out what’s happening, but I was seeing an increase of 60-70 ms with a variance of 30 ms in my case.

Will take a closer look soon.

1 Like

I don’t see such a high variance, but I suppose your results are related to a large buffer size setting.

Speaking of statistics, I did further tests with different audio device settings, and compared average RTL values on 10 tests before and after the 5ec536f13 commit.

What I notice is that on average, RTL is increased of an integer value of the audio callback buffer sample size: I’ve measured 4 for 64/44.1kHz, 5 for 256/88.2 kHz and 6 for 512/48 kHz.

Looks like a cache of that size is added somewhere…

(I’ve updated the https://repetito.com/RoundTripLatency/RoundTripLatency.zip to be able to specify the device and its buffer size and sampling rate)

I can now see how that change lead to increased roundtrip latency. It’s roughly a consequence of line 1699 of juce_mac_CoreAudio.cpp in 5ec536f1, where an indeterminate amount of extra items could be added to the output FIFO if the callback, which was previously nullptr, is now activated by the ScopedErrorForwarder.

The bad news is, this isn’t relevant anymore, because the AudioIODeviceCombiner has been refactored since, and in its current revision having the ScopedErrorForwarder or not has no effect on the roundtrip latency anymore.

The RTL is higher than it was in the old commits, but it also looks completely deterministic, whereas with the old revisions I was seeing a fluctuation of about 30 ms.

I know that real improvements went into the device combiner, so for now all I can say, is that I’ll give it a closer look and see if the latency can be reduced.

Thanks @attila for this investigation and information!

Further thoughts:

When comparing the audio callback callstack at different Juce versions, I notice that:

At 5b355f637 (“Graph: Tidy up names”, 2022-08-12), at 5ec536f13 (“CoreAudio: Forward errors to callback during device initialisation”, 2022-08-18) and before that: audioDeviceIOCallbackWithContext is called from within AudioIODeviceCombiner::run, a specific AudioIODeviceCombiner thread.

Whereas at the most recent Juce versions, audioDeviceIOCallbackWithContext is called directly from the CoreAudio AudioDeviceIOProc entry point HALC_ProxyIOContext::IOWorkLoop().

I understand that with older Juce versions, when processing audio in a specific AudioIODeviceCombiner thread, it was necessary to add cache buffers to pass audio from one context to another. Cache buffers add latency.

But now that AudioDeviceIOProc is called directly from HALC_ProxyIOContext::IOWorkLoop(), there shouldn’t be any cache buffers.

When processing audio from within HALC_ProxyIOContext::IOWorkLoop(), the RTL should therefore be lowest as possible. At least as low as the values measured with the 5b355f637 commit and before that.