External Sample Rate Change Handling


#1

Hey Jules, an external sample rate change to a different value causes the audio device to be stopped without any notification. Can we get a call to audioDeviceStopped (or perhaps some other preferred solution, (audioDeviceError?) ) so the application can respond to it?

CoreAudioInternal:
timerCallback calls stop(false) if the sample rate has been changed to something different than we set it to.

    void timerCallback() override
    {
        stopTimer();
        JUCE_COREAUDIOLOG ("CoreAudio device changed callback");

        const double oldSampleRate = sampleRate;
        const int oldBufferSize = bufferSize;
        updateDetailsFromDevice();

        if (oldBufferSize != bufferSize || oldSampleRate != sampleRate)
        {
            callbacksAllowed = false;
            stop (false);
            updateDetailsFromDevice();
            callbacksAllowed = true;
        }
    }

stop(false) sets the AudioIODeviceCallback to nullptr, stops the device and removes the IOProc.

    void stop (bool leaveInterruptRunning)
    {
        {
            const ScopedLock sl (callbackLock);
            callback = nullptr;
        }

        if (started
             && (deviceID != 0)
             && ! leaveInterruptRunning)
        {
            OK (AudioDeviceStop (deviceID, audioIOProc));

           #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5
            OK (AudioDeviceRemoveIOProc (deviceID, audioIOProc));
           #else
            OK (AudioDeviceDestroyIOProcID (deviceID, audioProcID));
            audioProcID = 0;
           #endif

            started = false;

            { const ScopedLock sl (callbackLock); }

            // wait until it's definately stopped calling back..
            for (int i = 40; --i >= 0;)
            {
                Thread::sleep (50);

                UInt32 running = 0;
                UInt32 size = sizeof (running);

                AudioObjectPropertyAddress pa;
                pa.mSelector = kAudioDevicePropertyDeviceIsRunning;
                pa.mScope = kAudioObjectPropertyScopeWildcard;
                pa.mElement = kAudioObjectPropertyElementMaster;

                OK (AudioObjectGetPropertyData (deviceID, &pa, 0, 0, &size, &running));

                if (running == 0)
                    break;
            }

            const ScopedLock sl (callbackLock);
        }

        if (inputDevice != nullptr)
            inputDevice->stop (leaveInterruptRunning);
    }

#2

I think the idea there was that it should just re-start at the new rate and carry on… Not sure how I’d test this myself (?) but perhaps just a simple change would do it:

[code] if (oldBufferSize != bufferSize || oldSampleRate != sampleRate)
{
AudioIODeviceCallback* oldCallback = callback;

        callbacksAllowed = false;
        stop (false);
        updateDetailsFromDevice();
        callbacksAllowed = true;

        start (oldCallback);
    }

[/code]


#3

Well I think we need a notification somewhere. If the sample rate changes then you’re going to want to update any graphs/plugins you’ve got going and so forth.

In my case the sample rate change was due to a sync’d ADAT connection silently forcing 48 khz (had to do some digging to figure out what was going on) but a basic test can be done with two instances of the plugin host demo and manually changing the sample rate in one instance and pressing the “Test” button in the settings panel of the other instance. The first instance’s test sound works but the second instance’s doesn’t.

I’ve patched my version of timerCallback in a similar way but instead of automatically restarting I use the callback to call audioDeviceStopped. In the callback I check my stored settings against what the device is set to. If the settings are different then I issue a message to the user and ask if they want to try resetting the stored setting or accept the new one. Maybe there’s a better solution for using the callback but I do think the notification is important.

if (oldBufferSize != bufferSize || oldSampleRate != sampleRate)
        {
            AudioIODeviceCallback* oldCallback = callback;

            callbacksAllowed = false;
            stop (false);
            updateDetailsFromDevice();
            callbacksAllowed = true;
            
            //notify the application that the device has stopped.
            oldCallback->audioDeviceStopped()
        }

#4

Ah, sorry, I was looking at the wrong bit of code, and assuming that the stop/start calls would send the audioDeviceStopped/started callbacks. But that happens in the other class, not this one.

I’ve made a few changes now that should restart it properly, but haven’t got a system I could test it on, so would appreciate you trying it and letting me know what happens!


#5

We’ll need to pass the callback to restart() as calling stop (false) will null it out. Maybe something like this:

    void timerCallback() override
    {
        stopTimer();
        JUCE_COREAUDIOLOG ("CoreAudio device changed callback");

        const double oldSampleRate = sampleRate;
        const int oldBufferSize = bufferSize;
        updateDetailsFromDevice();

        if (oldBufferSize != bufferSize || oldSampleRate != sampleRate)
        {
            AudioIODeviceCallback * oldCallback = callback;
            callbacksAllowed = false;
            stop (false);                           
            updateDetailsFromDevice();
            callbacksAllowed = true;

            owner.restart(oldCallback);
        }
    }

#6

Ah yes, good point. Have added that now, thanks!


#7

Sorry, my suggestion wasn’t quite right. owner’s stop() expects the internal callback to be in place. Not sure how to go with that one without resorting to some hackery.

    void stop()
    {
        if (isStarted)
        {
            AudioIODeviceCallback* const lastCallback = internal->callback;    //this will be null.

            isStarted = false;
            internal->stop (true);

            if (lastCallback != nullptr)
                lastCallback->audioDeviceStopped();
        }
    }

#8

this one’s becoming a bit of a saga!

Ok… how about just:

[code] void timerCallback() override
{
stopTimer();
JUCE_COREAUDIOLOG (“CoreAudio device changed callback”);

    const double oldSampleRate = sampleRate;
    const int oldBufferSize = bufferSize;
    updateDetailsFromDevice();

    if (oldBufferSize != bufferSize || oldSampleRate != sampleRate)
        owner.restart();
}

[/code]

and

[code] void restart()
{
AudioIODeviceCallback* oldCallback = internal->callback;
stop();

    if (oldCallback != nullptr)
        start (oldCallback);
}

[/code]


#9

This looks great and a quick test shows that things are handling properly. I had the same approach going here but was trying to maintain the call to stop (false) from within restart, which was mucking things up design wise. But it doesn’t look like there was any point in recreating the io proc anyway, so this is a nice improvement all around.

Thanks!


#10

Cool, thanks, I’ll check that in…