Trouble understand AudioDeviceSelectorComponent

Well, I’m round tripping around this component, but can’t get it to work as I’m expecting.

I’ve added a tab for this component (exactly like the juce’s demo).
I select a audio output (I have 2 possible)
Then, in another tab, I start a AudioSourcePlayer with a resampling source attached.
It plays for, well few sec, then it stops.

I’ve traced this and stop happens because the AudioDeviceSelectorComponent comboChanged callback is called (I haven’t, it’s called asynchronously), and this line is executed:

384             else if (comboBoxThatHasChanged == sampleRateDropDown)
385             {
386                 if (sampleRateDropDown->getSelectedId() > 0)
387                 {
388                     config.sampleRate = sampleRateDropDown->getSelectedId();
389                     error = setup.manager->setAudioDeviceSetup (config, true);     ////////// HERE, it stops the device and doesn't restart it.
390                 }
391             }

I’m a bit lost here.
Everything was working fine until I added this component (but obviously, writing the output device name in the source code wasn’t good)
Since it’s called async’ly I can’t figure out why it’s called at all. Even then, I don’t understand why setAudioDeviceSetup stop the device.
I can modify it with a global “isPlaying” flag, but it’s dirty, and I’m sure you have a better solution.

I expect the state (playing) is kept, or the source to be prepared again if the parameters changed, but it’s not.

I’ve used that component a lot, and never seen anything like that. Are you saying that the comboBoxChanged method is getting called without you using the combo box? It’s a synchronous callback so you should be able to see what’s triggering it. (Or maybe I’m misunderstanding what you mean).

Yes, the combobox is on a tab that’s not even visible by that time!
When I start the player, I can hear, let’s say, 1 sec of audio then it stops.
If I put a breakpoint on comboBoxChanged, and changeListenerCallback, I get the code which is triggered (automatically) this way:

