You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中MidiSequence多乐器播放音色异常如何解决?

问题描述

使用SwiftUI开发MIDI应用,希望乐曲中播放两种乐器,预期先听到小提琴音符,再听到钢琴音符,但实际播放第一个音符后,后续所有音符都以钢琴音色播放。

相关代码

ContentView.swift

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()

    }
    
}

MidiPlayer.swift

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()
            
        }

        }

    }

}

Song.swift

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")
          
        }
      
    }
}

SongComposer.swift

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
        
    }

}
解决方案

问题根源在于所有轨道都使用了同一个MIDI通道(通道0)。MIDI的程序变更消息(0xC0开头)是针对通道生效的,当第二个轨道发送钢琴的程序变更消息到通道0时,会覆盖之前设置的小提琴音色,导致后续所有通道0的音符都以钢琴音色播放。

解决方法是给每个轨道分配独立的MIDI通道(MIDI支持0-15共16个通道),让每个轨道的乐器变更和音符都在专属通道上运行,避免互相覆盖。

修改后的Song类代码

import Foundation
import AVFoundation

class Song {

    var musicSequence: MusicSequence?
    var tracks: [Int: MusicTrack] = [:]
    // 新增:记录每个轨道对应的MIDI通道
    private var trackChannels: [Int: UInt8] = [:]
    
    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
        // 给当前轨道分配唯一通道(从0开始递增,最多支持16个轨道)
        let channel = UInt8(trackId)
        trackChannels[trackId] = channel
        
        // 程序变更消息的status = 0xC0 + 通道号,确保每个通道独立设置乐器
        var inMessage = MIDIChannelMessage(status: 0xC0 + channel, 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)
        // 获取当前轨道对应的通道
        guard let channel = trackChannels[trackId] else {
            fatalError("Track ID not found")
        }
        
        var musicNote = MIDINoteMessage(channel: channel,
                                        note: note,
                                        velocity: 64,
                                        releaseVelocity: 0,
                                        duration: duration)
        
        guard MusicTrackNewMIDINoteEvent(tracks[trackId]!, time, &musicNote) == OSStatus(noErr) else {
            fatalError("Cannot add Note")
        }
    }
}

修改说明

  1. 新增trackChannels字典,用于记录每个轨道ID对应的MIDI通道号。
  2. addTrack方法中,为每个新轨道分配唯一的通道号(从0开始),并修改程序变更消息的status值为0xC0 + 通道号,确保乐器设置只对当前轨道的通道生效。
  3. addNote方法中,获取当前轨道对应的通道号,将音符消息绑定到该通道,确保音符使用对应轨道的乐器音色。

这样修改后,小提琴轨道使用通道0,钢琴轨道使用通道1,两者的乐器设置互不干扰,就能按预期先后播放两种乐器的音色了。


内容的提问来源于stack exchange,提问作者Kristof Nijs

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 17:32:35