Search code examples
swiftswiftuiavfoundation

How to play multiple instruments on a MidiSequence in Swift?


I am trying to create a midi app in SwiftUI. I would like to play two instruments in a song.

I have created a test app based on code I found on the internet.

First you should hear some notes of violin, then piano. However, after playing the first note, all the following notes are played with piano sound.

How can I solve this?

The code is as follows:

import SwiftUI

struct ContentView: View {
    
    @StateObject var player = MidiPlayer()
    
    var body: some View {
        
        VStack {
            
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            
            Button(action: playSong) {
                
                Text("Hello, world!")
                
            }

        }
        .padding()
        .onAppear {
            
            playSong()
            
        }
        
    }
    
    private func playSong() {
        
        let composer = SongComposer()
        let song = composer.compose()

        player.prepareSong(song: song)

        player.playSong()

    }
    
}
import Foundation
import AVFoundation

class MidiPlayer: ObservableObject {

    var midiPlayer: AVMIDIPlayer?
    var bankURL: URL
    
    init() {
    
        guard let bankURL = Bundle.main.url(forResource: "Nokia_Tongbao_Bank__Series_30__8-bit", withExtension: "sf2") else {
        
            fatalError("\"Nokia_Tongbao_Bank__Series_30__8-bit.sf2\" file not found.")
            
        }
        
        self.bankURL = bankURL
        
    }
    
    func prepareSong(song: Song) {
    
        var data: Unmanaged<CFData>?
        
        guard MusicSequenceFileCreateData(song.musicSequence!, MusicSequenceFileTypeID.midiType, MusicSequenceFileFlags.eraseFile, 480, &data) == OSStatus(noErr) else {
        
            fatalError("Cannot create music midi data")
            
        }
        
        if let md = data {
        
            let midiData = md.takeUnretainedValue() as Data
            
            do {
            
                try self.midiPlayer = AVMIDIPlayer(data: midiData, soundBankURL: self.bankURL)
                
            } catch let error {
            
                fatalError(error.localizedDescription)
                
            }

        }
        
        self.midiPlayer!.prepareToPlay()

    }
    
    func playSong() {
    
        DispatchQueue.main.async {
            
        if let md = self.midiPlayer {
        
            md.currentPosition = 0
        md.play()
            
        }

        }

    }

}
import Foundation
import AVFoundation

class Song {

    var musicSequence: MusicSequence?
    var tracks: [Int: MusicTrack] = [:]
    
    init() {
    
        guard NewMusicSequence(&musicSequence) == OSStatus(noErr) else {
        
            fatalError("Cannot create MusicSequence")
            
        }
        
    }

func addTrack(instrumentId: UInt8) -> Int {

    var track: MusicTrack?
    
    guard MusicSequenceNewTrack(musicSequence!, &track) == OSStatus(noErr) else {
    
        fatalError("Cannot add track")
        
    }
    
    let trackId = tracks.count
    tracks[trackId] = track
    
    var inMessage = MIDIChannelMessage(status: 0xC0, data1: instrumentId, data2: 0, reserved: 0)
    MusicTrackNewMIDIChannelEvent(track!, 0, &inMessage)

    return trackId
    
}

func setTempo(tempo: Float64) {

    let timeStamp = MusicTimeStamp(0)
    
    var tempoTrack: MusicTrack?
    MusicSequenceGetTempoTrack(musicSequence! ,&tempoTrack)
    
    removeTempoEvents(tempoTrack: tempoTrack!)
    
    MusicTrackNewExtendedTempoEvent(tempoTrack!, timeStamp, tempo)

}

private func removeTempoEvents(tempoTrack: MusicTrack) {

    var tempIter: MusicEventIterator?
    NewMusicEventIterator(tempoTrack, &tempIter)
    
    var hasEvent: DarwinBoolean = false
    MusicEventIteratorHasCurrentEvent(tempIter!, &hasEvent)
    
    while (hasEvent == true) {
    
        var stamp = MusicTimeStamp(0)
        var type: MusicEventType = 0
        var data: UnsafeRawPointer? = nil
        var sizeData: UInt32 = 0
        
        MusicEventIteratorGetEventInfo(tempIter!, &stamp, &type, &data, &sizeData)
        
        if (type == kMusicEventType_ExtendedTempo) {
        
            MusicEventIteratorDeleteEvent(tempIter!);
            MusicEventIteratorHasCurrentEvent(tempIter!, &hasEvent)
            
        } else {
        
            MusicEventIteratorNextEvent(tempIter!)
            MusicEventIteratorHasCurrentEvent(tempIter!, &hasEvent)
            
        }
        
    }
    
    DisposeMusicEventIterator(tempIter!)

}

