AudioGraphIOProcessor plays only first two channels since commit 4fa0516


#1

Hi everybody,
I develop a standalone app that uses an AudioProcessorGraph.
During an initialisation I call

AudioSourceProcessor* proc2 = new AudioSourceProcessor(src2);
AudioProcessorGraph::Node::Ptr srcNode2 = this->addNode(proc2);
addConnection(srcNode2->nodeId, 0, themeProcNode->nodeId, numStems*2);
addConnection(srcNode2->nodeId, 1, themeProcNode->nodeId, numStems*2 + 1);

Since the commit 4fa0516 "Revised multibus API and added support for multibus hosting" I hear only the first stem (first two channels).
I added DBGs in getNextAudioBlock of the sources, they are called.
I tried to override isLayoutSupported() to return always true, but that is not called (there is no host involved, it’s played by an AudioProcessorPlayer).

I have no idea, what changed, that would require an action on my side…

Any hints are greatly appreciated,
Cheers.


#2

Hi Daniel,

It’s hard to know what’s going wrong without seeing the implementation of AudioSourceProcessor, so please excuse me if I’m babbling about something you probably already know:

The revised multibus API was written in a way so that it remains backward compatible to pre 4.1 JUCE projects, i.e. just pretend that the deprecated multibus API was never there :slight_smile: .

In fact when testing the new multibus API, I tried compiling the 4.0 version of the plug-in host with the new latest JUCE on develop (keeping the plug-in host source code at version 4.0) and tested that everything worked. I also did the same thing but reverse: I compiled the 4.0 version of the audio plugin demo with the new JUCE multibus API and checked that it works both in the old host and new host.

Pre 4.1, the default number of input and output channels of an AudioProcessor was always zero (see code here). Also the AudioProcessorGraph would never change the number of channels. This means that when using the AudioProcessorGraph in pre JUCE 4.1 code, you would need to call setPlayConfigDetails on each node with the desired number of channels before adding them. The audio plugin host does this indirectly as most nodes are AudioPluginInstances and the setPlayConfigDetails will be called by the wrapper when the AudioPluginInstance is created.

As was the case with pre 4.1 JUCE, the revised multibus API once again requires you to tell the graph how many channels a Node should have by calling setPlayConfigDetails. However, with the new multibus API, it is also possible to tell the graph how many channels the node should have by calling setBusesLayout instead - or by having the AudioProcessor create a default layout by using the new non-default AudioProcessor constructors.

See some code below that uses the latter technique. You can just copy it into the Main.cpp of a new GUI application - you also need to add the juce_audio_utils module.

Hope this helps!

Fabian

#include "../JuceLibraryCode/JuceHeader.h"

//==============================================================================
class MultiToneAudioProcessor : public AudioProcessor
{
public:
    static constexpr int numOutStems = 2;
    static constexpr int channelsPerStem = 2;
    
    MultiToneAudioProcessor ()
        : AudioProcessor (getBusProperties())
    {
        for (int i = 0; i < numOutStems; ++i)
            tones.add (new ToneGeneratorAudioSource);
    }
    
    //==============================================================================
    const String getName() const override                { return "MultiToneAudioProcessor"; }
    double getTailLengthSeconds() const override         { return 0.0; }
    bool acceptsMidi() const override                    { return false; }
    bool producesMidi() const override                   { return false; }
    AudioProcessorEditor* createEditor() override        { return nullptr; }
    bool hasEditor() const override                      { return false; }
    int getNumPrograms() override                        { return 0; }
    int getCurrentProgram() override                     { return -1; }
    void setCurrentProgram (int) override                {}
    const String getProgramName (int) override           { return String(); }
    void changeProgramName (int, const String&) override {}
    void getStateInformation (MemoryBlock&) override     {}
    void setStateInformation (const void*, int) override {}
    
    //==============================================================================
    void prepareToPlay (double sampleRate, int maximumExpectedSamplesPerBlock) override
    {
        jassert (getBusCount (false) == numOutStems);
        
        for (int i = 0; i < numOutStems; ++i)
        {
            if (ToneGeneratorAudioSource* source = tones[i])
            {
                source->setAmplitude (0.5f);
                source->setFrequency (440.0f + (static_cast<float> (i) * 110.f));
                source->prepareToPlay (maximumExpectedSamplesPerBlock, sampleRate);
            }
        }
    }
    
    void releaseResources() override
    {
        for (int i = 0; i < numOutStems; ++i)
            if (ToneGeneratorAudioSource* source = tones[i])
                source->releaseResources();
    }
    
