重启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
相关产品推荐
相关产品推荐