    func addNote(trackId: Int, note: UInt8, duration: Float, position: Float) {
    
      let time = MusicTimeStamp(position)
      
      var musicNote = MIDINoteMessage(channel: 0,
                                      note: note,
                                      velocity: 64,
                                      releaseVelocity: 0,
                                      duration: duration)
                                      

        guard MusicTrackNewMIDINoteEvent(tracks[trackId]!, time, &musicNote) == OSStatus(noErr) else {
      
            fatalError("Cannot add Note")
          
        }
      
    }
}
import Foundation

class SongComposer {

    func compose() -> Song {
    
        let song = Song()
        song.setTempo(tempo: 240)
        var currentPosition: Float = 0
        
        
        let trackId = song.addTrack(instrumentId: 48)

        for _ in 0...1 {
        
            song.addNote(trackId: trackId, note: 64,
                      duration: 1.0, position: currentPosition)
                      
            currentPosition += 1.0
            
            song.addNote(trackId: trackId, note: 63,
                      duration: 1.0, position: currentPosition)
                      
            currentPosition += 1.0
            
        }
        
        song.addNote(trackId: trackId, note: 64,
                  duration: 1.0, position: currentPosition)
                  
        currentPosition += 1.0
        
        song.addNote(trackId: trackId, note: 59,
                  duration: 1.0, position: currentPosition)
                  
        currentPosition += 1.0
        
        song.addNote(trackId: trackId, note: 62,
                  duration: 1.0, position: currentPosition)
        
currentPosition += 2.0

song.addNote(trackId: trackId, note: 57, duration: 3.0, position:currentPosition)
song.addNote(trackId: trackId, note: 45, duration: 3.0, position:currentPosition)

        currentPosition -= 1.0
        
        song.addNote(trackId: trackId, note: 60,
                  duration: 1.0, position: currentPosition)
                  
        currentPosition += 4.0
        
        // track 2
        
        let track2Id = song.addTrack(instrumentId: 00)

        for _ in 0...1 {
        
            song.addNote(trackId: track2Id, note: 64,
                      duration: 1.0, position: currentPosition)
                      
            currentPosition += 1.0
            
            song.addNote(trackId: track2Id, note: 63,
                      duration: 1.0, position: currentPosition)
                      
            currentPosition += 1.0
            
        }
        
        song.addNote(trackId: track2Id, note: 64,
                  duration: 1.0, position: currentPosition)
                  
        currentPosition += 1.0
        
        song.addNote(trackId: track2Id, note: 59,
                  duration: 1.0, position: currentPosition)
                  
        currentPosition += 1.0
        
        song.addNote(trackId: track2Id, note: 62,
                  duration: 1.0, position: currentPosition)
        
currentPosition += 2.0

song.addNote(trackId: track2Id, note: 57, duration: 3.0, position:currentPosition)
song.addNote(trackId: trackId, note: 45, duration: 3.0, position:currentPosition)

        currentPosition -= 1.0
        
        song.addNote(trackId: track2Id, note: 60,
                  duration: 1.0, position: currentPosition)
                  
        return song
        
    }

}

Thanks in advance.


