Loopy Pro: Create music, your way.

What is Loopy Pro?Loopy Pro is a powerful, flexible, and intuitive live looper, sampler, clip launcher and DAW for iPhone and iPad. At its core, it allows you to record and layer sounds in real-time to create complex musical arrangements. But it doesn’t stop there—Loopy Pro offers advanced tools to customize your workflow, build dynamic performance setups, and create a seamless connection between instruments, effects, and external gear.

Use it for live looping, sequencing, arranging, mixing, and much more. Whether you're a live performer, a producer, or just experimenting with sound, Loopy Pro helps you take control of your creative process.

Download on the App Store

Loopy Pro is your all-in-one musical toolkit. Try it for free today.

Saving a midi file corrupts it

Hi All,

Really hoping someone can help as I'm pulling my hair out here, and I really don't have much to spare.

The app I'm developing is coming along slowly but surely (except for being butt-ugly and still trying to resolve heterogenous object saving/loading in JSON, dammit!), but I've come across an issue recently which I'm having a real problem trying to resolve.

I'm using Swift and SwiftUI, and have tried to resolve the issue with both AudioKit and MIKMIDI. I'm simply trying to save the notes I've produced as a midi file. Up until recently, and I fear it might have been the latest OS update, everything was working fine (using AudioKit) and now when I try to save a file...well, it's just corrupt. A single track with 5 notes starting from position 0 looks like this when viewed in Garageband:

I've used an online midi-analyzer and it gives an error of "Undefined variable: last". The example code I've written is as simple as can be (though you'll need MIKMIDI, and a soundfont in a folder called resources to make it work) and the code is shown at the bottom of the post.

My next step will be to load a file I know works and then save it straight back out again to see if there's any difference.

Has anyone else come across this issue, or is it just something I'm doing wrong? The fact it's happening in both frameworks is really frustrating but suggests that they're not the problem. Has the midi format changed in latest version, which would be weird I admit? Not knowing a great deal about how Mac's work under the hood, is there a .mid definition that could have been corrupted? Or is it that I'm simply doing something fundamentally wrong?

I'd really appreciate it if someone could help me out here as I've scoured and posted elsewhere but have not had any responses back.

Thanks in advance,

Jes

import SwiftUI
import MIKMIDI


let sequence = MIKMIDISequence()
let sequencer = MIKMIDISequencer()

func initialise(){

    do {
        let tempo = 120.0
        let signature = MIKMIDITimeSignature(numerator: 4, denominator: 4)

        sequence.setOverallTempo(tempo)
        sequence.setOverallTimeSignature(signature)

        for t in sequence.tracks {
            sequence.removeTrack(t)
        }

        let _ = try sequence.addTrack()

        let track = sequence.tracks[0]
        let trackSynth = sequencer.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)")
            }

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

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

        var notes = [MIKMIDINoteEvent]()
        for n in 0..<5 {
            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
        sequence.length = length
        sequencer.sequence = sequence

        print("Duration in seconds \(sequencer.sequence.durationInSeconds)")

        print("Tempo Track \(sequence.tempoTrack.length), \(sequence.tempoTrack.notes.count)")

        for t in sequence.tracks {
            print("Track Number \(t.trackNumber)")
            for notes in t.notes {
                print("Note \(notes.note), \(notes.duration), \(notes.timeStamp)")
            }
        }

        let hexValues = sequencer.sequence.dataValue!.map { String(format: "%02X", $0) }
        print(hexValues.joined(separator: " "))


    } catch let error {
        print(error.localizedDescription)
    }

}

func startPlayback(){
    sequencer.startPlayback()
}

func stopPlayback(){
    sequencer.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 saveFile()->String{
    try! sequence.write(to: getDocumentsDirectory().appendingPathComponent("MIKMIDI.mid"))
    return getDocumentsDirectory().absoluteString
}

struct ContentView: View {
    var body: some View {
        VStack {
            Text("MIKMIDI Test 01")
            let _ = initialise()

            Button("Play", action: startPlayback)
            Button("Stop", action: stopPlayback)
            Button("Save") {
                let _ = print(saveFile())
            }
        }
        .padding()
    }
}

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

Comments

  • Hi All.

    I think I might have resolved it by starting at position 1...

    Could it really be that simple? That seems to have worked with the code above, now need to check in my full app. Not sure that's ever changed but worth verifying - I'll get back to you as soon as I've found out.

    Jes

  • Hi All.

    Ok, so tried setting the start position to 1 when using both MIKMIDI and AudioKit; the former produces a valid file, the latter still has the same problem. I've taken a look at the Data within both sequences I've created using each Framework and there is a clear discrepancy though I don't understand the format well enough to identify what that is.

    I'll post the full (again, butt-ugly) test code below and if anyone can tell me what I'm doing wrong it'd be appreciated.

    I've got no problem in using AudioKit for the main part of my app and MIKMIDI solely for saving the data but, obviously, would prefer not to (I currently can't use MIKMIDI for everything as I'm running into problems with choosing different instruments from soundfont).

    Again, thanks in advance,

    Jes

    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()
        }
    }
    
  • @SadOldGoth : have you contacted the AudioKit folks about this?

  • @espiegel123 said:
    @SadOldGoth : have you contacted the AudioKit folks about this?

    Yes, I have, via StackOverflow, though not quite as fully detailed as above. If you know of another channel to contact them, could you let me know? Thanks (and thanks for responding).

    Jes

  • @SadOldGoth said:

    @espiegel123 said:
    @SadOldGoth : have you contacted the AudioKit folks about this?

    Yes, I have, via StackOverflow, though not quite as fully detailed as above. If you know of another channel to contact them, could you let me know? Thanks (and thanks for responding).

    Jes

    There is contact number information on

    https://audiokitpro.com/audiokit/

    You may also be able to log bugs via the relevant github

Sign In or Register to comment.