Automatically switch audio output when headphones inserted

I want to make my app use headphones when they are available, otherwise speakers, and handle plugging and unplugging headphones dynamically.

I can get a changeListenerCallback whenever headphones are inserted or removed. But how to disambiguate between insert/remove?

I'm guessing probably enumerate through all input devices present and attempt to match *phones* against the name of each.  But I can't see any method of AudioDeviceManager for enumerating devices.

How should I be doing this?

π

what does your app do when you plug headphones in now? does it still play thru the speakers?  

Dig thru the JuceDEMO source code for the AudioSettings demo, where they instantiate the ComboBoxes with the available devices

https://github.com/julianstorer/JUCE/blob/4d34212557658c4201365c2c66cfe20adb4a7f6f/modules/juce_audio_utils/gui/juce_AudioDeviceSelectorComponent.cpp "addNamesToDeviceBox()" is what you're looking for.

https://www.juce.com/doc/classAudioIODeviceType#a949e9e1d52d22c8b5929e85bf5e607dc

Just to close this thread off...

The following code triggers a callback every time a device is removed or added, which compares the new devices StringArray with the old and extracts the string for the device in question. It then a attempts to initialise the output device manager with said device. If this device was just removed, the initialisation will fall back onto the default output device.

This has the effect of switching to headphones when they are plugged in and back to speakers when they are removed.

 Also included is a GUI for manually switching output.

π

class AudioSettings : ChangeListener
{
private:
    ScopedPointer<AudioDeviceManager> pInputDeviceManager;
    ScopedPointer<AudioDeviceManager> pOutputDeviceManager;
    StringArray deviceNames;

public:
    // GUI to chanage settings
    void launchInputWindow()  { launch(pInputDeviceManager,  1, 0); }
    void launchOutputWindow() { launch(pOutputDeviceManager, 0, 2); }

    void launch(AudioDeviceManager* pDeviceManager, int inputs, int outputs)
    {
        AudioDeviceSelectorComponent* settingsPane = new AudioDeviceSelectorComponent(
            *pDeviceManager,
            inputs, inputs,        // min/max inputs
            outputs, outputs,    // min/max outputs
            inputs > 0,            // showMidiInputOptions
            outputs > 0,        // showMidiOutputSelector
            true,                // showChannelsAsStereoPairs
            true                // showAdvancedOptions
            );
        settingsPane->setSize(640, 480);

        DialogWindow::LaunchOptions options;

        options.content.setOwned(settingsPane);
        options.dialogTitle = "Audio Settings";
        options.dialogBackgroundColour = Colours::lightgrey;
        options.escapeKeyTriggersCloseButton = true;
        options.useNativeTitleBar = true;
        options.resizable = true;

        DialogWindow* dialogWindow = options.launchAsync();
        dialogWindow->centreWithSize(450, 250);
    }

    AudioDeviceManager& getInputDeviceManager()  { return *pInputDeviceManager;  }
    AudioDeviceManager& getOutputDeviceManager() { return *pOutputDeviceManager; }