    void processBlock (AudioBuffer<float>& buffer, MidiBuffer&) override
    {
        jassert (getBusCount (false) == numOutStems);
        
        for (int busIdx = 0; busIdx < numOutStems; ++busIdx)
        {
            AudioBuffer<float> busBuffer = getBusBuffer (buffer, false, busIdx);
            busBuffer.clear();
            
            AudioSourceChannelInfo bufferToFill (&busBuffer, 0, busBuffer.getNumSamples());
            if (ToneGeneratorAudioSource* source = tones[busIdx])
                source->getNextAudioBlock (bufferToFill);
        }
    }
    
private:
    static BusesProperties getBusProperties()
    {
        BusesProperties retval;
        
        for (int i = 0; i < numOutStems; ++i)
            retval.addBus (false, "Output #" + String (i + 1), AudioChannelSet::canonicalChannelSet (channelsPerStem));
        
        return retval;
    }
    
    OwnedArray<ToneGeneratorAudioSource> tones;
};


//==============================================================================
class GraphMixerApplication  : public JUCEApplication
{
public:
    //==============================================================================
    GraphMixerApplication()
    {
        graph.setPlayConfigDetails (0, 2, 44100., 512);
        
        AudioProcessorGraph::Node* outNode =
        graph.addNode (new AudioProcessorGraph::AudioGraphIOProcessor (AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode));
        
        AudioProcessorGraph::Node* sourceNode =
        graph.addNode (new MultiToneAudioProcessor());
        
        for (int busIdx = 0; busIdx < MultiToneAudioProcessor::numOutStems; ++busIdx)
            for (int channelIdx = 0; channelIdx < MultiToneAudioProcessor::channelsPerStem; ++channelIdx)
                graph.addConnection (sourceNode->nodeId, (busIdx * MultiToneAudioProcessor::channelsPerStem) + channelIdx,
                                     outNode->nodeId, channelIdx);
        
        player.setProcessor (&graph);
        dm.initialiseWithDefaultDevices (0, 2);
        dm.addAudioCallback (&player);
    }

    const String getApplicationName() override       { return ProjectInfo::projectName; }
    const String getApplicationVersion() override    { return ProjectInfo::versionString; }
    bool moreThanOneInstanceAllowed() override       { return true; }
    void initialise (const String& commandLine) override  { mainWindow = new MainWindow (getApplicationName()); }
    void shutdown() override                         { mainWindow = nullptr; }
    void systemRequestedQuit() override              { quit(); }
    void anotherInstanceStarted (const String& /*commandLine*/) override {}

    //==============================================================================
    class MainWindow    : public DocumentWindow
    {
    private:
        class MainContentComponent : public Component
        {
        public:
            MainContentComponent()
                : helloWorld ("helloWorld", "Hello World!")
            {
                addAndMakeVisible (helloWorld);
                
                helloWorld.setJustificationType (Justification::centred);
                
                setOpaque(true);
                setSize (320, 240);
            }
            
            void paint (Graphics& g) override
            {
                g.fillAll (Colours::lightgrey);
            }
            
            void resized() override
            {
                helloWorld.setBounds (getLocalBounds());
            }
        private:
            Label helloWorld;
        };
    public:
        MainWindow (String name)  : DocumentWindow (name,
                                                    Colours::lightgrey,
                                                    DocumentWindow::allButtons)
        {
            setUsingNativeTitleBar (true);
            setContentOwned (new MainContentComponent(), true);

            centreWithSize (getWidth(), getHeight());
            setVisible (true);
        }

        void closeButtonPressed() override { JUCEApplication::getInstance()->systemRequestedQuit(); }

    private:
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
    };

private:
    ScopedPointer<MainWindow> mainWindow;
    
    AudioProcessorGraph graph;
    AudioProcessorPlayer player;
    AudioDeviceManager dm;
};

//==============================================================================
START_JUCE_APPLICATION (GraphMixerApplication)

#3

Hi Fabian,

thanks for your thoughts, that was what I hoped for (can’t expect anything else with that vague report :wink: ).
The strange thing is, that the project is actually older, it was started using 4.0.3 between january and april 2016. So it isn’t aware of any multibus definition. It simply uses the channels. Also the processor is not used from outside, it only feeds via the AudioProcessorPlayer the AudioDeviceIOCallback directly. It serves as a player for a neod-electron app, so it doesn’'t instanciate any JUCEApplication.

The problem is only, that the behaviour definitly changes after that commit. I verified that multiple times by git checkout and did a build-clean inbetween. It seems we created a situation the system before silently coped and did what we expected, but now it behaves differently. A typical side-effect.

Now I checked the stems, and it’s not the first but the last stem that I hear. It seems that the addConnection removes the previous connection. So eventually the themeProcNode provides only 2 input channels? I will dig into that direction.

I keep you updated, if you have any further ideas what might have gone wrong, let me know.

Thanks,
Daniel

Edit: I just foud out debugging, that addConnection failed during canConnect(). I added

AudioSourceProcessor* proc = new AudioSourceProcessor(src);
proc->setChannelLayoutOfBus (false, 0, AudioChannelSet::stereo());
AudioProcessorGraph::Node::Ptr srcNode = this->addNode(proc);

which makes addConnection to work, but now I don’t hear anything (which is eventually good, because I think what I heard before was an artefact…)
I didn’t start that project, so I wasn’t aware that in each node there is an AudioProcessor, I think I’ll have to adapt all of them…