Why isn't my multichannel mapping working correctly?
Asked Answered
V

1

7

I recently posted this question about using multiroute with iOS and I thought I solved it, however I've discovered is doesn't quite work: AVAudioEngine Multichannel mapping

The issue I'm having is the multiroute is only working for the first two output channels. I'm trying to make it work for a 4 channel audio interface.

I have managed to route audio to each output of the USB interface using AVAudioPlayer:

var avplayer = AVAudioPlayer()

@IBAction func avAudioPlayerPlay(_ sender: Any)
{
    let audioSession = AVAudioSession.sharedInstance()
    let route = audioSession.currentRoute

    // set the session category
    do
    {
        //try audioSession.setCategory(.multiRoute)
        try audioSession.setCategory(.multiRoute, options: .mixWithOthers)
    }
    catch
    {
        print("unable to set category", error)
        return
    }

    // activate the audio session - turns on multiroute I believe
    do
    {
        try audioSession.setActive(true)
        //try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
    }
    catch
    {
        print("unable to set active", error)
        return
    }

    //audio interface + headphone jack
    let outputs:[AVAudioSessionChannelDescription] = [
        route.outputs[0].channels![2], // 3rd channel on Audio Interface
        route.outputs[1].channels![1]  // Right Channel of Headphones
    ]

    guard let filePath: String = Bundle.main.path(forResource: "audio", ofType: "m4a") else { return }
    let fileURL: URL = URL(fileURLWithPath: filePath)

    do
    {
        avplayer = try AVAudioPlayer(contentsOf: fileURL)
    }
    catch
    {
        print("play error", error)
        return
    }

    avplayer.channelAssignments = outputs

    let result = avplayer.play()
    print(result)
}

But I can't get it to work using AVAudioEngine:

private func getOutputChannelMapIndices(_ names:[String?]) -> [Int]
{
    let session = AVAudioSession.sharedInstance()
    let route = session.currentRoute
    let outputPorts = route.outputs

    var channelMapIndices:[Int] = []

    for name in names
    {
        var chIndex = 0
        for outputPort in outputPorts
        {
            guard let channels = outputPort.channels else
            {
                continue
            }
            for channel in channels
            {
                print(channel.channelName)
                if channel.channelName == name
                {
                    if names.count > channelMapIndices.count
                    {
                        channelMapIndices.append(chIndex)
                    }
                }
                chIndex += 1
            }
        }
    }
    return channelMapIndices
}

@IBAction func nodesPlay(_ sender: Any)
{
    let channelNames = [
        "UMC204HD 192k 3",
        "Headphones Left",
        "Headphones Right",
        nil
    ]

    let audioSession = AVAudioSession.sharedInstance()

    // set the session category
    do
    {
        //try audioSession.setCategory(.multiRoute)
        try audioSession.setCategory(.multiRoute, options: .mixWithOthers)
    }
    catch
    {
        print("unable to set category", error)
        return
    }

    // activate the audio session - turns on multiroute I believe
    do
    {
        try audioSession.setActive(true)
        //try audioSession.setActive(true, options: .notifyOthersOnDeactivation)
    }
    catch
    {
        print("unable to set active", error)
        return
    }

    let channelMapIndices = getOutputChannelMapIndices(channelNames)

    print("channelMapIndices: ", channelMapIndices)

    engine = AVAudioEngine()
    output = engine.outputNode
    mixer = engine.mainMixerNode

    player = AVAudioPlayerNode()

    engine.attach(player)

    guard let filePath: String = Bundle.main.path(forResource: "audio", ofType: "m4a") else { return }
    let fileURL: URL = URL(fileURLWithPath: filePath)
    let file = try! AVAudioFile(forReading: fileURL)

    let outputNumChannels = output.outputFormat(forBus: 0).channelCount
    print("outputNumChannels:" , outputNumChannels)

    var outputChannelMap:[Int] = Array(repeating: -1, count: Int(outputNumChannels))

    let numberOfSourceChannels = file.processingFormat.channelCount
    print("numberOfSourceChannels: ", numberOfSourceChannels)

    var sourceChIndex = 0
    for chIndex in channelMapIndices
    {
        if chIndex < outputNumChannels && sourceChIndex < numberOfSourceChannels
        {
            outputChannelMap[chIndex] = sourceChIndex
            sourceChIndex += 1
        }
    }

    print("outputChannelMap: ", outputChannelMap)

    if let au = output.audioUnit
    {
        let propSize = UInt32(MemoryLayout.size(ofValue: outputChannelMap))
        print("propSize:", propSize)
        let result = AudioUnitSetProperty(au, kAudioOutputUnitProperty_ChannelMap, kAudioUnitScope_Global, 0, &outputChannelMap, propSize)
        print("result: ", result)
    }

    let channelLayout = AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_DiscreteInOrder | UInt32(numberOfSourceChannels))
    let format = AVAudioFormat(streamDescription: file.processingFormat.streamDescription, channelLayout: channelLayout)

    engine.connect(player, to: mixer, format:format)
    engine.connect(mixer, to: output, format:format)

    player.scheduleFile(file, at: nil, completionHandler: nil)

    do
    {
        try engine.start()
    }
    catch
    {
        print("can't start", error)
        return
    }

    player.play()
}

