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

如何用AVMIDIPlayer循环播放MIDI文件?解决延迟与音频截断问题

解决AVMIDIPlayer循环播放延迟与音频截断问题

问题说明

使用AVMIDIPlayer的完成回调实现MIDI文件循环播放时,循环间隙出现微小延迟与音频截断现象,以下是针对性解决方案:

核心解决方案

1. 优化音频会话与播放准备

  • 配置音频会话为播放优先模式,避免系统音频策略干扰播放连续性
  • 确保AVMIDIPlayer完全完成预加载后再启动播放,避免因资源未就绪导致的截断

2. 替换回调式循环为精确进度调度

依赖play()的完成回调会因主线程调度延迟导致间隙,改为监听播放进度,在曲目接近结束时提前重置播放位置,实现无缝衔接

3. 双实例交替预加载(可选,极端场景)

对于对无缝要求极高的场景,使用两个AVMIDIPlayer实例交替预加载与播放,彻底消除切换间隙

修改后的完整代码

import SwiftUI
import AVFoundation

struct ContentView: View {
    @StateObject private var midiPlayer = MIDIPlayer()
       
    var body: some View {
        VStack {
            Button(action: {
                midiPlayer.startLooping()
            }) {
                Text("播放MIDI")
            }
            Button(action: {
                midiPlayer.stopLooping()
            }) {
                Text("停止播放")
            }
        }
        .onAppear {
            midiPlayer.configureAudioSession()
        }
    }
}

class MIDIPlayer: ObservableObject {
    private var midiPlayer: AVMIDIPlayer?
    private var progressTimer: Timer?
    private var isLooping: Bool = false
    // 提前触发重置的阈值(单位:秒,根据曲目长度调整)
    private let loopThreshold: TimeInterval = 0.1
       
    init() {
        loadMIDI()
    }
    
    func configureAudioSession() {
        do {
            let session = AVAudioSession.sharedInstance()
            try session.setCategory(.playback, mode: .default)
            try session.setActive(true)
        } catch {
            print("音频会话配置失败: \(error)")
        }
    }
    
    func loadMIDI() {
        guard let midiFileURL = Bundle.main.url(forResource: "cdec", withExtension: "mid") else {
            print("未找到MIDI文件")
            return
        }
        
        do {
            let soundBankURL = Bundle.main.url(forResource: "Nokia", withExtension: "sf2")
            midiPlayer = try AVMIDIPlayer(contentsOf: midiFileURL, soundBankURL: soundBankURL)
            // 等待预加载完成
            midiPlayer?.prepareToPlay()
        } catch {
            print("加载MIDI文件失败: \(error)")
        }
    }
    
    func startLooping() {
        guard let player = midiPlayer else { return }
        isLooping = true
        player.currentPosition = 0
        player.play()
        // 启动进度监听定时器
        startProgressMonitoring()
    }
    
    func stopLooping() {
        isLooping = false
        progressTimer?.invalidate()
        progressTimer = nil
        midiPlayer?.stop()
        midiPlayer?.currentPosition = 0
    }
    
    private func startProgressMonitoring() {
        progressTimer?.invalidate()
        // 每0.05秒检查一次播放进度
        progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { [weak self] _ in
            guard let self = self, let player = self.midiPlayer, self.isLooping else {
                self?.progressTimer?.invalidate()
                return
            }
            
            let remainingTime = player.duration - player.currentPosition
            // 当剩余时间小于阈值时,重置播放位置
            if remainingTime <= self.loopThreshold {
                player.currentPosition = 0
            }
        }
    }
}

代码说明

  • 音频会话配置:设置为playback类别,确保后台播放优先级,避免系统自动暂停或降低音量
  • 进度监听:通过定时器高频检查播放进度,在曲目接近结束时提前重置位置,避免回调延迟
  • 阈值调整:loopThreshold可根据MIDI曲目长度微调,推荐设置为0.1-0.2秒,平衡无缝性与准确性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 23:43:12