admaDIC App Development & IT Solutions

AVAudioEngine with Real-Time Pitch Shifting

by Annett Schwarze | 2026-05-15

For iOS audio output, similar to the approach used in our Music Theory Learning app, one can create a powerful note player using just a few base audio files by shifting their pitch with `AVAudioUnitTimePitch`. This approach calculates the semitone difference between the desired note and the nearest available file, letting one play any note on the fly while keeping the app lightweight.

Using `AVAudioEngine`, a `AVAudioPlayerNode` feeds into a pitch node, then to the mixer, with pitch adjusted in cents (100 per semitone). It’s a smart way to handle dynamic music playback and demonstrates advanced AVFoundation audio graph techniques.

        
// Simplified concept
func playNote(note: String) -> KeyPlayer? {
    let audioEngine = AVAudioEngine()
    let playerNode = AVAudioPlayerNode()
    let pitch = AVAudioUnitTimePitch()

    // Not every note has its own audio file. Instead, a small set of base notes
    // is stored, and notes in between are reached by shifting the pitch.
    // The spec maps each note to the number of semitones it is above the nearest
    // available base note file:
    //   "A:0"  → file "A" exists, no shift needed
    //   "A#:1" → no file for A#, use file "A" and shift up 1 semitone
    //   "C#:1" → no file for C#, use file "C" and shift up 1 semitone
    //   "D:2"  → no file for D,  use file "C" and shift up 2 semitones
    let spec = "A:0,A#:1,B:0,C:0,C#:1,D:2,D#:0,E:1,F:2,F#:0,G:1,G#:2"
    var names: [String] = []
    var shifts: [Int] = []
    for part in spec.split(separator: ",") {
        let els = part.split(separator: ":")
        names.append(String(els[0]))
        shifts.append(Int(els[1])!)
    }

    guard let index = names.firstIndex(of: note) else { return nil }
    let semitoneShift = shifts[index]          // semitones above the base-note file
    let baseNoteName  = names[index - semitoneShift]  // step back to the file that exists

    let url = Bundle.main.url(forResource: baseNoteName, withExtension: "wav")!
    let audioFile = try AVAudioFile(forReading: url)

    audioEngine.attach(playerNode)
    audioEngine.attach(pitch)
    audioEngine.connect(playerNode, to: pitch, format: audioFile.processingFormat)
    audioEngine.connect(pitch, to: audioEngine.mainMixerNode, format: audioFile.processingFormat)

    // AVAudioUnitTimePitch.pitch is expressed in **cents** (100 cents = 1 semitone).
    // Multiplying the semitone shift by 100 converts it to the unit the API expects.
    pitch.pitch = 100 * Float(semitoneShift)

    try audioEngine.start()
    playerNode.scheduleFile(audioFile, at: nil)
    playerNode.play()
}
    
Audio Playing

 

www.admadic.de | webmaster@admadic.de | Legal Notice and Trademarks | Privacy
© 2005-2007 - admaDIC | All Rights Reserved
All other trademarks and/or registered trademarks are the property of their respective owners
Last Change: Fri May 15 09:04:01 2026 GMT