Solution

  • I finaly found the solution for my problem. I made the following changes to the code:

    • Set a timestamp for the MusicTrackNewMIDIChannelEvent function.
    • Added an extra function, namely setInstrument()
    • Added the function getStatus() so the MIDIChannelMessage is send to the richt channel
    • In the initializer MIDINoteMessage the channel is set to the trackId, so that each track uses its own channel.

    Below the modified code for the Song class:

    import Foundation
    import AVFoundation
    
    class Song {
    
        var musicSequence: MusicSequence?
        var tracks: [Int: MusicTrack] = [:]
        
        init() {
        
            guard NewMusicSequence(&musicSequence) == OSStatus(noErr) else {
            
                fatalError("Cannot create MusicSequence")
                
            }
            
        }
    
    func addTrack(instrumentId: UInt8) -> Int {
    
        var track: MusicTrack?
        
        guard MusicSequenceNewTrack(musicSequence!, &track) == OSStatus(noErr) else {
        
            fatalError("Cannot add track")
            
        }
        
        let trackId = tracks.count
        tracks[trackId] = track
        
        let time = MusicTimeStamp(0.0)
          
        var inMessage = MIDIChannelMessage(status: self.getStatus(trackId: trackId), data1: instrumentId, data2: 0, reserved: 0)
        MusicTrackNewMIDIChannelEvent(track!, time, &inMessage)
    
        return trackId
        
    }
    
        func setInstrument(trackId: Int, instrumentId: UInt8, position: Float) {
    
            if let track = self.tracks[trackId] {
                
                  let time = MusicTimeStamp(position)
    
                var inMessage = MIDIChannelMessage(status: self.getStatus(trackId: trackId), data1: instrumentId, data2: 0, reserved: 0)
            MusicTrackNewMIDIChannelEvent(track, time, &inMessage)
    
            }
                
        }
        
        private func getStatus(trackId: Int) -> UInt8 {
            
            switch trackId {
                
            case 0:
                return 0xC0
            case 1:
                return 0xC1
            case 2:
                return 0xC2
            case 3:
                return 0xC3
            case 4:
                return 0xC4
            case 5:
                return 0xC5
            case 6:
                return 0xC6
            case 7:
                return 0xC7
            case 8:
                return 0xC8
            case 9:
                return 0xC9
            case 10:
                return 0xCA
            case 11:
                return 0xCB
            case 12:
                return 0xCC
            case 13:
                return 0xCD
            case 14:
                return 0xCE
            case 15:
                return 0xCF
            default:
                return 0xC0
                
            }
            
        }
        
    func setTempo(tempo: Float64) {
    
        let timeStamp = MusicTimeStamp(0)
        
        var tempoTrack: MusicTrack?
        MusicSequenceGetTempoTrack(musicSequence! ,&tempoTrack)
        
        removeTempoEvents(tempoTrack: tempoTrack!)
        
        MusicTrackNewExtendedTempoEvent(tempoTrack!, timeStamp, tempo)
    
    }
    
    private func removeTempoEvents(tempoTrack: MusicTrack) {
    
        var tempIter: MusicEventIterator?
        NewMusicEventIterator(tempoTrack, &tempIter)
        
        var hasEvent: DarwinBoolean = false
        MusicEventIteratorHasCurrentEvent(tempIter!, &hasEvent)
        
        while (hasEvent == true) {
        
            var stamp = MusicTimeStamp(0)
            var type: MusicEventType = 0
            var data: UnsafeRawPointer? = nil
            var sizeData: UInt32 = 0
            
            MusicEventIteratorGetEventInfo(tempIter!, &stamp, &type, &data, &sizeData)
            
            if (type == kMusicEventType_ExtendedTempo) {
            
                MusicEventIteratorDeleteEvent(tempIter!);
                MusicEventIteratorHasCurrentEvent(tempIter!, &hasEvent)
                
            } else {
            
                MusicEventIteratorNextEvent(tempIter!)
                MusicEventIteratorHasCurrentEvent(tempIter!, &hasEvent)
                
            }
            
        }
        
        DisposeMusicEventIterator(tempIter!)
    
    }
    
        func addNote(trackId: Int, note: UInt8, duration: Float, position: Float) {
        
          let time = MusicTimeStamp(position)
          
            var musicNote = MIDINoteMessage(channel: UInt8(trackId <= 15 ? trackId : 0),
                                          note: note,
                                          velocity: 64,
                                          releaseVelocity: 0,
                                          duration: duration)
                                          
    
            guard MusicTrackNewMIDINoteEvent(tracks[trackId]!, time, &musicNote) == OSStatus(noErr) else {
          
                fatalError("Cannot add Note")
              
            }
          
        }
        
    }
    

    Happy programming! Kristof Nijs