AVAudioEngine and Multiroute

This thread has been locked by a moderator.

Some developers have asked how to setup multiroute channel mapping with AVAudioEngine when using multichannel output hardware.


Currently, to get multi routing working with AVAudioEngine the application needs to create and set a custom channel map on the outputNode Audio Unit via the C API.


We've provided some sample code below demonstrating how an application can accomplish this task. Doing so allows you to route specific channels to various destinations.

For more information see WWDC 2012 Session 505 Audio Session and Multiroute Audio in iOS.


For example, a standard channel map would look like this:

AURemoteIO Client Format 2 Channels: 0, 1
                                     |  |
Output Audio Unit Channel Map      : 0, 1, -1, -1
                                     |  |
AURemoteIO Output Format 4 Channels: 0, 1,  2,  3
                                    L  R HDMI1 HDMI2


A custom channel map routing audio to HDMI 1 & HDMI 2 would look like this:

AURemoteIO Client Format 2 Channels: 0, 1
                                     |  |_______
                                     |_______   |
                                             |  |
Output Audio Unit Channel Map      : -1, -1, 0, 1
                                             |  |
AURemoteIO Output Format 4 Channels: 0,  1,  2, 3
                                    L  R  HDMI1 HDMI2


Sample Custom Channel Map example code:


//  configureAVAudioEngine() assume the inChannelNames array contains user
//  specified channel names used by getOutputChannelMapIndices() to create
//  the channel map index.
//
// Example:
//
// NSArray *inChannelNames = [NSArray arrayWithObjects:
//                              @"Headphones Left",
//                              @"Headphones Right",
//                              @"HDMI 2",
//                              nil];

NSMutableArray* getOutputChannelMapIndices(NSArray *inChannelNames)
{
    AVAudioSession *session = [AVAudioSession sharedInstance];
    AVAudioSessionRouteDescription *route = [session currentRoute];
    NSArray *outputPorts = [route outputs];

    NSMutableArray *channelMapIndices = [NSMutableArray array];
    int chIndex = 0;

    for (NSString *inChName in inChannelNames) {
        chIndex = 0;
        for (AVAudioSessionPortDescription *outputPort in outputPorts) {
            for (AVAudioSessionChannelDescription *channel in outputPort.channels) {
                if ([channel.channelName isEqualToString:inChName]) {
                    if ([inChannelNames count] > [channelMapIndices count]) {
                        [channelMapIndices addObject:[NSNumber numberWithInt:chIndex]];
                    }
                }
                chIndex++;
            }
        }
    }

    return channelMapIndices;
}

- (void)configureAVAudioEngine(NSArray *inChannelNames)
{
    // ------------------------------------------------------------------
    // AVAudioSession setup
    // ------------------------------------------------------------------

    AVAudioSession *sessionInstance = [AVAudioSession sharedInstance];
    NSError *error = nil;

    // set the session category
    bool success = [sessionInstance setCategory:AVAudioSessionCategoryMultiRoute withOptions:AVAudioSessionCategoryOptionMixWithOthers error:&error];
    NSAssert(success, @"Error setting AVAudioSession category! %@", [error localizedDescription]);

    // activate the audio session
    success = [sessionInstance setActive:YES error:&error];
    NSAssert(success, @"Error setting AVAudioSession active! %@", [error localizedDescription]);

    // Get channel map indices based on user specified channelNames
    NSMutableArray *channelMapIndices = getOutputChannelMapIndices(inChannelNames);
    NSAssert(channelMapIndices && channelMapIndices.count > 0, @"Error getting indices for user specified channel names!");

    // ------------------------------------------------------------------
    // AVAudioEngine setup
    // ------------------------------------------------------------------

    _engine = [[AVAudioEngine alloc] init];
    _output = _engine.outputNode;
    _mixer = _engine.mainMixerNode;

    _player = [[AVAudioPlayerNode alloc] init];
    [_engine attachNode:_player];

    // open the file to play
    AVAudioFile *file = [self getFileToPlay:@"lpcm_multichannel ofType:@"caf"];  // multichannel audio file

    // create output channel map
    UInt32 outputNumChannels = [_output outputFormatForBus:0].channelCount;
    NSAssert(outputNumChannels > 0 && outputNumChannels <= 512, @"Error: invalid number of output channels!"); // reasonable bounds

    SInt32 outputChannelMap[outputNumChannels];
    memset(outputChannelMap, -1, sizeof(outputChannelMap)); // unmapped

    SInt32 sourceNumChannels = (SInt32)file.processingFormat.channelCount;
    SInt32 sourceChIndex = 0;
    for (id chIndex in channelMapIndices) {
        int chIndexVal = [(NSNumber*)chIndex intValue];

        if (chIndexVal < outputNumChannels && sourceChIndex < sourceNumChannels) {
            outputChannelMap[chIndexVal] = sourceChIndex;
            sourceChIndex++;
        }
    }

    // set channel map on outputNode AU
    UInt32 propSize = (UInt32)sizeof(outputChannelMap);
    OSStatus err = AudioUnitSetProperty(_output.audioUnit, kAudioOutputUnitProperty_ChannelMap, kAudioUnitScope_Global, 0, outputChannelMap, propSize);
    NSAssert(noErr == err, @"Error setting channel map! %d", (int)err);

    // make connections
    AVAudioChannelLayout *channelLayout = [[AVAudioChannelLayout alloc]
                                            initWithLayoutTag:kAudioChannelLayoutTag_DiscreteInOrder | (UInt32)sourceNumChannels];
    AVAudioFormat *format = [[AVAudioFormat alloc]
                              initWithStreamDescription:file.processingFormat.streamDescription
                              channelLayout:channelLayout];

    [_engine connect:_player to:_mixer format:format];
    [_engine connect:_mixer to:_output format:format];

    // schedule the file on player
    [_player scheduleFile:file atTime:nil completionHandler:nil];

    // start engine and player
    success = [_engine startAndReturnError:&error];
    NSAssert(success, @"Error starting engine! %@", [error localizedDescription]);

    [_player play];
}
Up vote post of theanalogkid
6.3k views