How do I properly mix two AVAudioSourceNode objects (Signal Generators) into stereo output?

Viewed 19

I'm attempting to take Apple's Building a Signal Generator sample code and augment it so that I can play a different frequency in my left ear of the stereo channel from my right ear in the stereo channel. Unfortunately the result of my attempt is significantly distorted noise rather than the two distinguishable tones.

I have also attempted at creating two AVAudioSourceNodes with the same frequency and linear signal to test against my perception, but it still seemed distorted.

At one point I wrapped the audio engine connection and starting in a DispatchQueue.global(qos: .background).async { } but that also didn't seem to matter. It's still present in the code.

Here is the code:

import SwiftUI
import AVFoundation

let frequency: Float = 432
let amplitude: Float = 1

let twoPi = 2 * Float.pi

let linear = { (phase: Float) -> Float in
    return phase
}

//let sine = { (phase: Float) -> Float in
//    return sin(phase)
//}
//
//let whiteNoise = { (phase: Float) -> Float in
//    return ((Float(arc4random_uniform(UINT32_MAX)) / Float(UINT32_MAX)) * 2 - 1)
//}
//
//let sawtoothUp = { (phase: Float) -> Float in
//    return 1.0 - 2.0 * (phase * (1.0 / twoPi))
//}
//
//let sawtoothDown = { (phase: Float) -> Float in
//    return (2.0 * (phase * (1.0 / twoPi))) - 1.0
//}
//
//let square = { (phase: Float) -> Float in
//    if phase <= Float.pi {
//        return 1.0
//    } else {
//        return -1.0
//    }
//}
//
//let triangle = { (phase: Float) -> Float in
//    var value = (2.0 * (phase * (1.0 / twoPi))) - 1.0
//    if value < 0.0 {
//        value = -value
//    }
//    return 2.0 * (value - 0.5)
//}

typealias Signal = (Float) -> Float

let signal: Signal = linear

let engine = AVAudioEngine()
let mainMixer = engine.mainMixerNode
let output = engine.outputNode
let outputFormat = output.inputFormat(forBus: 0)
let sampleRate = Float(outputFormat.sampleRate)
// Use the output format for the input, but reduce the channel count to 1.
let inputFormat = AVAudioFormat(commonFormat: outputFormat.commonFormat,
                                sampleRate: outputFormat.sampleRate,
                                channels: 1,
                                interleaved: outputFormat.isInterleaved)

var currentPhase: Float = 0
// The interval to advance the phase each frame.
let phaseIncrement = (twoPi / sampleRate) * frequency

let leftSrcNode = AVAudioSourceNode { _, _, frameCount, audioBufferList -> OSStatus in
    let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList)
    for frame in 0..<Int(frameCount) {
        // Get the signal value for this frame at time.
        let value = signal(currentPhase) * amplitude
        // Advance the phase for the next frame.
        currentPhase += phaseIncrement
        if currentPhase >= twoPi {
            currentPhase -= twoPi
        }
        if currentPhase < 0.0 {
            currentPhase += twoPi
        }
        // Set the same value on all channels (due to the inputFormat, there's only one channel though).
        for buffer in ablPointer {
            let buf: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer)
            buf[frame] = value
        }
    }
    return noErr
}

let rightSrcNode = AVAudioSourceNode { _, _, frameCount, audioBufferList -> OSStatus in
    let ablPointer = UnsafeMutableAudioBufferListPointer(audioBufferList)
    for frame in 0..<Int(frameCount) {
        // Get the signal value for this frame at time.
        let value = signal(currentPhase) * amplitude
        // Advance the phase for the next frame.
        currentPhase += phaseIncrement
        if currentPhase >= twoPi {
            currentPhase -= twoPi
        }
        if currentPhase < 0.0 {
            currentPhase += twoPi
        }
        // Set the same value on all channels (due to the inputFormat, there's only one channel though).
        for buffer in ablPointer {
            let buf: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer)
            buf[frame] = value
        }
    }
    return noErr
}


@main
struct binauralApp: App {

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
    
    init() {
        DispatchQueue.global(qos: .background).async {
            leftSrcNode.pan = -1.0
            engine.attach(leftSrcNode)
            engine.connect(leftSrcNode, to: mainMixer, format: inputFormat)
            
            rightSrcNode.pan = 1.0
            engine.attach(rightSrcNode)
            engine.connect(rightSrcNode, to: mainMixer, format: inputFormat)
            
            engine.connect(mainMixer, to: output, format: outputFormat)
            mainMixer.outputVolume = 0.5
            do {
               try engine.start()
            } catch {
               print("Could not start engine: \(error.localizedDescription)")
            }
        }
    }
}
1 Answers

This was a silly mistake in not fully understanding the signal generator sample code.

The solution was to decouple the two variables that were being mutated simultaneously between the two render blocks currentPhase and phaseIncrement.

Related