2

*** Update 2 ***

Ok, done some more digging and managed to get things working with MIKMIDI by starting at position 1 rather than position 0; the same fix hasn't worked with AudioKit.

Further, I've created a new, ugly AF, app that replicates behaviour across both frameworks and outputs, among other things, the track data, and there is definitely a difference between them. I include the code below for perusal. You'll need both MIKMIDI and AudioKit available as frameworks, and a soundfont. Both appear to be working identically but the generated data is different. Again, it could be that I'm making a fundamental error for which I apologise if that's the case, but if anyone can point out the issue I'd be grateful. Many thanks.

import SwiftUI
import MIKMIDI
import AudioKit

let MIKsequence = MIKMIDISequence()
let MIKsequencer = MIKMIDISequencer()

var AKoutputSampler = MIDISampler(name: "output")
var AKsequencer = AppleSequencer()
var AKmixer = Mixer()
var AKengine = AudioEngine()

func AKsetup() {
    print("AK Setup Start---------")
    AKmixer.addInput(AKoutputSampler)
    AKengine.output = AKmixer

    do {
        try AKengine.start()
        

    } catch {
        print("error starting the engine: \(error)")
    }
    print("AK Setup End ----------")
}

func AKinitialise(){
    print("AK Initilise Start --------")
    AKsequencer = AppleSequencer()

    for t in 0..<AKsequencer.tracks.count {
        AKsequencer.deleteTrack(trackIndex: t)
    }
    
    let AKtrackManager = AKsequencer.newTrack("piano")

    for note in 1..<6{
        AKtrackManager?.add(noteNumber: MIDINoteNumber(note+60), velocity: 100, position: Duration(beats: Double(note * 16)/16), duration: Duration(beats: 0.25),channel: 1)
    }
    
    let length = AKtrackManager?.length
    print("Length = \(length)")
    let mnd : [MIDINoteData] = (AKtrackManager?.getMIDINoteData())!
    for d in mnd {
        
        print("Note \(d.noteNumber), position \(d.position.seconds)")
    }
    
    AKsequencer.setLength(Duration(beats: Double(length!)))
    AKsequencer.disableLooping()
    AKsequencer.setTempo(120)
    AKsequencer.addTimeSignatureEvent(timeSignature: TimeSignature(topValue: 4, bottomValue: .four))
    
    AKtrackManager?.setMIDIOutput(AKoutputSampler.midiIn)
    
    let hexValues = AKsequencer.genData()!.map { String(format: "%02X", $0) }
    print(hexValues.joined(separator: " "))
    AKsequencer.debug()

    print("AK Initialise End ---------")
}
    
func loadSF2(name: String, ext: String, preset: Int, sampler: MIDISampler) {
    print("Load SF2 Start")
    guard let url = Bundle.main.url(forResource: name, withExtension: ext) else {
        print("LoadSF2: Could not get SoundFont URL")
        return
    }
    do {
        try sampler.loadMelodicSoundFont(url: url, preset: preset)
    } catch {
        print("can not load SoundFont \(name) with error: \(error)")
    }
    print("Load SF2 End")
}

func AKplay() {
    AKengine.stop()
    
    loadSF2(name: "Chaos Bank", ext: "sf2", preset: 1, sampler: AKoutputSampler)

    do {
        try AKengine.start()
    } catch {
        print("error starting the engine: \(error)")
    }
    AKsequencer.play()
}

func AKstop(){
    AKsequencer.stop()
    AKsequencer.rewind()
}

func MIKinitialise(){
    print("MIK Initialise Start")
    do {
        let tempo = 120.0
        let signature = MIKMIDITimeSignature(numerator: 4, denominator: 4)
                
        MIKsequence.setOverallTempo(tempo)
        MIKsequence.setOverallTimeSignature(signature)
        
        for t in MIKsequence.tracks {
            MIKsequence.removeTrack(t)
        }
        
        let _ = try MIKsequence.addTrack()
        
        let track = MIKsequence.tracks[0]
        let trackSynth = MIKsequencer.builtinSynthesizer(for: track)
        
        if let soundfont = Bundle.main.url(forResource: "Chaos Bank", withExtension: "sf2") {
            do {
                try  trackSynth?.loadSoundfontFromFile(at: soundfont)
            } catch {
                print("can not load SoundFont  with error: \(error)")
            }

            let instrumentId = MIKMIDISynthesizerInstrument(id: 10, name: "Eric")
            try trackSynth!.selectInstrument(instrumentId!, error: ())

            print("Available Instruments \(trackSynth!.availableInstruments)")
        }

        var notes = [MIKMIDINoteEvent]()
        for n in 1..<6 {
            let note = MIKMIDINoteEvent(timeStamp:Double(n),note:UInt8(60 + n),velocity:100,duration:0.25,channel:1)
            notes.append(note)
        }
        
        track.addEvents(notes)
        let length = track.length
        MIKsequence.length = length
        MIKsequencer.sequence = MIKsequence
        
        print("Duration in seconds \(MIKsequencer.sequence.durationInSeconds)")

        print("Tempo Track \(MIKsequence.tempoTrack.length), \(MIKsequence.tempoTrack.notes.count)")
        
        for t in MIKsequence.tracks {
            print("Track Number \(t.trackNumber)")
            for notes in t.notes {
                print("Note \(notes.note), \(notes.duration), \(notes.timeStamp)")
            }
        }

        let hexValues = MIKsequencer.sequence.dataValue!.map { String(format: "%02X", $0) }
        print(hexValues.joined(separator: " "))
        
                
    } catch let error {
        print(error.localizedDescription)
    }
    print("MIK Initialise End")
}

