Skip to content

Instantly share code, notes, and snippets.

@ABridoux
Last active November 18, 2023 18:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ABridoux/fa31bb912cc5504e51f50a5b8a6d0fa0 to your computer and use it in GitHub Desktop.
Save ABridoux/fa31bb912cc5504e51f50a5b8a6d0fa0 to your computer and use it in GitHub Desktop.
Wrapper around an `AVAudioUnit` of type `kAudioUnitType_Mixer` and sub type `kAudioUnitSubType_MatrixMixer` for convenient usage.
import AVFoundation
// MARK: - MatrixMixerNodeWrapper
/// Wrapper around an audio unit node of type `kAudioUnitSubType_MatrixMixer` for convenience.
public final class MatrixMixerNodeWrapper {
// MARK: Properties
/// Matrix mixer to map input channels
///
/// Helpful links about `kAudioUnitSubType_MatrixMixer` usage:
/// - https://lists.apple.com/archives/coreaudio-api/2008/Apr/msg00169.html
/// - https://stackoverflow.com/questions/48059405/how-should-an-aumatrixmixer-be-configured-in-an-avaudioengine-graph
/// - https://stackoverflow.com/questions/53208006/change-audio-volume-of-some-channels-using-avaudioengine
private let matrixMixerNode: AVAudioUnit
/// Volume of the node output.
public var volume: Float = 1 {
didSet { updateOutputVolume() }
}
// MARK: Init
public init() async throws {
let matrixUnitDescription = AudioComponentDescription(
componentType: kAudioUnitType_Mixer,
componentSubType: kAudioUnitSubType_MatrixMixer,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0
)
matrixMixerNode = try await AVAudioUnit.instantiate(with: matrixUnitDescription)
}
}
// MARK: - Node
extension MatrixMixerNodeWrapper {
/// Wrapped node.
public var node: AVAudioNode { matrixMixerNode }
}
// MARK: - Setup
extension MatrixMixerNodeWrapper {
/// Setup the matrix mixer to pass audio.
///
/// - important: Has to be called after the `AVAudioEngine` has been started.
public func setup() {
AudioUnitSetParameter(matrixMixerNode.audioUnit, kMatrixMixerParam_Volume, kAudioUnitScope_Global, 0xFFFF_FFFF, 1, 0)
for inputChannelIndex in 0..<matrixMixerNode.inputFormat(forBus: 0).channelCount {
AudioUnitSetParameter(
matrixMixerNode.audioUnit,
kMatrixMixerParam_Volume,
kAudioUnitScope_Input,
inputChannelIndex,
1,
0
)
}
updateOutputVolume()
}
private func updateOutputVolume() {
for outputChannelIndex in 0..<matrixMixerNode.outputFormat(forBus: 0).channelCount {
AudioUnitSetParameter(
matrixMixerNode.audioUnit,
kMatrixMixerParam_Volume,
kAudioUnitScope_Output,
outputChannelIndex,
volume,
0
)
}
}
}
// MARK: - Mapping
extension MatrixMixerNodeWrapper {
/// Set the volume for the input channel to the output channels in the `outputChannelIndexes` set.
public func setInputVolume(
_ volume: Float,
forInputChannel inputChannelIndex: Int,
toOutputChannels outputChannelIndexes: Set<Int>
) {
var outputChannelIndex = 0
while outputChannelIndex < matrixMixerNode.outputFormat(forBus: 0).channelCount {
let volume = outputChannelIndexes.contains(outputChannelIndex) ? volume : 0
let crossPoint = UInt32((inputChannelIndex << 16) | outputChannelIndex)
AudioUnitSetParameter(
matrixMixerNode.audioUnit,
kMatrixMixerParam_Volume,
kAudioUnitScope_Global,
crossPoint,
volume,
0
)
outputChannelIndex += 1
}
}
}
@ABridoux
Copy link
Author

ABridoux commented Sep 16, 2023

Notes

Usage

Setup

// instantiate the wrapper
let matrixMixerNodeWrapper = try await MatrixMixerNodeWrapper()

// connect the node the engine
engine.attach(matrixMixerNodeWrapper.node)
engine.connect(playerNode, to : matrixMixerNodeWrapper.node, format: nil)

// then each time the engine is started, call:
matrixMixerNodeWrapper.node.setup()

Note

Calling setup every time the engine is started is not mandatory, but I find it easier to manage and couldn't find any drawbacks.

Update volume and channel connexions

Set node output volume:

matrixMixerNodeWrapper.volume = 0.5

Set volume for one connexion:

matrixMixerNodeWrapper.setInputVolume(1, forInputChannel: 0, to: [0, 1])

Important

When no volume is set for a connexion, its default value is 0. Also, it's better to set the volume for each connexion each time the engine is started.

Connexions examples

Make a stereo signal mono

matrixMixerNodeWrapper.setInputVolume(1, forInputChannel: 0, to: [0])
matrixMixerNodeWrapper.setInputVolume(1, forInputChannel: 1, to: [0])

Invert left and right channels

matrixMixerNodeWrapper.setInputVolume(1, forInputChannel: 0, to: [1])
matrixMixerNodeWrapper.setInputVolume(1, forInputChannel: 1, to: [0])

Links

Here are some links that were useful to make that work:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment