swiftaudiocore-audiosynthesizer

Pitch changes when playing a fixed-frequency sine wave in Apple CoreAudio


thanks for reading my question.

I was using Apple CoreAudio to render a simple sine wave at 440Hz in real time. For the first 128 seconds of rendering, I was able to hear a nice 440Hz sine wave. But after 128 seconds, the pitch of the sound suddenly became low. Then, after 256 seconds of playback, the pitch of the sound became higher than 440Hz. Finally, when the playback time reached 512 seconds, the sound was no longer audible. The code below is what I ran. This code is similar to the code written on the website below. (Japanese though)

Website that I saw

import SwiftUI
import AVFoundation

var player = Player(engine: &box)

@main
struct MyApp: App {
    init() {
        do {
            try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord)
            let ioBufferDuration = 128.0 / 44100.0
            try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(ioBufferDuration)
        } catch {
            assertionFailure("AVAudioSession setup error: \(error)")
        }
    }
    
    var body: some Scene {
        WindowGroup {
            MyView()
        }
    }
}
import Foundation
import AVFoundation

class Player {
    var audioUnit: AudioUnit?
    var audioEngine: AVAudioEngine = AVAudioEngine()
    var sampleRate: Float = 440.0
    var time: Float = 0
    var deltaTime: Float = 0
    var mainMixer: AVAudioMixerNode?
    var outputNode: AVAudioOutputNode?
    var format: AVAudioFormat?
    var engine: Box?

    private lazy var sourceNode = AVAudioSourceNode { (_, _, frameCount, outputData) -> OSStatus in
        let ablPointer = UnsafeMutableAudioBufferListPointer(outputData)
        for frame in 0..<Int(frameCount) {
            let sampleVal = sin(440.0 * 2.0 * Float(Double.pi) * self.time)
            self.time += self.deltaTime
            for buffer in ablPointer {
                let buf: UnsafeMutableBufferPointer<Float> = UnsafeMutableBufferPointer(buffer)
                buf[frame] = sampleVal
            }
        }
        return noErr
    }

    init(engine: inout Box) {
        mainMixer = audioEngine.mainMixerNode
        outputNode = audioEngine.outputNode
        format = outputNode!.inputFormat(forBus: 0)
        sampleRate = Float(format!.sampleRate)
        deltaTime = 1 / Float(sampleRate)
        
        self.engine = engine

        let inputFormat = AVAudioFormat(commonFormat: format!.commonFormat, sampleRate: Double(sampleRate), channels: 1, interleaved: format!.isInterleaved)
        audioEngine.attach(sourceNode)
        audioEngine.connect(sourceNode, to: mainMixer!, format: inputFormat!)
        audioEngine.connect(mainMixer!, to: outputNode!, format: nil)
        mainMixer?.outputVolume = 1
    }

    func start() {
        do {
            try audioEngine.start()
        } catch {
            fatalError("Coud not start engine: \(error.localizedDescription)")
        }
    }

    func stop() {
        audioEngine.stop()
    }
}

Solution

  • Your Float time is running out of precision. Convert it to a Double to have the same problem occur later, or replace self.time with an expression using the number of samples processed:

    var sampleCount: Int64 = 0  // instance variable
    
    // in sourceNode callback:
    let sampleVal = sin(440.0 * 2.0 * Double.pi * Double(sampleCount) / Double(sampleRate) )
    sampleCount += 1