func startMIKPlayback(){
    MIKsequencer.startPlayback()
}

func stopMIKPlayback(){
    MIKsequencer.stop()
}

func getDocumentsDirectory() -> URL {
    // find all possible documents directories for this user
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)

    // just send back the first one, which ought to be the only one
    print(paths[0])
    return paths[0]
}

func saveMIKFile()->String{
    let date = Date()
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "YYYYMMddHHmmss"

    let filename = dateFormatter.string(from: date) + " MIK Song.mid"
    try! MIKsequence.write(to: getDocumentsDirectory().appendingPathComponent(filename))
    return getDocumentsDirectory().absoluteString
}

func saveAudioKitFile()->String{
    let date = Date()
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "YYYYMMddHHmmss"

    let filename = dateFormatter.string(from: date) + "AK Song.mid"
    try! AKsequencer.genData()!.write(to: getDocumentsDirectory().appendingPathComponent(filename))
    return getDocumentsDirectory().absoluteString
}

struct ContentView: View {
    
    init(){
        AKsetup()
        MIKinitialise()
    }
    
    var body: some View {
        HStack{
            VStack {
                Text("MIKMIDI Test 01")
                
                Button("Play", action: startMIKPlayback)
                Button("Stop", action: stopMIKPlayback)
                Button("Save") {
                    let _ = print(saveMIKFile())
                }
            }
            .padding()
            
            VStack {
                Text("AudioKit Test 01")
                let _ = AKinitialise()
                
                Button("Play", action: AKplay)
                Button("Stop", action: AKstop)
                Button("Save") {
                    let _ = print(saveAudioKitFile())
                }
            }
            .padding()
        }
        
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

*** End Update 2 *** *** Update *** Still having the same problem, and now I've tried with both AudioKit and MIKMIDI. I've run the generated file through a midi analyzer online and it says "Undefined Variable: Last". I've reached out to both MIKMIDI and Midi-Analyzer authors to see if they can assist but if anyone can throw light on this issue, I'd be grateful. *** End Update ***

I'm working on an app that saves a midi sequence to file, using AudioKit and genData(). However, it seems that a recent update - either OS or Audiokit - has affected the way things save.

The startnote now seems to be offset on tracks by a varying amount, and the rest of the track then follows that offset. Oftentimes the end notes of the track may be missing. This problem was not occurring until recently.

Showing output of the sequence shows the data in the correct positions but it's coming out like this (the notes should be starting at position 0 in the track):

Pattern Offset shown in Garageband

I've also had difficulty in importing the same midi file into other packages; again, this wasn't a problem until recently.

I'm happy to be told I'm doing something amiss but, as I've said, it seems to have been working up until recently.

Any help would be really appreciated.

func getDocumentsDirectory() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return paths[0]
}

func saveFile()->String{
    try! sequencer.genData()!.write(to: getDocumentsDirectory().appendingPathComponent("QuickTestmidi.mid"))
    return getDocumentsDirectory().absoluteString
}

func setSequence_01(){
    var trackLength = 0.0
    sequencer.tracks[0].setMIDIOutput(outputSampler[0].midiIn)
    
    for track in sequencer.tracks {
        track.clear()
    }
    
    for i in 0..<16 {
        sequencer.tracks[0].add(noteNumber: MIDINoteNumber(96 + i), velocity: MIDIVelocity(100), position: Duration(beats: Double(2 * i)), duration: Duration(beats: 1),channel: 1)
                
        trackLength = Double(sequencer.tracks[0].length)
                        
        sequencer.setLength(Duration(beats:trackLength))
        sequencer.enableLooping()
        sequencer.setTempo(120)
        sequencer.addTimeSignatureEvent(timeSignature: TimeSignature(topValue: 4, bottomValue: .four))
    }
}
SadOldGoth
  • 21
  • 3
  • Hate to throw another framework into the mix, but I think you should try MIDIKit, which is so good we'll be eventually removing MIDI from AudioKit and using MIDIKit as a dependency. – Aurelius Prochazka Nov 17 '22 at 17:48

0 Answers0