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

重启AVAudioEngine后如何同步多个AVAudioPlayerNode?

解决AVAudioEngine暂停后恢复时多AVAudioPlayerNode同步播放问题

核心思路

引擎暂停后,AVAudioPlayerNode的lastRenderTime会失效,无法依赖它恢复同步。我们需要在暂停时主动记录每个播放器的播放进度,恢复时以引擎全局的渲染时间为基准,让所有播放器从记录的位置同步启动,同时保证控制中心状态正确。

实现步骤

1. 记录暂停时的播放进度

在暂停操作中,先保存每个AVAudioPlayerNode的当前播放位置(采样点),再暂停播放器和引擎:

// 用于存储每个播放器的暂停位置
private var playerPausePositions: [AVAudioPlayerNode: AVAudioFramePosition] = [:]

func pause() {
    // 先记录所有播放器的当前播放位置
    for player in players {
        if let nodeTime = player.lastRenderTime, let playerTime = player.playerTime(forNodeTime: nodeTime) {
            playerPausePositions[player] = playerTime.sampleTime
        }
    }
    
    // 暂停所有播放器
    players.forEach { $0.pause() }
    // 暂停引擎,确保控制中心状态正确
    engine.pause()
}

2. 恢复时同步播放

修改你的play()方法,分两种情况处理:

  • 引擎未运行:启动引擎后,以引擎输出节点的全局渲染时间为基准,让所有播放器从记录的位置同步启动
  • 引擎已运行:沿用你原来的lastRenderTime同步逻辑
func play() {
    if !engine.isRunning {
        do {
            setupAudioSession()
            try engine.start()
            
            // 获取引擎全局的基准渲染时间
            let outputFormat = engine.outputNode.outputFormat(forBus: 0)
            let sampleRate = outputFormat.sampleRate
            
            guard let engineRenderTime = engine.outputNode.lastRenderTime, engineRenderTime.isSampleTimeValid else {
                // 引擎刚启动时若基准时间无效,直接从记录位置启动所有播放器(此时引擎刚启动,天然同步)
                for player in players {
                    if let position = playerPausePositions[player] {
                        player.play(from: position)
                    } else {
                        player.play()
                    }
                }
                playerPausePositions.removeAll()
                return
            }
            
            // 以引擎基准时间为参照,计算每个播放器的同步启动时间
            for player in players {
                let startTime: AVAudioTime
                if let pausePosition = playerPausePositions[player] {
                    // 计算从暂停位置到基准时间的偏移,确保同步启动
                    let playerTime = AVAudioTime(sampleTime: pausePosition, atRate: sampleRate)
                    startTime = engineRenderTime.offset(by: playerTime.timeInterval)
                } else {
                    startTime = engineRenderTime
                }
                player.play(at: startTime)
            }
            
            // 清空暂停位置记录
            playerPausePositions.removeAll()
            
        } catch {
            print(error)
            return
        }
    } else {
        // 引擎已在运行,沿用原同步逻辑
        guard let firstPlayer = players.first, 
              let renderTime = firstPlayer.lastRenderTime, 
              renderTime.isSampleTimeValid else {
            players.forEach { $0.play() }
            return
        }
        
        let startTime = AVAudioTime(sampleTime: renderTime.sampleTime, atRate: sampleRate)
        players.forEach { $0.play(at: startTime) }
    }
}

关键细节说明

  • 为什么用引擎输出节点的lastRenderTime:引擎启动后,输出节点的渲染时间是全局统一的基准,比单个播放器的lastRenderTime更可靠,尤其是刚启动阶段。
  • 暂停时的顺序:必须先记录播放位置,再暂停播放器和引擎,否则lastRenderTime会失效,无法获取有效进度。
  • 控制中心状态:暂停引擎的操作保留,确保控制中心显示正确的播放/暂停状态,同时通过记录进度保证恢复时的同步。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 17:42:09