How should an AUMatrixMixer be configured in an AVAudioEngine graph?

2

I'm completely stuck on this and would appreciate any assistance...

I have an AUMatrixMixer implemented in an AVAudioEngine graph and I cannot get any sound. If I swap out the AUMatrixMixer for an AUMultiChannelMixer I can get sound. I have installed a tap on the immediate upstream node (AUHighPassFilter) and I can see audio being pulled by the AUMatrixMixer. If I move the tap to the output of the AUMatrixMixer I can see data being pulled from the next downstream node - the AVAudioEngine main mixer node - but it is all silence...

There is not a lot of commentary out there about AUMatrixMixers so it could be some magic setting that I don't know about. As reference sources, I have an email from Apple Tech Support with this key observation:

"...To use a Matrix-Mixer within your AVAudioEngine setup, you will need to create an AVAudioUnit using the +instantiateWithComponentDescription:options:completionHandler: API on AVAudioUnit found here:

Since an AVAudioUnit is a subclass of AVAudioNode, you can then use your AVAudioUnit that leverages the Matrix-Mixer in your AVAudioEngine setup. You would be able to have a setup similar to the following:

AVAudioPlayerNode -> AVAudioUnit (with is a Matrix Mixer Audio unit configured to split your channel) -> Main Mixer (which you would configure for multi-route channel mapping) -> Output..."

So it should work. I have also analysed the example code provided by Apple:

(MatrixMixerTest https://developer.apple.com/library/mac/samplecode/MatrixMixerTest/Introduction/Intro.html) - this is not quite what I am trying to do but I cannot see anything different in the use of the API's etc.

The code for the creation of the AUMatrixMixer (one input bus, two output busses):

private func setupMatrixMixer() {
    AVAudioUnit.instantiate(with: matrixMixerDescr, options: [.loadOutOfProcess], completionHandler: {(audioUnit, auError) in
        if let au = audioUnit {
            self.matrixMixer = au
            var error:OSStatus = noErr
            var numInputBuses:UInt32 = 1
            var numOutputBuses:UInt32 = 2
            // Input bus config
            error = AudioUnitSetProperty(self.matrixMixer.audioUnit,
                                    AudioUnitPropertyID(kAudioUnitProperty_ElementCount),
                                    AudioUnitScope(kAudioUnitScope_Input),
                                    0,
                                    &numInputBuses,
                                    UInt32(MemoryLayout<UInt32>.size))
            if error != noErr {
                assert(true, "ERROR: Setting matrix mixer number of input buses")
                return
            }
            // Output bus config
            error = AudioUnitSetProperty(self.matrixMixer.audioUnit,
                                         AudioUnitPropertyID(kAudioUnitProperty_ElementCount),
                                         AudioUnitScope(kAudioUnitScope_Output),
                                         0,
                                         &numOutputBuses,
                                         UInt32(MemoryLayout<UInt32>.size))
            if error != noErr {
                assert(true, "ERROR: Setting matrix mixer number of output buses")
                return
            }
        }
        else { trace(level: .skim, items: "ERROR: failed to create matrix mixer. Error code: \(String(describing: auError))")}
    } )
}

The graph creation:

private func makeEngineConnections() {
    // Get the engine's optional singleton main mixer node
    let output = engine.mainMixerNode
    // Connect nodes
    engine.connect(player, to: timePitch, fromBus: 0, toBus: 0, format: audioFormat)
    engine.connect(timePitch, to: lowPassFilter, fromBus: 0, toBus: 0, format: audioFormat)
    engine.connect(lowPassFilter, to: highPassFilter, fromBus: 0, toBus: 0, format: audioFormat)
    engine.connect(highPassFilter, to: matrixMixer, fromBus: 0, toBus: 0, format: audioFormat)
    engine.connect(matrixMixer, to: output, fromBus: 0, toBus: 0, format: audioFormat)
    engine.connect(matrixMixer, to: output, fromBus: 1, toBus: 1, format: audioFormat)
}

A dump of the engine graph after setup:

________ GraphDescription ________ AVAudioEngineGraph 0x1701c6450: initialized = 1, running = 1, number of nodes = 8

 ******** output chain ********

 node 0x1700a9960 {'auou' 'rioc' 'appl'}, 'I'
     inputs = 1
         (bus0) <- (bus0) 0x1740ee180, {'aumx' 'mcmx' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]

 node 0x1740ee180 {'aumx' 'mcmx' 'appl'}, 'I'
     inputs = 2
         (bus0) <- (bus0) 0x1700f0200, {'aumx' 'mxmx' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]
         (bus1) <- (bus1) 0x1700f0200, {'aumx' 'mxmx' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]
     outputs = 1
         (bus0) -> (bus0) 0x1700a9960, {'auou' 'rioc' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]

 node 0x1700f0200 {'aumx' 'mxmx' 'appl'}, 'I'
     inputs = 1
         (bus0) <- (bus0) 0x1740ee100, {'aufx' 'hpas' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]
     outputs = 2
         (bus0) -> (bus0) 0x1740ee180, {'aumx' 'mcmx' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]
         (bus1) -> (bus1) 0x1740ee180, {'aumx' 'mcmx' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]

 node 0x1740ee100 {'aufx' 'hpas' 'appl'}, 'I'
     inputs = 1
         (bus0) <- (bus0) 0x1740ee700, {'aufx' 'lpas' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]
     outputs = 1
         (bus0) -> (bus0) 0x1700f0200, {'aumx' 'mxmx' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]

 node 0x1740ee700 {'aufx' 'lpas' 'appl'}, 'I'
     inputs = 1
         (bus0) <- (bus0) 0x1740ee480, {'aufc' 'nutp' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]
     outputs = 1
         (bus0) -> (bus0) 0x1740ee100, {'aufx' 'hpas' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]

 node 0x1740ee480 {'aufc' 'nutp' 'appl'}, 'I'
     inputs = 1
         (bus0) <- (bus0) 0x174198fc0, {'augn' 'sspl' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]
     outputs = 1
         (bus0) -> (bus0) 0x1740ee700, {'aufx' 'lpas' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]

 node 0x174198fc0 {'augn' 'sspl' 'appl'}, 'I'
     outputs = 1
         (bus0) -> (bus0) 0x1740ee480, {'aufc' 'nutp' 'appl'}, [ 2 ch,  44100 Hz, 'lpcm' (0x00000029) 32-bit little-endian float, deinterleaved]

 ******** other nodes ********

 node 0x1700ef000 {'aumx' 'mcmx' 'appl'}, 'U'

The setting of the AUMatrixMixer internals is a tad tedious. In summary:

  • enable the input bus
  • set volume(s) on each input channel (2 of them)
  • enable the output bus (actually, this is redundant as they are both permanently enabled)
  • set volume(s) on each output channel (4 of them)
  • set volumes on each crosspoint (I set everything to 1 (max volume) for now) - 8 of them
  • set the global volume for the mixer (1 setting)

This is a dump of the internal state after doing the above:

Matrix dimensions: [2, 4]
Input element count: 1
Input channel 0 volume: 1.0
Input channel 1 volume: 1.0
Output element count: 2
Output channel 0 volume: 1.0
Output channel 1 volume: 1.0
Output channel 2 volume: 1.0
Output channel 3 volume: 1.0
Crosspoint volumes: [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]
Input 0 enabled parameter: 1.0
Output 0 enabled parameter: 1.0
Output 1 enabled parameter: 1.0

As you can see, everything is enabled and all volumes set to maximum yet no output sound....

As mentioned above, by using a tap on the output of the various nodes, I have verified that good data is moving from the high pass filer to the matrix mixer but it is silence that is moving between the matrix mixer and the main mixer node.

Does anyone know of anything else that I need to do in order to get sound out of this?

Regards, AC

ios
audio
asked on Stack Overflow Jan 2, 2018 by Andrew Coad

1 Answer

2

I had been facing this same situation for weeks now. And just now, I was writing a piece of sample code to ask Apple Code-Level support about it. I tested it just as I was about to send, assuming it wouldn't work as usual, but astonishingly it did work! I think, like AUGraph, there must be some specific order of setting the stream formats, connecting the nodes, etc. that's required for it to work properly. (And, as with AUGraph, the documentation doesn't exactly explain what that order is.) So I'm not really sure what I did differently this time, but at least it works for me now.

So here's a barebones example that successfully uses a matrix mixer with AVAudioEngine:

NSURL *audioURL = /*an audio URL*/
AVAudioFile *file = [[AVAudioFile alloc] initForReading:audioURL error:nil];
AVAudioPlayerNode *audioPlayer = [[AVAudioPlayerNode alloc] init];

_engine = [[AVAudioEngine alloc] init];

[_engine attachNode:audioPlayer];

AudioComponentDescription mixerDesc;
mixerDesc.componentType = kAudioUnitType_Mixer;
mixerDesc.componentSubType = kAudioUnitSubType_MatrixMixer;
mixerDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
mixerDesc.componentFlags = kAudioComponentFlag_SandboxSafe;

[AVAudioUnit instantiateWithComponentDescription:mixerDesc options:kAudioComponentInstantiation_LoadInProcess completionHandler:^(__kindof AVAudioUnit * _Nullable mixerUnit, NSError * _Nullable error) {

    [_engine attachNode:mixerUnit];

    /*Give the mixer one input bus and one output bus*/
    UInt32 inBuses = 1;
    UInt32 outBuses = 1;
    AudioUnitSetProperty(mixerUnit.audioUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &inBuses, sizeof(UInt32));
    AudioUnitSetProperty(mixerUnit.audioUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Output, 0, &outBuses, sizeof(UInt32));

    /*Set the mixer's input format to have 2 channels*/
    UInt32 inputChannels = 2;
    AudioStreamBasicDescription mixerFormatIn;
    UInt32 size;
    AudioUnitGetProperty(mixerUnit.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &mixerFormatIn, &size);
    mixerFormatIn.mChannelsPerFrame = inputChannels;
    AudioUnitSetProperty(mixerUnit.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &mixerFormatIn, size);

    /*Set the mixer's output format to have 2 channels*/
    UInt32 outputChannels = 2;
    AudioStreamBasicDescription mixerFormatOut;
    AudioUnitGetProperty(mixerUnit.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &mixerFormatOut, &size);
    mixerFormatOut.mChannelsPerFrame = outputChannels;

    AudioUnitSetProperty(mixerUnit.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &mixerFormatOut, size);

    /*Connect the nodes*/
    [_engine connect:audioPlayer to:mixerUnit format:nil];
    [_engine connect:mixerUnit to:_engine.outputNode format:nil];

    /*Start the engine*/
    [_engine startAndReturnError:nil];

    /*Play the audio file*/
    [audioPlayer scheduleFile:file atTime:nil completionHandler:nil];
    [audioPlayer play];

    /*Set all matrix volumes to 1*/

    /*Set the master volume*/
    AudioUnitSetParameter(mixerUnit.audioUnit, kMatrixMixerParam_Volume, kAudioUnitScope_Global, 0xFFFFFFFF, 1.0, 0);

    for(UInt32 i = 0; i < inputChannels; i++) {

        /*Set input volumes*/
        AudioUnitSetParameter(mixerUnit.audioUnit, kMatrixMixerParam_Volume, kAudioUnitScope_Input, i, 1.0, 0);

        for(UInt32 j = 0; j < outputChannels; j++) {
            /*Set output volumes (only one outer iteration necessary)*/
            if(i == 0) {
                AudioUnitSetParameter(mixerUnit.audioUnit, kMatrixMixerParam_Volume, kAudioUnitScope_Output, j, 1.0, 0);
            }

            /*Set cross point volumes - 1.0 for corresponding
             inputs/outputs, otherwise 0.0*/
            UInt32 crossPoint = (i << 16) | (j & 0x0000FFFF);
            AudioUnitSetParameter(mixerUnit.audioUnit, kMatrixMixerParam_Volume, kAudioUnitScope_Global, crossPoint, (i == j) ? 1.0 : 0.0, 0);
        }

    }

    /*If you want to verify it's working, try something like this to silence only one channel of audio
    AudioUnitSetParameter(mixerUnit.audioUnit, kMatrixMixerParam_Volume, kAudioUnitScope_Output, 0, 0.0, 0);
    */
}];
answered on Stack Overflow Feb 22, 2018 by Isabel • edited Feb 22, 2018 by Isabel

User contributions licensed under CC BY-SA 3.0