If anyone could explain why I can't seem to play any audio to output 3 or 4 I would really appreciate it.

Note, a lot of this code was translated from here: https://forums.developer.apple.com/thread/15416

Vernation answered 6/6, 2020 at 11:25 Comment(3)
What is the value of kAudioOutputUnitProperty_ChannelMap before you set it? How many channels are in kAudioUnitProperty_StreamFormat on the output unit?Crevice
kAudioOutputUnitProperty_ChannelMap appears to be blank before I set it. If I get it after I set it then it appears that only the first value of the array is set. There appears to be 4 channels on the output.Vernation
I’ve discovered if I multiply propSize by 4 then getting the channelmap returns the correct result... but the output still isn’t correctVernation
C
5

I believe the problem is the line

let propSize = UInt32(MemoryLayout.size(ofValue: outputChannelMap))

This is giving you the size of the array object, which is essentially the size of a pointer, not the size of the objects in the array. See the discussion in the Apple docs.

The size of the property should be the number of channels contained in the array multiplied by the size of Int32, since AudioUnitSetProperty is a C API and that would be the size of a corresponding C array.

let propSize = UInt32(MemoryLayout<Int32>.stride * outputChannelMap.count)

You should also declare outputChannelMap as an array of Int32 since that is the type expected by kAudioOutputUnitProperty_ChannelMap:

var outputChannelMap:[Int32] = Array(repeating: -1, count: Int(outputNumChannels))
Crevice answered 8/6, 2020 at 20:35 Comment(7)
Yep. This is the output: 0: -1, 1: -1. If I multiply propSize by two I get this: 0: -1, 1: -1, 2: -1, 3: -1.Vernation
Also.. this is what outputChannelMap looks like: [-1, -1, 0, -1]Vernation
If I don't multiply by two I only get two array values.Vernation
I think you may have pointed me in the right direction.. I've just changed outputChannelMap to an array of Int32 values and its outputing the correct mapping nowVernation
I've just tried it with a second usb interface and this seems to work for the propSize: let propSize:UInt32 = UInt32(MemoryLayout.size(ofValue: outputChannelMap)) * (outputNumChannels / numberOfSourceChannels) - Does this make any sense?Vernation
I meant the propSize for setting the output map. Sorry about the confusion. I've changed it now to this which I think is actually correct: let propSize:UInt32 = UInt32(MemoryLayout<Int32>.stride * outputChannelMap.count)Vernation
Let us continue this discussion in chat.Vernation

© 2022 - 2024 — McMap. All rights reserved.