Breakpoint 1, juce::AudioDeviceSettingsPanel::comboBoxChanged (this=0x2514a30, comboBoxThatHasChanged=0x2455ea0) at ../../src/gui/components/special/juce_AudioDeviceSelectorComponent.cpp:389
389                     error = setup.manager->setAudioDeviceSetup (config, true);
(gdb) list
384             else if (comboBoxThatHasChanged == sampleRateDropDown)
385             {
386                 if (sampleRateDropDown->getSelectedId() > 0)
387                 {
388                     config.sampleRate = sampleRateDropDown->getSelectedId();
389                     error = setup.manager->setAudioDeviceSetup (config, true);
390                 }
391             }
392             else if (comboBoxThatHasChanged == bufferSizeDropDown)
393             {
(gdb) c

Breakpoint 2, juce::AudioDeviceSelectorComponent::changeListenerCallback (this=0x25148a0) at ../../src/gui/components/special/juce_AudioDeviceSelectorComponent.cpp:1106
1106        if (deviceTypeDropDown != 0)
(gdb) n
1111        if (audioDeviceSettingsComp == 0
(gdb) n
1142        if (midiInputsList != 0)
(gdb) n
1148        if (midiOutputSelector != 0)
(gdb) n
1168        resized();
(gdb) c

Breakpoint 1, juce::AudioDeviceSettingsPanel::comboBoxChanged (this=0x2514a30, comboBoxThatHasChanged=0x2455ea0) at ../../src/gui/components/special/juce_AudioDeviceSelectorComponent.cpp:389
389                     error = setup.manager->setAudioDeviceSetup (config, true);
(gdb) c

and so on. If I add a breakpoint on the ::stop method of the device, it’s called in setAudioDeviceSetup.
I’ve no pointer on the AudioDeviceSelectorComponent except for the one in the tab. The AudioDeviceManager refered is the one used in the player.
If I don’t instantiate the AudioDeviceSelectorComponent, it’s working with no issue.

I’m guessing this must be something to do with the fact that you’re using Linux, or I’d have seen this before.

…but what are you doing that’s different from the demo app? That also uses tabs, but the audio doesn’t stop for me. Can you suggest a hack that I can make to reproduce this?

There isn’t anything special here.
AudioDeviceManager start a AudioSourcePlayer where the source is a ResamplingAudioSource.
I’ve tried to set up the jucedemo to reproduce the bug but I aborted (it would take too much time).

Is there a way to add the source of an async message so I can find out who triggered the combobox changed callback ?

Not easy to catch the culprit that is triggering a change message, but in this case, the only thing that could be doing would be the AudioDeviceSelectorComponent::changeListenerCallback, which would be triggered by a change in the audio device. Maybe the ALSA device is firing a change message unnecessarily?

I’ve noticed this:

  1. I play with the device selector component,
  2. select the second sound card,
  3. click “Advanced settings” to show the sample rate combo,
  4. change from 44100 to 48000,
  5. finally change tab and hit play, then it doesn’t stop.

I’m using the sample ratio set from the source file in the initialization.

Ok, more evidences here:
In the message thread, I query the stream’s source sample rate, and open the device with such sample rate.
On success, I start the device and then start the decoding thread.
Then, I exit the loading function, and the message loop resume.

It seems like the open(device) step queue an async message to the DeviceSelectorComponent to tell it that the sampling rate changed.
However, between the time the device is started, and the message is received, the thread have started and start to play content.
When the message is received (later), the DeviceSelectorComponent try to re-change the sampling rate (likely, it doesn’t do anything, but this still stop the device).

Seems to me that the DeviceSelectorComponent should only call setAudioDeviceSetup() if the sampling rate actually changed (so the callback will still trigger, but it won’t cause the device manager to stop the callback).
What do you think ?

audiodevicemanger::setAudioDeviceSetup always checks the new setup, so that if you call it with the same settings that are already playing, it’ll ignore it. So I don’t really see how the component could make it restart unless something actually did change the sampling rate.

I’ve debugged the “setup” state in the comboBoxChanged callback, and it seems that in this callback, the audio device manager still doesn’t use the new sampling rate value by that time.
So I’ve solved it by adding this ugly hack in my code:

 if (audioManager.getCurrentAudioDevice()->open(0, channelMap, sampleRate, sampleCount) == String::empty)
                        AudioDeviceManager::AudioDeviceSetup config;
                        audioManager.getAudioDeviceSetup (config);
                        config.sampleRate = sampleRate;
                        config.bufferSize = sampleCount;
                        config.outputChannels = channelMap;
                        config.inputChannels = 0;

                        // This prevent the audio manager to stop the device when
                        // the callback called by the open() call above will be triggered
                        audioManager.setAudioDeviceSetup(config, true);

I think the open() call above should have triggered a synchronous message in that case, to avoid my code from doing what the callback does.

Are you setting these properties directly on the AudioIODevice object, or via the AudioDeviceManager? The device manager doesn’t actually listen for changes to its device, because it doesn’t expect you to change the device’s settings directly. So if you just get a pointer to the device and start setting values, the manager won’t pick that up at all. The idea is that you do all the setup through the manager, and avoid communicating with the device directly.

So you mean I’m not supposed to call device->open() at all, but instead use deviceManager.setAudioSetup() and deviceManager.addAudioCallback()
So this code is wrong ?

 if (!audioManager.getCurrentAudioDevice()) 
         audioManager.initialise(0, track->audioFormat.getChannelCount(), savedXMLSetup, true);
 if (audioManager.getCurrentAudioDevice()) 
     if (audioManager.getCurrentAudioDevice()->open(0, channelMap, sampleRate, sampleCount) == String::empty)
           // Start threads and playback here

How can I ensure the audio device opened successfully then ?

Yes, that’s completely wrong. The whole point of the manager is that it saves you from having to deal with the device objects directly. You don’t need to open it, or really to care whether it opened correctly or not, you just deal with the manager and let it worry about creating and deleting its device. All you have to do is:

audioManager.initialise (0, track->audioFormat.getChannelCount(), savedXMLSetup, true); audioManager.addAudioCallback (&sourcePlayer);

You can tell whether it opened by checking the error message returned by the initialise() call.

It’s not completely equivalent. I can’t set the samplingRate or the channelMap (or the buffer size) this way.
Or, maybe I have to deal with modifying the XML source, but it seems complex for this task.
Or should I initialize, then query the setup, change it, and set it back ? (in that case, it still tedious, or am I missing something ?)

Thanks for your answer in all case.

The manager is designed to make it easy to let the system create and manage the device, and to let the user choose their settings. It’s not designed to give you programmatic control over the device - if you need more control, then maybe it’d be better to create and manage the device directly, without using a manager object.

I guess it’s not a solution, as I would loose the AudioDeviceSelectorComponent functionality.
Anyway, what about adding at least a sampling rate parameter to the AudioDeviceManager::initialise call ?
I don’t think it’s a bad idea to set the sampling rate and check if the audio device accept it (as most audio device support multiple sampling rate). This saves some systematic software based resampling why the sound card probably does this better and faster.

This way I can save the getSetup / update / setSetup state and remove all my audio device hacking code directly.

What’s wrong with using the preferredSetupOptions object to give it the sample rate that you want?

I guess nothing. It cause me headache to understand this (I understand it as a chicken & egg issue, as how can you get a setup object while the audio device is not initialized ?)
My setup use 2 cards, so if I query a getAudioDeviceSetup on an AudioDeviceManager that’s not initialized, won’t that return a valid setup object only for the first sound card ?
Then, if the latter call to initialise fails, is it because of the unsupported sample rate or because of any -not related- unsupported feature that the first card support but not the second one ?

As I can’t parse the returned string for the call (driver dependent error), there is no way to know, am I wrong ?

AudioDeviceSetup setup; setup.sampleRate = whateverYouWant;

Excellent. So the default value of the AudioDeviceSetup’s member means “ignore me”.
Great. Thanks for the answer, it’s crystal clear now.