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") } } }
修改说明
- 新增
trackChannels字典,用于记录每个轨道ID对应的MIDI通道号。 addTrack方法中,为每个新轨道分配唯一的通道号(从0开始),并修改程序变更消息的status值为0xC0 + 通道号,确保乐器设置只对当前轨道的通道生效。addNote方法中,获取当前轨道对应的通道号,将音符消息绑定到该通道,确保音符使用对应轨道的乐器音色。
这样修改后,小提琴轨道使用通道0,钢琴轨道使用通道1,两者的乐器设置互不干扰,就能按预期先后播放两种乐器的音色了。
内容的提问来源于stack exchange,提问作者Kristof Nijs
相关产品推荐
相关产品推荐

