Losing audio sync with CoreAudio

I work on a sound spatialization software called SpatGRIS.

We use AudioSourcePlayer::audioDeviceIOCallback() (JUCE 7.0.2) to get access to audio buffers and make sound spatilization up to 128 channels. Audio travels from DAW to a sound server (jack), or virtual audio loopback driver (128 channel BlackHole on MacOS), get processed for spatialization in SpatGris, and finally goes out on the multichannel audio system.

Starting with JUCE 7.0.3, we experience a situation that looks like audio drift. Depsite output VU meters showing audio is getting out, we hear distortion, then total silence. This occurs after about 50 minutes of continuous playing with JUCE 7.0.3, and after about 8 minutes with JUCE 7.0.4 and 7.0.5.
The problem is happening with CoreAudio only. ASIO and pipewire-jack play just fine.

The only change in our audio related code JUCE 7.0.3 brings is AudioSourcePlayer::audioDeviceIOCallbackWithContext(), which does not make any difference for our code to compile.

Is there a way to prevent clock drift with JUCE ?
Has anyone experience this kind of issue ?

Any help is appreciated !

I can’t think of any changes in JUCE that would obviously lead to the behaviour you’re seeing. Why do you believe that the issue involves clock drift? Based on the description you’ve given, I’d be inclined to check for infs or nans in the output. I think CoreAudio might have a protection feature where it mutes output that contains these sorts of values (not 100% sure about this though). Have you tried adding checks for out-of-range values in the output?

Thanks for your reply!

I made some checks for out-of-range values in the output, and I don’t catch anything when the glitch happens.

Only because it sounds like it. But maybe this is something else. We made a lot of tests in issue 389 (in french) and it appears a build with JUCE 7.0.2 plays fine on Win, Mac and linux. The same build with JUCE 7.0.3 and above brings audio glitches, distortion and silence on Mac only. I don’t understand why we have a problem on one platform only. This is why I’m looking for platform specific code in JUCE. And I see that juce_mac_CoreAudio.cpp had a lot of changes for 7.0.3.

Any idea on what could make audio glitch/distortion/silence after 50 minutes of playing ?

There’s some code in JUCE that is designed to output silence in the case that the output thread runs out of valid data from the input thread to read.

When the output becomes silent, what’s the value of numZerosToWrite in outputAudioCallback?

Under normal circumstances, this value should be zero. If numZerosToWrite is non-zero, that might indicate consistent xruns, clock drift, or some other timing issue. However, if numZerosToWrite is zero, then the problem is probably elsewhere.

As I’m unable to reproduce the problem locally, I recommend inspecting the program thoroughly after it has broken. A good starting point would be to work out exactly where the silence is coming from.

  • If your audioDeviceIOCallbackWithContext produces silence, then the problem happens before reaching JUCE code.
  • If your audioDeviceIOCallbackWithContext has non-silent output, but the outputAudioCallback produces silence, that would indicate that the problem is likely in JUCE’s fifo/synchronisation code.
  • If the outputAudioCallback output is not silent, then that might indicate that the system has muted the output for some reason.

It would also be helpful if you could provide a minimal code example, and/or detailed instructions to reproduce the issue, so that I can attempt to debug the issue locally if necessary.

When the glitch happens, my audioDeviceIOCallbackWithContext never produces silence, and numZerosToWrite is non-zero. Its value is the size of the buffer most of the time.

When silence occurs, restarting SpatGris does not always bring back the sound. It looks like CoreAudio is ignoring JUCE. To make it work again we have to restart CoreAudio with :

sudo launchctl kickstart -kp system/com.apple.audio.coreaudiod

We have a temporary fix. We can use an aggregate device with audio drift correction and the clock source set on the external sound card. But this is not ideal for our setup.

I tried to reproduce the issue outside of SpatGris (using audioDeviceIOCallbackWithContext and an AudioDeviceManager ) without success. Until I’m able to do so, reproducing the issue locally would mean building SpatGris.

Let me know if you’re willing to go down that road.

I encountered a similar issue trying to reproduce the issue outside of SpatGris. It might be related, as it has to do with buffer size and number of input and output channels, and is not reproducible in JUCE 7.0.2. And the sound of the glitch is the same described in the previous posts. (And fortunately we don’t have to wait one hour for the glitch to appear.)

