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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 09:40:59