I’m running into a strange issue where an AVAudioSequencer
crashes the second time prepareToPlay
is called when I connect and disconnect headphones two times without starting the sequencer. I’m subscribing to the AVAudioEngineConfigurationChange
notification, and recreating my audio sampler nodes in response. Everything is fine if the sequencer is started between the headphone disconnections. Anyone have an idea as to what is going on? I’ve included a minimal example that demonstrates the issue, it’s reproducible for me in the simulator. The SoundFont is an open source piano one I found online.
import AVFoundation
import Combine
import SwiftUI
@main
struct TestMidiApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
let sequencer = Sequencer()
var body: some View {
VStack {
Button("TAP ME") {
try? sequencer.play()
}
}
.padding()
.onAppear {
do {
try sequencer.setup()
} catch {
print(error)
}
}
}
}
class Sequencer {
let soundFontURL = Bundle.main.url(forResource: "sc55_piano_v2", withExtension: "sf2")!
private var engine: AVAudioEngine!
private var sequencer: AVAudioSequencer!
private var cancellables: [AnyCancellable] = []
private var track: AVMusicTrack!
private var sampler: AVAudioUnitSampler!
func setup() throws {
try AVAudioSession.sharedInstance().setCategory(.playback)
try AVAudioSession.sharedInstance().setActive(true)
let engine = AVAudioEngine()
let samplerNode = AVAudioUnitSampler()
try samplerNode.loadSoundBankInstrument(
at: soundFontURL,
program: 0,
bankMSB: UInt8(kAUSampler_DefaultMelodicBankMSB),
bankLSB: UInt8(kAUSampler_DefaultBankLSB)
)
engine.attach(samplerNode)
engine.connect(samplerNode, to: engine.outputNode, format: nil)
self.sampler = samplerNode
try engine.start()
self.engine = engine
let sequencer = AVAudioSequencer(audioEngine: engine)
let track = sequencer.createAndAppendTrack()
track.addEvent(
AVMIDINoteEvent(
channel: 0,
key: 60,
velocity: 72,
duration: 1
),
at: 0
)
track.destinationAudioUnit = samplerNode
self.track = track
self.sequencer = sequencer
setupSubs()
}
func play() throws {
if !engine.isRunning {
try engine.start()
}
sequencer?.stop()
sequencer?.currentPositionInBeats = 0
try sequencer?.start()
}
func setupSubs() {
NotificationCenter.default.publisher(for: .AVAudioEngineConfigurationChange).sink { _ in
try? self.reset()
}.store(in: &cancellables)
}
func reset() throws {
engine.detach(sampler)
let samplerNode = AVAudioUnitSampler()
try samplerNode.loadSoundBankInstrument(
at: soundFontURL,
program: 0,
bankMSB: UInt8(kAUSampler_DefaultMelodicBankMSB),
bankLSB: UInt8(kAUSampler_DefaultBankLSB)
)
engine.attach(samplerNode)
engine.connect(samplerNode, to: engine.outputNode, format: nil)
self.sampler = samplerNode
track.destinationAudioUnit = samplerNode
sequencer.prepareToPlay() // crashes here the second time
}
}
#Preview {
ContentView()
}