In the app from the code below, randomly changing the buffer size and toggling on and off inputs and outputs make the glitch happen.

Conditions to reproduce the issue :

  • JUCE 7.0.3, 7.0.4 or 7.0.5
  • The AudioDeviceManager has to be initialized with 128 input channels.
  • Use an external sound card for audio output.

Setup:

  • Use BlackHole (or any 128 channel capable virtual audio device) as audio driver for your DAW. (BlackHole 128 is included in the SpatGris installer (no need to install SpatGris).
  • Configure your DAW to send audio to multiple BlackHole channels. Enough to cover your tests (the app only uses the first 2 available input and output channels).
  • In the app from the code below, set BlackHole as audio input and an external sound card for audio output.

Thank you @reuk or anyone helping me with this. I know it looks like an edge case, but it is the core design that make SpatGris simple and easy to use on MacOS.

Here’s the code :
MainComponent.h

#pragma once

#include <JuceHeader.h>

//==============================================================================
class MainComponent
    : juce::AudioSourcePlayer
    , public juce::Component
{
public:
    //==============================================================================
    MainComponent();
    ~MainComponent() override;

    //==============================================================================
    void audioDeviceError(const juce::String & errorMessage) override;
    void audioDeviceIOCallbackWithContext(const float * const * inputChannelData,
                                          int totalNumInputChannels,
                                          float * const * outputChannelData,
                                          int totalNumOutputChannels,
                                          int numSamples,
                                          const juce::AudioIODeviceCallbackContext & context) override;
    void audioDeviceAboutToStart(juce::AudioIODevice * device) override;
    void audioDeviceStopped() override;

    //==============================================================================
    void paint(juce::Graphics & g) override;
    void resized() override;

private:
    //==============================================================================
    juce::AudioDeviceManager mAudioDeviceManager{};
    juce::AudioDeviceSelectorComponent mAudioDeviceSelectorComponent;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainComponent)
};

MainComponent.cpp:

#include "MainComponent.h"

//==============================================================================
MainComponent::MainComponent()
    : mAudioDeviceSelectorComponent(mAudioDeviceManager, 0, 128, 0, 128, false, false, false, false)
{
    setSize(800, 600);

    addAndMakeVisible(mAudioDeviceSelectorComponent);

    mAudioDeviceManager.initialise(128, 2, nullptr, true);
    mAudioDeviceManager.addAudioCallback(this);
}

//==============================================================================
MainComponent::~MainComponent()
{
    mAudioDeviceManager.removeAudioCallback(this);
}

//==============================================================================
void MainComponent::audioDeviceError(const juce::String & errorMessage)
{
    jassertfalse;
}

//==============================================================================
void MainComponent::audioDeviceIOCallbackWithContext(const float * const * inputChannelData,
                                                     int totalNumInputChannels,
                                                     float * const * outputChannelData,
                                                     int totalNumOutputChannels,
                                                     int numSamples,
                                                     const juce::AudioIODeviceCallbackContext & context)
{
    std::for_each_n(outputChannelData, totalNumOutputChannels, [numSamples](float * const data) {
        std::fill_n(data, numSamples, 0.0f);
    });

    // directly copy inputChannelData to outputChannelData
    auto const numInputChannelsToCopy{ std::min(totalNumInputChannels, 2) };
    for (int i{}; i < numInputChannelsToCopy; ++i) {
        auto const * sourceData{ inputChannelData[i] };
        auto * destinationData{ outputChannelData[i] };
        std::copy_n(sourceData, numSamples, destinationData);
    }
}

//==============================================================================
void MainComponent::audioDeviceAboutToStart(juce::AudioIODevice * device)
{
    std::cout << "audio device about to start" << std::endl;
}

//==============================================================================
void MainComponent::audioDeviceStopped()
{
    std::cout << "audio device stopped" << std::endl;
}

//==============================================================================
void MainComponent::paint(juce::Graphics & g)
{
    g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
}

//==============================================================================
void MainComponent::resized()
{
    mAudioDeviceSelectorComponent.setSize(getWidth(), getHeight());
}

Following up almost a year later; I think I’m facing the same problem. I think this has to do with a non-null context in the mac core audio file. Curious to see if doing this will reproduce the issue for you Core Audio glitching