Maximilian
Maximilian

Reputation: 1117

AudioKit: Using the new AKSequencer with any variety of the callback instruments

This topic has been covered Numerous Times, and I have successfully used a AKMIDICallbackInstrument with the old AKAppleSequencer in my previous apps.

I am starting to use the new AKSequencer which is absolutely phenomenal: elegant interface, and easy to use. However, I cannot for my life figure out how to handle callback events with it. I need to use a callback in order to trigger GUI events based on the sequencer playing.

Here is my example code:

    private func setMetronome(bpm: BPM, beats:Int)
    {
        sequencer = AKSequencer(targetNode: metronomeSampler)
        sequencer.tempo = bpm
        sequencer.loopEnabled = false
        sequencer.length = Double(beats)

        metroCallback.callback = {status, noteNumber, velocity in
            if let midiStatus = AKMIDIStatus(byte: status), midiStatus.type != .noteOn { return }

            //Do callback stuff here
        }

        let metroCallbackTrack = sequencer.addTrack(for: metroCallback)

        for i in 0..<beats
        {
            if i == 0
            {
                sequencer.add(noteNumber: MIDINoteNumber(67), position: Double(i), duration: 1.0)
                metroCallbackTrack.add(noteNumber: MIDINoteNumber(67), position: Double(i), duration: 1.0)
            }
            else if (i % 4 == 0)
            {
                sequencer.add(noteNumber: MIDINoteNumber(67), position: Double(i), duration: 1.0)
                metroCallbackTrack.add(noteNumber: MIDINoteNumber(60), position: Double(i), duration: 1.0)
            }
            else
            {
                sequencer.add(noteNumber: MIDINoteNumber(60), position: Double(i), duration: 1.0)
                metroCallbackTrack.add(noteNumber: MIDINoteNumber(60), position: Double(i), duration: 1.0)
            }
            print("seq count:\(i)")
        }

        for track in sequencer.tracks
        {
            print("Adding track to mixer:\(track.length)")
            track >>> mixer
        }
    }

This code correctly creates a sequence of n number of beats, it plays back through my AKSampler all is well in the world. Except that no callback events happen (using print statements to confirm)

Thought Process

With AKAppleSequencer and AKMIDICallbackInstrument, you could set the globalMIDIOutput with the AKAppleSequencer with the midi input of AKMIDICallBackInstrument.

Now the new AKSequencer and AKCallbackInstrument do not have these options, nor does the new AKSequencerTrack (the old AKAppleSequencer would use AKMusicTrack objects which could set midi input/output). In looking at the implementation of the new AKSequencer, it is driven by AKNode objects, AKCallbackInstrument is a AKNode object and should be able be driven by a track with the right midi data.

I add a track to my sequencer, and from that track, and the necessary midi data that duplicate exactly the midi events I want to callback on and perform my GUI events. However with this approach, it does not seem to call the callback.

Does anyone have any idea how to use these new components with a callback? I really don't want to go back to AKAppleSequencer unless there is clearly no way to drive callbacks with the new AKSequencer.

Upvotes: 2

Views: 961

Answers (3)

heiko
heiko

Reputation: 1436

A working version of the above example for Audiokit 5.3. Actually the native sequencer is now part of AudioKitEx, so you'll have to import both packages. Really a pitty, that there is not a working example in the cookbook. All in all the most precise and reliable timer for iOS and Mac.

From the previous example, I introduced the new Classnames, dropped the mixer and reduced the Sequencer to one SequencerTrack, this is possible, because they operate on their own. With Audiokit 5.3. it is not possible, to connect a track to the mixer directly, besides that, the >>> operator doesn't work anymore.

So this should be the most minimal version of an Audiokit Sequencer:

//  Created by c_booth,
//  current version by Heiko Henrich on 23.12.22.
//

import AudioKit
import AudioKitEX
import AVFoundation

class SequencerWrapper {
    var cbInst: CallbackInstrument!
    var engine: AudioEngine!
    var track: SequencerTrack!
    init() {
        engine = AudioEngine()
        cbInst = CallbackInstrument { status, note, vel in
            guard let status = MIDIStatus(byte: status),
                let type = status.type,
                type == .noteOn else { return }
            print("note on: \(note)")
            // trigger sampler etc from here
        }
        engine.output = cbInst

        // set up a track
        track = SequencerTrack(targetNode: cbInst)
        track.add(noteNumber: 60, position: 0, duration: 0.5)
        track.length = 1.0
        
        try! engine.start()
        track.playFromStart()
    }
}

Upvotes: 1

Daniel
Daniel

Reputation: 81

Thanks for the working example @c_booth! Just wanted to add for any dummies like me that couldn't figure out why the above example wasn't working, you will still need to call AudioKit.start().

Upvotes: 3

c_booth
c_booth

Reputation: 2225

To get AKCallbackInstrument working with the new AKSequencer, try connecting your callback instrument to your output, e.g.,

metroCallback >>> mixer

Not obvious, but has worked for me.

Edit: including a minimal working version of the new AKSequencer with AKCallbackInstrument

class SequencerWrapper {
    var seq: AKSequencer!
    var cbInst: AKCallbackInstrument!
    var mixer: AKMixer!

    init() {
        mixer = AKMixer()
        AudioKit.output = mixer
        seq = AKSequencer()
        cbInst = AKCallbackInstrument()

        // set up a track
        let track = seq.addTrack(for: cbInst)
        for i in 0 ..< 4 {
            track.add(noteNumber: 60, position: Double(i), duration: 0.5)
        }
        track.length = 4.0
        track.loopEnabled = true
        track >>> mixer  // must send track to mixer

        // set up the callback instrument
        cbInst.callback = { status, note, vel in
            guard let status = AKMIDIStatus(byte: status),
                let type = status.type,
                type == .noteOn else { return }
            print("note on: \(note)")
            // trigger sampler etc from here
        }
        cbInst >>> mixer // must send callbackInst to mixer
    }

    func play() {
        seq.playFromStart()
    }
}

Upvotes: 8

Related Questions