    AudioSettings()
    {
        pInputDeviceManager = new AudioDeviceManager();
        pOutputDeviceManager = new AudioDeviceManager();

        pInputDeviceManager->initialise(
            0,            // numInputChannelsNeeded
            0,            // numOutputChannelsNeeded
            nullptr,    // XML
            true,        // selectDefaultDeviceOnFailure
            String(),    // preferredDefaultDeviceName
            0            // preferredSetupOptions
            );

        pOutputDeviceManager->initialise(
            0,            // numInputChannelsNeeded
            2,            // numOutputChannelsNeeded
            nullptr,    // XML
            true,        // selectDefaultDeviceOnFailure
            String(),    // preferredDefaultDeviceName
            0            // preferredSetupOptions
            );

        pOutputDeviceManager->addChangeListener(this);

        deviceNames = pOutputDeviceManager->getCurrentDeviceTypeObject()->getDeviceNames();
    }
public:
    void changeListenerCallback(ChangeBroadcaster*) override
    {
        AudioDeviceManager& output = *pOutputDeviceManager;
        
        StringArray newDeviceNames = output.getCurrentDeviceTypeObject()->getDeviceNames();
        
        bool didAddDevice = newDeviceNames.size() > deviceNames.size();
        StringArray& smaller = didAddDevice ? deviceNames : newDeviceNames;
        StringArray& larger = didAddDevice ? newDeviceNames : deviceNames;

        String device;
        for (String s : larger)
            if (!smaller.contains(s))
                device = s;
        
        DBG("changeListenerCallback: " + String(didAddDevice ? "Added " : "Removed ") + device);

        // if removed a device, this will fail for device, but fallback to default
        output.removeChangeListener(this);
        output.initialise(
            0,            // numInputChannelsNeeded
            2,            // numOutputChannelsNeeded
            nullptr,    // XML
            true,        // selectDefaultDeviceOnFailure
            device,        // preferredDefaultDeviceName
            0            // preferredSetupOptions
            );
        output.addChangeListener(this);

        deviceNames = newDeviceNames;
    }
}

 

I'm getting a problem with the above code (running on Windows):

{ // ctor
    :
    pOutputDeviceManager->addChangeListener(this);
    deviceNames = pOutputDeviceManager->getCurrentDeviceTypeObject()->getDeviceNames();
}

