Context:
I am trying to play multiple MIDI sequences in an iOS app using AVFoundation
. The tracks are loaded in MIDI files and I have successfully managed to play them one by one if I load them in a AVAudioSequencer
. I also have an AVAudioUnitSampler
object which is connected in an AVAudioEngine
and it successfully plays the selected instrument from the loaded sound bank in the sampler.
The setup works correctly if I create a new AVAudioSequencer
each time I play a sound. However, if I would like to reuse a sequencer after it's finished, it sounds like it's not using the sampler's instrument.
I suspect when I create the AVAudioSequencer
objects they are automatically connected to the AVAudioEngine
but only the last object will get the connected to the sampler.
I've tried to manually connect the destinationAudioUnit
of the tracks in the sequencer to the sampler, but then it doesn't play a sound at all. I also tried to make multiple samplers and connect them all to the engine, but that didn't work either.
My main question is: What is the proper way of using multiple AVAudioSequencer
objects with one AVAudioUnitSampler
? Or do I need to create a sampler for each sequencer and connect them somehow?
Here's a very basic playground example of two sequencers. When I run it, sequencer B successfully plays the sound through the sampler, but A is not using the instrument.
import UIKit
import PlaygroundSupport
import AVFoundation
class MyViewController : UIViewController {
let buttonA = UIButton(type: .system), buttonB = UIButton(type: .system)
let engine = AVAudioEngine()
lazy var sequencerA = AVAudioSequencer(audioEngine: engine)
lazy var sequencerB = AVAudioSequencer(audioEngine: engine)
let sampler = AVAudioUnitSampler()
// UI setup
override func loadView() {
let view = UIView()
view.backgroundColor = .white
buttonA.setTitle("Sequencer A", for: .normal)
buttonB.setTitle("Sequencer B", for: .normal)
buttonA.addTarget(self, action: #selector(playSequencerA), for: .touchUpInside)
buttonB.addTarget(self, action: #selector(playSequencerB), for: .touchUpInside)
view.addSubview(buttonA)
view.addSubview(buttonB)
buttonA.frame = CGRect(x: 150, y: 200, width: 100, height: 100)
buttonB.frame = CGRect(x: 150, y: 300, width: 100, height: 100)
self.view = view
}
// Sound engine setup
override func viewDidLoad() {
engine.attach(sampler)
engine.connect(sampler, to: engine.mainMixerNode, format: nil)
let soundBankPath = playgroundSharedDataDirectory.appendingPathComponent("gs_instruments.dls")
let midiA = playgroundSharedDataDirectory.appendingPathComponent("sfx_a.mid")
let midiB = playgroundSharedDataDirectory.appendingPathComponent("sfx_b.mid")
try! sampler.loadSoundBankInstrument(at: soundBankPath, program: 11, bankMSB: UInt8(kAUSampler_DefaultMelodicBankMSB), bankLSB: UInt8(kAUSampler_DefaultBankLSB))
try! sequencerA.load(from: midiA, options: [])
try! sequencerB.load(from: midiB, options: [])
try! engine.start()
}
@objc public func playSequencerA() { play(sequencerA) }
@objc public func playSequencerB() { play(sequencerB) }
func play(_ sequencer: AVAudioSequencer) {
if sequencer.isPlaying { sequencer.stop() }
sequencer.currentPositionInBeats = 0
try! sequencer.start()
}
}
PlaygroundPage.current.liveView = MyViewController()
Edit:
After some additional experiments I suspect that the AVAudioEngine
cannot have more than one AVAudioSequencer
instance (or I'm still doing something wrong). As a workaround, I have created a separate AVAudioEngine
object for each MIDI file that I need to play and they all have their own sampler and sequencer inputs, which plays the sounds just fine. I'm pretty sure this solution is not optimal, so I would be glad for any tips about a better setup.