2

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.

Endanke
  • 867
  • 13
  • 24

0 Answers0