void changeListenerCallback(ChangeBroadcaster*) override {
    AudioDeviceManager& output = *pOutputDeviceManager; 
    StringArray newDeviceNames = output.getCurrentDeviceTypeObject()->getDeviceNames(); // (*)

    // Now compare deviceNames with newDeviceNames...

When I plug in my headphones, (*) correctly lists all devices. But when I unplug them, it is still returning the same list!

I think this is a bug.

I've tried setting breakpoints and walking back through the call stack, but I can't see any mistake.

 I remove headphones.

https://github.com/julianstorer/JUCE/blob/master/modules/juce_audio_devices/native/juce_win32_DirectSound.cpp#L1264 calls my changeListenerCallback

If I enumerate newList.outputDeviceNames there, the headphones entry has (correctly?) vanished:

Primary Sound Driver
Speakers (Cirrus Logic CS4206B (AB 06))
Digital Audio (S/PDIF) (Cirrus Logic CS4206B (AB 06))

But enumerating (*), it has appeared again:

Headphones (Cirrus Logic CS4206B (AB 06))
Digital Audio (S/PDIF) (Cirrus Logic CS4206B (AB 06))
Speakers (Cirrus Logic CS4206B (AB 06))

However about 20% of the time the Headphones (Cirrus Logic CS4206B (AB 06)) line doesn't appear.

I just wanted to check in before doing a second round of debugging.

Does this look like a bug so far?

π

PS EDIT: https://github.com/julianstorer/JUCE/blob/master/modules/juce_events/native/juce_win32_HiddenMessageWindow.h#L109

Ok,..

Windows registers Device-Removed-Event here:

https://github.com/julianstorer/JUCE/blob/master/modules/juce_events/native/juce_win32_HiddenMessageWindow.h#L123

-> line 105:

void triggerAsyncDeviceChangeCallback()
{
    // We'll pause before sending a message, because on device removal, the OS hasn't always updated
    // its device lists correctly at this point. This also helps avoid repeated callbacks.
    startTimer (500); // <-- trying 1500 doesn't fix
}

-> line 133

-> https://github.com/julianstorer/JUCE/blob/master/modules/juce_audio_devices/native/juce_win32_DirectSound.cpp#L1256

    void systemDeviceChanged() override
    {
        DSoundDeviceList newList;
        newList.scan();
        if (newList != deviceList)
        {
            deviceList = newList;
            for (auto s : newList.outputDeviceNames) // <-- I add this...
                DBG(s);
            callDeviceChangeListeners();
        }
    }

output:

Primary Sound Driver
Speakers (Cirrus Logic CS4206B (AB 06))
Digital Audio (S/PDIF) (Cirrus Logic CS4206B (AB 06))

I don't know what Primary Sound Driver entails... anyway -- onwards:

Only one listener, here: https://github.com/julianstorer/JUCE/blob/master/modules/juce_audio_devices/audio_io/juce_AudioDeviceManager.cpp#L79

-> line 195:

void AudioDeviceManager::audioDeviceListChanged()
{
    if (currentAudioDevice != nullptr)
    {
        DBG(currentAudioDevice->getName()); // <-- "Headphones (Cirrus Logic CS4206B (AB 06))"
        currentSetup.sampleRate = currentAudioDevice->getCurrentSampleRate();
        currentSetup.bufferSize = currentAudioDevice->getCurrentBufferSizeSamples();
        currentSetup.inputChannels = currentAudioDevice->getActiveInputChannels();
        currentSetup.outputChannels = currentAudioDevice->getActiveOutputChannels();
    }
    sendChangeMessage();
}

Should currentAudioDevice still be referencing the just-removed-device?

Moving on...

-> https://github.com/julianstorer/JUCE/blob/master/modules/juce_events/broadcasters/juce_ChangeBroadcaster.cpp#L61

-> https://github.com/julianstorer/JUCE/blob/master/modules/juce_events/broadcasters/juce_AsyncUpdater.cpp#L61

... which emits a Windows message

... which gets picked up again in juce_ChangeBroadcaster.cpp#L92

... which eventually triggers my callback:


class MainContentComponent : public AudioAppComponent, ChangeListener
{
public:
    MainContentComponent() {
        setSize(800, 600);
        setAudioChannels(0, 2);
        deviceManager.addChangeListener(this);
    }
    void changeListenerCallback(ChangeBroadcaster*) override {
        static int i=0; DBG("\nchangeListenerCallback:" + String(i++));
        StringArray newDeviceNames = deviceManager.getCurrentDeviceTypeObject()->getDeviceNames();
        for (auto s : newDeviceNames)
            DBG(s);
    }

Aaaaaaaaaaaaaaah!

This gets hit twice!

changeListenerCallback:0
Headphones (Cirrus Logic CS4206B (AB 06))
Digital Audio (S/PDIF) (Cirrus Logic CS4206B (AB 06))
Speakers (Cirrus Logic CS4206B (AB 06))

changeListenerCallback:1
Speakers (Cirrus Logic CS4206B (AB 06))
Digital Audio (S/PDIF) (Cirrus Logic CS4206B (AB 06))

Inserting the headphones gives another pair of hits!

My code was diffing the output with the prev-output, and first run was generating an empty string.

Wow.

Well, that was a fun run through the innards of JUCE audio.

https://youtu.be/IjarLbD9r30?t=38s

:]

Why is it hitting twice, out of curiosity?

π

Ok this works…

// Settings.h
#ifndef SETTINGS_H_INCLUDED
#define SETTINGS_H_INCLUDED
#include "../JuceLibraryCode/JuceHeader.h"
using namespace ::juce;
class AudioSettings : ChangeListener
{
private:
    ScopedPointer<AudioDeviceManager> pInputDeviceManager;
    ScopedPointer<AudioDeviceManager> pOutputDeviceManager;
    void launch(AudioDeviceManager* pDeviceManager, int inputs, int outputs)
    {
        AudioDeviceSelectorComponent* settingsPane = new AudioDeviceSelectorComponent(
            *pDeviceManager,
            inputs, inputs,        // min/max inputs
            outputs, outputs,    // min/max outputs
            inputs > 0,            // showMidiInputOptions
            outputs > 0,        // showMidiOutputSelector
            true,                // showChannelsAsStereoPairs
            true                // showAdvancedOptions
            );
        settingsPane->setSize(640, 480);
        DialogWindow::LaunchOptions options;
        options.content.setOwned(settingsPane);
        options.dialogTitle = "Audio Settings";
        options.dialogBackgroundColour = Colours::lightgrey;
        options.escapeKeyTriggersCloseButton = true;
        options.useNativeTitleBar = true;
        options.resizable = true;
        DialogWindow* dialogWindow = options.launchAsync();
        dialogWindow->centreWithSize(450, 250);
    }
public:
    AudioDeviceManager&amp; getInputDeviceManager() {
        return *pInputDeviceManager;
    }
    AudioDeviceManager&amp; getOutputDeviceManager() {
        return *pOutputDeviceManager;
    }
    AudioSettings()
    {
        pInputDeviceManager = new AudioDeviceManager();
        pOutputDeviceManager = new AudioDeviceManager();
        auto deviceSetup = AudioDeviceManager::AudioDeviceSetup();
        deviceSetup.sampleRate = 48000;
        pInputDeviceManager->initialise(
            0,                // numInputChannelsNeeded
            0,                // numOutputChannelsNeeded
            nullptr,        // XML
            true,            // selectDefaultDeviceOnFailure
            String(),        // preferredDefaultDeviceName
            &amp;deviceSetup    // preferredSetupOptions
            );
        pOutputDeviceManager->initialise(
            0,                // numInputChannelsNeeded
            2,                // numOutputChannelsNeeded
            nullptr,        // XML
            true,            // selectDefaultDeviceOnFailure
            String(),        // preferredDefaultDeviceName
            &amp;deviceSetup    // preferredSetupOptions
            );
        
        DBG(pOutputDeviceManager->getCurrentAudioDevice()->getCurrentSampleRate());
        for (auto s : pOutputDeviceManager->getCurrentAudioDevice()->getAvailableSampleRates())
            DBG(s);
    
        //deviceNames = pOutputDeviceManager->getCurrentDeviceTypeObject()->getDeviceNames();
        pOutputDeviceManager->addChangeListener(this);
    }
private:
    StringArray deviceNames;
public:
    void changeListenerCallback(ChangeBroadcaster*) override
    {
        AudioDeviceManager&amp; output = *pOutputDeviceManager;
        
        StringArray newDeviceNames = output.getCurrentDeviceTypeObject()->getDeviceNames();
        if (deviceNames.size() == 0) // first time only!
            deviceNames = newDeviceNames;
        // changeListenerCallback hits twice for each insert or removal, giving us 
        // before-change and after-change lists
        if (newDeviceNames.size() == deviceNames.size()) 
            return;
        bool didAddDevice = newDeviceNames.size() > deviceNames.size();
        StringArray&amp; smaller = didAddDevice ? deviceNames : newDeviceNames;
        StringArray&amp; larger = didAddDevice ? newDeviceNames : deviceNames;
        String device;
        for (String s : larger)
            if (!smaller.contains(s))
                device = s;
        
        DBG("changeListenerCallback: " + String(didAddDevice ? "Added " : "Removed ") + device);
        output.removeChangeListener(this);
        // if removed a device, this will fail for device, but fallback to default
        auto deviceSetup = AudioDeviceManager::AudioDeviceSetup();
        deviceSetup.sampleRate = 44100;
        output.initialise(
            0,                // numInputChannelsNeeded
            2,                // numOutputChannelsNeeded
            nullptr,        // XML
            true,            // selectDefaultDeviceOnFailure
            device,            // preferredDefaultDeviceName
            &amp;deviceSetup    // preferredSetupOptions
            );
        output.addChangeListener(this);
        jassert(output.getCurrentAudioDevice());
        DBG(output.getCurrentAudioDevice()->getCurrentSampleRate());
        deviceNames = newDeviceNames;
    }
    void launchInputWindow() {
        launch(pInputDeviceManager, 1, 0);
    }
    void launchOutputWindow() {
        launch(pOutputDeviceManager, 0, 2);
    }
};
#endif  // SETTINGS_H_INCLUDED

π

can you edit your code so that it doesn’t say “&deviceSetup” or “output.getCurrentDeviceTypeObject()->getDeviceNames();”

I guess the new forum doesn’t like > < or & symbols.

Fixed.

Yep there’s some fail going on migrating old code.

Needs some script to search&replace " & l t ;" with “<” etc (had to insert spaces heh), and remove bolded sections.

It’s everywhere!

π