SwiftUI+SpriteKit节奏游戏:MIDI播放时AudioKit监听无响应
节奏游戏开发中AVMIDIPlayer播放MIDI文件无法触发AudioKit MIDIListener事件的问题
问题描述
使用SwiftUI+SpriteKit开发iOS节奏游戏,需求是播放MIDI文件时,根据当前播放的音符触发对应事件。采用AVMIDIPlayer播放MIDI文件,尝试通过AudioKit的MIDIListener监听事件,但播放文件时receivedMIDINoteOn函数始终无法触发;手动调用midi.sendNoteOnMessage(noteNumber: 60, velocity: 127)时,监听函数能正常响应。
测试代码
class MidiPlayer: ObservableObject { let midiFile: URL = Bundle.main.url(forResource: "no_name" , withExtension: "mid")! var midi = MIDI() var midiPlayer: AVMIDIPlayer? func playFile() { do { self.midi.openOutput() try midiPlayer = AVMIDIPlayer(contentsOf: midiFile as URL, soundBankURL: nil) midiPlayer?.prepareToPlay() } catch { print("could not create MIDI player") } self.midi.sendNoteOnMessage(noteNumber: 60, velocity: 127) midiPlayer?.play { print("finished playing") } } } class MIDIReceiver: MIDIListener, ObservableObject { static let instance = MIDIReceiver() var midi = MIDI() func connectInputs() { print("connecting devices") midi.openInput() midi.addListener(self) } func receivedMIDINoteOn(noteNumber: AudioKit.MIDINoteNumber, velocity: AudioKit.MIDIVelocity, channel: AudioKit.MIDIChannel, portID: MIDIUniqueID?, timeStamp: MIDITimeStamp?) { print("okay3") print(String(noteNumber)) } func receivedMIDINoteOff(noteNumber: AudioKit.MIDINoteNumber, velocity: AudioKit.MIDIVelocity, channel: AudioKit.MIDIChannel, portID: MIDIUniqueID?, timeStamp: MIDITimeStamp?) { print("okay4") } } struct GameView: View { @StateObject var midi = MIDIReceiver() var scene: SKScene { let scene = GameScene() scene.size = CGSize(width: 800, height: 600) scene.scaleMode = .fill return scene } var body: some View { VStack { SpriteView(scene: scene) .frame(width: 800, height: 600) .ignoresSafeArea() }.onAppear(perform: { midi.connectInputs() }) } } class GameScene: SKScene { var player = MidiPlayer() override func didMove(to view: SKView) { physicsBody = SKPhysicsBody(edgeLoopFrom: frame) } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { player.playFile() } }
问题原因
AVMIDIPlayer默认直接将MIDI数据转换为音频输出,不会把MIDI事件发送到系统MIDI总线,因此AudioKit的MIDIListener无法捕获到这些事件。而手动调用sendNoteOnMessage是直接通过AudioKit的MIDI实例发送事件,所以能被监听到。
解决方案
方案1:使用AudioKit的MIDIFilePlayer替代AVMIDIPlayer
AudioKit提供的MIDIFilePlayer专门用于播放MIDI文件,会将MIDI事件注入到AudioKit的MIDI系统中,天然支持MIDIListener监听。
修改后的MidiPlayer类示例:
import AudioKit class MidiPlayer: ObservableObject { let midiFile: URL = Bundle.main.url(forResource: "no_name", withExtension: "mid")! let midi = MIDI() var midiFilePlayer: MIDIFilePlayer? func playFile() { do { // 初始化MIDIFilePlayer midiFilePlayer = try MIDIFilePlayer(url: midiFile) // 将播放器输出连接到AudioKit的MIDI输入 midiFilePlayer?.midiOutput = midi.input // 准备并播放 midiFilePlayer?.prepareToPlay() midiFilePlayer?.play { print("finished playing") } } catch { print("could not create MIDI file player: \(error)") } // 测试手动发送事件 midi.sendNoteOnMessage(noteNumber: 60, velocity: 127) } }
方案2:自定义AVMIDIPlayer的事件转发
如果坚持使用AVMIDIPlayer,可以通过设置其delegate,在播放过程中捕获MIDI事件并手动转发到AudioKit的MIDI实例。
修改后的代码示例:
import AVFoundation import AudioKit class MidiPlayer: ObservableObject, AVMIDIPlayerDelegate { let midiFile: URL = Bundle.main.url(forResource: "no_name", withExtension: "mid")! let midi = MIDI() var midiPlayer: AVMIDIPlayer? func playFile() { do { midi.openOutput() midiPlayer = try AVMIDIPlayer(contentsOf: midiFile, soundBankURL: nil) midiPlayer?.delegate = self // 设置代理 midiPlayer?.prepareToPlay() } catch { print("could not create MIDI player") } midi.sendNoteOnMessage(noteNumber: 60, velocity: 127) midiPlayer?.play { print("finished playing") } } // 捕获播放的音符开启事件并转发 func player(_ player: AVMIDIPlayer, didPlayMIDINote note: UInt8, velocity: UInt8, onChannel channel: UInt8) { midi.sendNoteOnMessage(noteNumber: MIDINoteNumber(note), velocity: MIDIVelocity(velocity), channel: MIDIChannel(channel)) } // 捕获播放的音符关闭事件并转发 func player(_ player: AVMIDIPlayer, didStopPlayingMIDINote note: UInt8, velocity: UInt8, onChannel channel: UInt8) { midi.sendNoteOffMessage(noteNumber: MIDINoteNumber(note), velocity: MIDIVelocity(velocity), channel: MIDIChannel(channel)) } }
注意事项
- 使用方案1时,需确保App启动时已正确初始化AudioKit(调用
AudioKit.start())。 - 方案2中
AVMIDIPlayerDelegate仅能捕获音符开/关事件,若需监听其他类型MIDI事件(如控制变化),需额外实现对应逻辑。
内容的提问来源于stack exchange,提问作者Seppo Hyvärinen
相关产品推荐
相关产品推荐

