How can I set up AppleSequencer and MIDISampler to ignore noteOffs?

88 Views Asked by At

I'm using AudioKit 5.6.2 to create a simple Drum Sequencer. I'm using some Samples that already have reverb on them and therefore are quite long (e.g. 1.5-2.0 seconds). If I set the Duration to the length of the sample the following happens: Sample one plays, Sample two plays and will be cut off early by the note off of the first sample.

I got the following code for creating a simple Drum Sequencer, similar to the one in the AudioKit CookBook:

import SwiftUI
import AudioKit
import AVFoundation

class DrumsConductor: ObservableObject {
    
    let engine = AudioEngine()
    let sequencer = AppleSequencer()
    private var drums = MIDISampler(name: "Drums")
    
    var tempo: Double = 60.0 {
        didSet {
            sequencer.setTempo(tempo)
        }
    }
    
    init() {
        engine.output = drums
    }
    
    func destroy() {
        drums.destroyEndpoint()
    }
    
    func playSequence() {
        sequencer.stop()
        sequencer.setTempo(tempo)
        if sequencer.trackCount < 1 {
            let _ = sequencer.newTrack()
        }
        sequencer.setGlobalMIDIOutput(drums.midiIn)
        
        for index in 0..<8 {
            sequencer.tracks[0].add(noteNumber: 48, velocity: 60, position: Duration(beats: Double(index)), duration: Duration(beats: 1.2))
        }
        
        sequencer.rewind()
        sequencer.play()
    }
    
    func stopSequence() {
        guard sequencer.isPlaying else { return }
        sequencer.stop()
        sequencer.preroll()
    }
    
    func start() {
        do {
            try engine.start()
        } catch let err {
            Log(err)
        }
        setupSound()
    }
    
    func stop() {
        engine.stop()
    }
    
    func setupSound() {
        let path = "Sounds/Sampler Instruments/Drums"
        
        do {
            try drums.loadSoundFont(path, preset: 0, bank: 0)
        } catch {
            fatalError(error.localizedDescription)
        }
    }
}

I tried to find a way to make the AppleSequencer or MIDISampler ignore noteOffs but couldn't find anything. I might be able to calculate the duration of each sample by measuring its distance to the next hit or I might use multiple tracks that are used alternating. But obviously I'd prefer a solution where the samples just are not cut off. Is there Anything I could do about it within existing AudioKit solutions?

Edit

I had this idea of copying MIDISampler to a new class DrumMIDISampler. I only changed the enableMIDI function to not send noteOffs.


public func enableMIDI(_ midiClient: MIDIClientRef = MIDI.sharedInstance.client,
                           name: String? = nil) {
        let cfName = (name ?? self.name) as CFString
        guard let midiBlock = avAudioNode.auAudioUnit.scheduleMIDIEventBlock else {
            fatalError("Expected AU to respond to MIDI.")
        }
        CheckError(MIDIDestinationCreateWithBlock(midiClient, cfName, &midiIn) { packetList, _ in
            for e in packetList.pointee {
                print("Call: \(e)")
                for event in e {
                    print("Event: \(event)")
                    event.data.withUnsafeBufferPointer { ptr in
                        guard let ptr = ptr.baseAddress else { return }
                        
                        
                        // Check if the event is a NoteOff event
                        let statusByte = ptr[0]
                        let isNoteOff = (statusByte >= 0x80 && statusByte <= 0x8F) || (ptr[0] == 0x90 && ptr[2] == 0) // NoteOff Event oder NoteOn Event mit einer Velocity von 0
                        
                        // Only process the event if it's not a NoteOff event
                        if !isNoteOff {
                            midiBlock(AUEventSampleTimeImmediate, 0, event.data.count, ptr)
                        }
                    }
                }
            }
        })
    }

It works as expected in a debug configuration. But interestingly it doesn't work in a release environment. All MIDIPackets that I get are empty (see print("Call: \(e)")):

Debug:

Call: MIDIPacket(timeStamp: 3977539537470, length: 3, data: (144, 48, 60,...

Release:

MIDIPacket(timeStamp: 0, length: 0, data: (0, 0, 0,...

How is this possible and what can I do about it?

0

There are 0 best solutions below