AVAudioPlayerNode запланированные буферы и изменение аудио маршрута в iOS11
Я вижу различное поведение между iOS 9/10 и iOS 11 для буферов, которые в будущем запланированы на AVAudioPlayerNode, когда происходит изменение аудио маршрута (например, вы подключаете наушники). Кто-нибудь испытывал что-то подобное и как вы решили это? Обратите внимание, что я сообщил об этой проблеме на форуме поддержки Apple AVFoundation почти две недели назад, и получил абсолютно нулевой ответ.
Код, который демонстрирует эту проблему, показан ниже - сначала краткое объяснение: код представляет собой простой цикл, который периодически планирует запуск буфера для воспроизведения в будущем. Процесс запускается путем вызова метода runSequence, который планирует аудиобуфер для воспроизведения в будущем и устанавливает обратный вызов завершения для вложенного метода audioCompleteHandler. Обратный вызов завершения снова вызывает метод runSequence, который планирует другой буфер и поддерживает процесс навеки. Это тот случай, когда всегда есть запланированный буфер, кроме случаев, когда выполняется обработчик завершения. Метод trace в разных местах является внутренним методом печати только при отладке, поэтому его можно игнорировать.
В обработчике уведомлений об изменении аудио маршрута (handleAudioRouteChange), когда новое устройство становится доступным (case .newDeviceAvailable), код перезапускает движок и проигрыватель, повторно активирует аудио сеанс и вызывает runSequence, чтобы запустить цикл заново.
Все это прекрасно работает на iOS 9.3.5 (iPhone 5C) и iOS 10.3.3 (iPhone 6), но не работает на iOS 11.1.1 (iPad Air). Природа ошибки заключается в том, что AVAudioPlayerNode не воспроизводит аудио, а вместо этого сразу вызывает обработчик завершения. Это вызывает безудержную ситуацию. Если я удаляю строку, которая снова запускает цикл (как указано в коде), она отлично работает на iOS 11.1.1, но не работает на iOS 9.3.5 и iOS 10.3.3. Эта ошибка другая: звук просто останавливается, и в отладчике я вижу, что цикл не зацикливается.
Таким образом, возможное объяснение состоит в том, что в iOS 9.x и iOS 10.x будущие запланированные буферы не планируются, когда происходит изменение звукового маршрута, в то время как в iOS 11.x будущие запланированные буферы не являются незапланированными.
Это приводит к двум вопросам: 1. Кто-нибудь видел подобное поведение и каково было решение? 2. Может ли кто-нибудь указать мне документацию, которая описывает точное состояние движка, плеера (ов) и сеанса, когда происходит изменение аудио маршрута (или прерывание звука)?
private func runSequence() {
// For test ony
var timeBaseInfo = mach_timebase_info_data_t()
mach_timebase_info(&timeBaseInfo)
// End for test only
let audioCompleteHandler = { [unowned self] in
DispatchQueue.main.async {
trace(level: .skim, items: "Player: \(self.player1.isPlaying), Engine: \(self.engine.isRunning)")
self.player1.stop()
switch self.runStatus {
case .Run:
self.runSequence()
case .Restart:
self.runStatus = .Run
self.tickSeq.resetSequence()
//self.updateRenderHostTime()
self.runSequence()
case .Halt:
self.stopEngine()
self.player1.stop()
self.activateAudioSession(activate: false)
}
}
}
// Schedule buffer...
if self.engine.isRunning {
if let thisElem: (buffer: AVAudioPCMBuffer, duration: Int) = tickSeq.next() {
self.player1.scheduleBuffer(thisElem.buffer, at: nil, options: [], completionHandler: audioCompleteHandler)
self.player1.prepare(withFrameCount: thisElem.buffer.frameLength)
self.player1.play(at: AVAudioTime(hostTime: self.startHostTime))
self.startHostTime += AVAudioTime.hostTime(forSeconds: TimeInterval(Double(60.0 / Double(self.model.bpm.value)) * Double(thisElem.duration)))
trace(level: .skim, items:
"Samples: \(thisElem.buffer.frameLength)",
"Time: \(mach_absolute_time() * (UInt64(timeBaseInfo.numer) / UInt64(timeBaseInfo.denom))) ",
"Sample Time: \(player1.lastRenderTime!.hostTime)",
"Play At: \(self.startHostTime) ",
"Player: \(self.player1.isPlaying)",
"Engine: \(self.engine.isRunning)")
}
else {
}
}
}
@objc func handleAudioRouteChange(_ notification: Notification) {
trace(level: .skim, items: "Route change: Player: \(self.player1.isPlaying) Engine: \(self.engine.isRunning)")
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else { return }
trace(level: .skim, items: audioSession.currentRoute, audioSession.mode)
trace(level: .none, items: "Reason Value: \(String(describing: userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt)); Reason: \(String(describing: AVAudioSessionRouteChangeReason(rawValue:reasonValue)))")
switch reason {
case .newDeviceAvailable:
trace(level: .skim, items: "In handleAudioRouteChange.newDeviceAvailable")
for output in audioSession.currentRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
startEngine()
player1.play()
activateAudioSession(activate: true)
//updateRenderHostTime()
runSequence() // <<--- Problem: works for iOS9,10; fails on iOS11. Remove it and iOS9,10 fail, works on iOS11
}
case .oldDeviceUnavailable:
trace(level: .skim, items: "In handleAudioRouteChange.oldDeviceUnavailable")
if let previousRoute =
userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
player1.stop()
stopEngine()
tickSeq.resetSequence()
DispatchQueue.main.async {
if let pp = self.playPause as UIButton? { pp.isSelected = false }
}
}
}
1 ответ
Итак, проблема решена дальнейшим копанием / тестированием:
- Существует разница в поведении между iOS 9/10 и iOS 11, когда AVAudioSession выдает уведомление об изменении маршрута. В обработчике уведомлений состояние движка не работает (engine.isRunning == false) около 90% времени для iOS 9/10, тогда как для iOS 11 состояние движка всегда работает (engine.isRunning == true)
- В 10% случаев, когда iOS 9/10 указывает, что движок работает (engine.isRunning == true), это на самом деле не так. Двигатель НЕ работает независимо от того, что говорит engine.isRunning
Поскольку движок был остановлен в iOS 9/10, ранее подготовленное аудио было выпущено, и аудио не запустится только после перезапуска движка; Вы должны перепланировать файл или буфер в точке выборки, где двигатель был остановлен. К сожалению, вы не можете найти текущее время выборки, когда двигатель остановлен (игрок возвращает ноль), поэтому вы должны:
- Запустить двигатель
- Возьмите время выборки и накапливайте его (+=) в постоянном свойстве
- Остановить игрока
- Перепланируйте звук (и подготовьте его), начиная с только что взятого образца
- Запустить плеер
Состояние движка в iOS 9/10 одинаково как для подключенного к корпусу наушников (.newDeviceAvailable), так и для удаленного кейса для наушников (.oldDeviceUnavailable), поэтому вам необходимо выполнить аналогичные действия и для удаленного кейса (накапливая образец необходимо время, чтобы вы могли перезапустить звук с того места, где он был остановлен, так как player.stop() сбросит время сэмплирования на 0)
- Ничего из этого не требуется для iOS 11, но приведенный ниже код работает для iOS 9/10 и 11, поэтому, вероятно, лучше сделать это одинаково для всех версий
Приведенный ниже код работает на моих тестовых устройствах для iOS 9.3.5 (iPhone 5C), iOS 10.3.3 (iPhone 6) и iOS 11.1.1 (iPad Air) (но меня все еще беспокоит тот факт, что я не могу найти ранее комментарий о том, как правильно обрабатывать изменение маршрута, и, должно быть, сотни людей, которые сталкивались с этой проблемой. Обычно, когда я не могу найти какой-либо предварительный комментарий по теме, я предполагаю, что я делаю что-то неправильно или просто не понимаю... ну что ж...)
@objc func handleAudioRouteChange(_ notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSessionRouteChangeReason(rawValue:reasonValue) else { return }
switch reason {
case .newDeviceAvailable:
for output in audioSession.currentRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
headphonesConnected = true
}
startEngine() // Do this regardless of whether engine.isRunning == true
if let lrt = player.lastRenderTime, let st = player.playerTime(forNodeTime: lrt)?.sampleTime {
playSampleOffset += st // Accumulate so that multiple inserts/removals move the play point forward
stopPlayer()
scheduleSegment(file: playFile, at: nil, player: player, start: playSampleOffset, length: AVAudioFrameCount(playFile.length - playSampleOffset))
startPlayer()
}
else {
// Unknown problem with getting sampleTime; reset engine, player(s), restart as appropriate
}
case .oldDeviceUnavailable:
if let previousRoute =
userInfo[AVAudioSessionRouteChangePreviousRouteKey] as? AVAudioSessionRouteDescription {
for output in previousRoute.outputs where output.portType == AVAudioSessionPortHeadphones {
headphonesConnected = false
}
}
startEngine() // Do this regardless of whether engine.isRunning == true
if let lrt = player.lastRenderTime, let st = player.playerTime(forNodeTime: lrt)?.sampleTime {
playSampleOffset += st // Accumulate...
stopPlayer()
scheduleSegment(file: playFile, at: nil, player: player, start: playSampleOffset, length: AVAudioFrameCount(playFile.length - playSampleOffset))
startPlayer() // Test only, in reality don't restart here; set play control to allow user to start audio
}
else {
// Unknown problem with getting sampleTime; reset engine, player(s), restart as appropriate
}
...