AudioBuffer channel pointers can be identical in Pro Tools

I encountered a very weird behaviour in Pro Tools with a multi out AAX plug-in.

In the processBlock() callback, buffer.getWritePointer() could return identical addresses for channels (other than in the main bus) that are not currently used (typically routed to an Aux Input track).

If these extra channels are actually being used, their address is unique as expected:

I can’t tell if this is how it’s supposed to work but, if it is, I think this lacks proper documentation, unless there’s a way to check if a bus is used before attempting to rely on its pointers? Note that buffer.getNumChannels() always returns 16 here so this won’t tell anything.

This only happens with AAX in Pro Tools. I literally spent a day checking other DAWs and formats and the returned pointers are always valid wether or not the buses are used.

Here’s a PIP to reproduce the issue.

/*******************************************************************************
 
 BEGIN_JUCE_PIP_METADATA

 name:                  AAXMultiOutTest
 version:               1.0.0

 dependencies:          juce_audio_basics, juce_audio_devices,  juce_audio_plugin_client, juce_audio_processors,
                        juce_audio_utils, juce_audio_formats, juce_core, juce_data_structures,
                        juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
 exporters:             xcode_mac, vs2022

 moduleFlags:           JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:                  AudioProcessor
 mainClass:             AAXMultiOutTestProcessor

 useLocalCopy:          1

 pluginCharacteristics: pluginIsSynth
 extraPluginFormats:    AAX

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

#include <JuceHeader.h>

class AAXMultiOutTestProcessor : public juce::AudioProcessor
{
public:
    //==============================================================================
    AAXMultiOutTestProcessor() : juce::AudioProcessor (BusesProperties()
                                                       .withOutput ("Output #1", juce::AudioChannelSet::stereo(), true)
                                                       .withOutput ("Output #2", juce::AudioChannelSet::stereo(), true)
                                                       .withOutput ("Output #3", juce::AudioChannelSet::stereo(), true)
                                                       .withOutput ("Output #4", juce::AudioChannelSet::stereo(), true)
                                                       .withOutput ("Output #5", juce::AudioChannelSet::stereo(), true)
                                                       .withOutput ("Output #6", juce::AudioChannelSet::stereo(), true)
                                                       .withOutput ("Output #7", juce::AudioChannelSet::stereo(), true)
                                                       .withOutput ("Output #8", juce::AudioChannelSet::stereo(), true))
    {
        
    }
    
    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        const auto& outputs = layouts.outputBuses;

        return layouts.inputBuses.isEmpty()
            && 1 <= outputs.size()
            && std::all_of (outputs.begin(), outputs.end(), [] (const auto& bus)
               {
                   return bus.isDisabled() || bus == juce::AudioChannelSet::stereo();
               });
    }

    void processBlock (juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) override
    {
        juce::ScopedNoDenormals noDenormals;
        buffer.clear();
        
        float* prevWritePointer = nullptr;
        
        for (int i = 0; i < buffer.getNumChannels(); ++i)
        {
            float* currWritePointer = buffer.getWritePointer (i);
            jassert (currWritePointer != prevWritePointer); // uh oh... At least two channel write pointers are identical!
            prevWritePointer = currWritePointer;
        }
    }
    
    void prepareToPlay (double sampleRate, int samplesPerBlock) override      { }
    void releaseResources() override                                          { }
    juce::AudioProcessorEditor* createEditor() override                       { return new juce::GenericAudioProcessorEditor (*this); }
    bool hasEditor() const override                                           { return true; }
    const juce::String getName() const override                               { return "AAX Multi Out Test"; }
    bool acceptsMidi() const override                                         { return true; }
    bool producesMidi() const override                                        { return false; }
    bool isMidiEffect() const override                                        { return false; }
    double getTailLengthSeconds() const override                              { return 0; }
    int getNumPrograms() override                                             { return 1; }
    int getCurrentProgram() override                                          { return 0; }
    void setCurrentProgram (int index) override                               { }
    const juce::String getProgramName (int index) override                    { return "None"; }
    void changeProgramName (int index, const juce::String& newName) override  { }
    void getStateInformation (juce::MemoryBlock& destData) override           { }
    void setStateInformation (const void* data, int sizeInBytes) override     { }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AAXMultiOutTestProcessor)
};

Thanks for your feedback.

Forgot to mention I’m using:

  • Latest Pro Tools (2023.3)
  • Latest AAX SDK (2.5.0)
  • Latest tip on JUCE develop

@t0m, @reuk would you have the opportunity to shed some light on this, please? Thanks in advance.

Thanks for your answer. I also had the intuition that these are optimisation mechanisms, that’s one of the reason I’m not talking about a real “issue”. However, since this behaviour is so specific, as you confirmed, I think it should be documented.

I actually encountered it because I was using auxiliary output buffers as reliable independent work buffers and then re-using them as source for further summing on the main output. Not a problem anymore, now that I know this won’t work with Pro Tools, but I suppose some short warning for the concerned methods in the AudioBuffer documentation wouldn’t hurt.

EDIT: this post was flagged as spam… what’s going on?

Can you cite any sources on this? This reads like a ChatGPT answer; it sounds very authoritative but gives no examples, docs, or links.

EDIT: Oh, looking at your post history it seems like you’re some sort of chatgpt bot? What’s the forum policy on that? LLMs are known to hallucinate and give incorrect answers the more detailed or niche a topic, and JUCE feels right on the border of that. Seems like at very least we should require some sort of disclaimer or “bot” in the user name or something?

3 Likes

Damned… thanks @jemmons! Waiting for the JUCE team to chime in.

We don’t want any ChatGPT answers posted on the forum. If our users want that input they can solicit it themselves.

There is nothing in the plug-in format standards that specifies how auxiliary buffers are used, so you should not make any assumptions about them.

3 Likes

Thanks @t0m.

That’s a relief, IMHO. I still don’t understand why my post was flagged, though.

Lesson learnt, although I think even this simple line of yours would have make things a lot easier if it’d been in the documentation, which generally contains such warnings where appropriate. Anyway, thanks for clarifying.

It looks like someone was attempting to flag the overall thread as containing spam, and accidentally tagged your post.

Got it. No harm done. Thanks again.