завершениеHandler AVAudioPlayerNode.scheduleFile() вызывается слишком рано

Я пытаюсь использовать новый AVAudioEngine в iOS 8.

Похоже, что завершениеHandler player.scheduleFile() вызывается до завершения воспроизведения звукового файла.

Я использую звуковой файл длиной 5 секунд, и сообщение println() появляется примерно за 1 секунду до окончания звука.

Я делаю что-то не так или неправильно понимаю идею завершенияHandler?

Спасибо!


Вот код:

class SoundHandler {
    let engine:AVAudioEngine
    let player:AVAudioPlayerNode
    let mainMixer:AVAudioMixerNode

    init() {
        engine = AVAudioEngine()
        player = AVAudioPlayerNode()
        engine.attachNode(player)
        mainMixer = engine.mainMixerNode

        var error:NSError?
        if !engine.startAndReturnError(&error) {
            if let e = error {
                println("error \(e.localizedDescription)")
            }
        }

        engine.connect(player, to: mainMixer, format: mainMixer.outputFormatForBus(0))
    }

    func playSound() {
        var soundUrl = NSBundle.mainBundle().URLForResource("Test", withExtension: "m4a")
        var soundFile = AVAudioFile(forReading: soundUrl, error: nil)

        player.scheduleFile(soundFile, atTime: nil, completionHandler: { println("Finished!") })

        player.play()
    }
}

person Oliver    schedule 03.04.2015    source источник


Ответы (6)


Документы AVAudioEngine из дней iOS 8, должно быть, просто неверны. Между тем, в качестве обходного пути я заметил, что если вы вместо этого используете scheduleBuffer:atTime:options:completionHandler:, обратный вызов запускается, как и ожидалось (после завершения воспроизведения).

Пример кода:

AVAudioFile *file = [[AVAudioFile alloc] initForReading:_fileURL commonFormat:AVAudioPCMFormatFloat32 interleaved:NO error:nil];
AVAudioPCMBuffer *buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:file.processingFormat frameCapacity:(AVAudioFrameCount)file.length];
[file readIntoBuffer:buffer error:&error];

[_player scheduleBuffer:buffer atTime:nil options:AVAudioPlayerNodeBufferInterrupts completionHandler:^{
    // reminder: we're not on the main thread in here
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"done playing, as expected!");
    });
}];
person taber    schedule 14.04.2015
comment
Любить это. Работает как шарм! - person Next Developer; 01.02.2016
comment
Собственно, после тестирования оказалось, что даже с буфером callback вызывается до остановки плеера. audioPlayer.scheduleBuffer(audioBuffer){ dispatch_async(dispatch_get_main_queue()) { [unowned self] in if (self.audioPlayer.playing == false){ self.stopButton.hidden = true } } } в этом примере условие никогда не выполняется - person Next Developer; 01.02.2016
comment
Странно то, что мой AVAudioPlayerNode воспроизводит звук на iOS9, но не работает на некоторых старых устройствах и устройствах под управлением iOS8. - person vikzilla; 20.02.2016
comment
Кто-то действительно зарегистрировал ошибку для этого? Могу сделать, если надо. - person lancejabr; 17.02.2017
comment
@lancejabr Я сделал, но ты тоже сможешь! чем больше сообщений об ошибках они получают, тем больше вероятность, что они это исправят. - person taber; 17.02.2017
comment
Хорошее решение, но вы теряете всех пользователей с устройствами до iOS 11. - person bio; 03.05.2018
comment
@био как так? Похоже, этот метод существует с iOS 8: developer.apple .com/documentation/avfoundation/ - person taber; 24.05.2018
comment
Извините, перепутал с методом scheduleFile с аналогичными параметрами. - person bio; 25.05.2018
comment
как я могу использовать этот метод в быстром любом образце кода? - person RAM; 18.09.2018
comment
Для любых будущих искателей обязательно прочитайте ответ arlomedia ниже. Этот ответ неверен; здесь нет ошибки. Обратный вызов выполняется, когда он должен. Вызывается после того, как проигрыватель запланировал воспроизведение буфера в потоке рендеринга или после остановки проигрывателя. Он вызывается, когда составлено расписание, а не когда завершено воспроизведение. Но ответ arlomedia включает в себя то, что вы, вероятно, ищете (AVAudioPlayerNodeCompletionDataPlayedBack). - person Rob Napier; 14.01.2020

Я вижу такое же поведение.

Исходя из моих экспериментов, я считаю, что обратный вызов вызывается после того, как буфер/сегмент/файл был «запланирован», а не после завершения воспроизведения.

Хотя в документах прямо указано: «Вызывается после того, как буфер полностью воспроизведен или проигрыватель остановлен. Может быть равен нулю».

Поэтому я думаю, что это либо ошибка, либо неправильная документация. понятия не имею какой

person Alan Queen    schedule 06.04.2015
comment
Тем временем он изменился на Called после того, как проигрыватель запланировал воспроизведение файла в потоке рендеринга или проигрыватель был остановлен. Может быть ноль.» — не уверен, включает ли это естественное завершение игрока. - person bio; 03.05.2018

Вы всегда можете рассчитать будущее время завершения воспроизведения звука, используя AVAudioTime. Текущее поведение полезно, поскольку оно поддерживает планирование дополнительных буферов/сегментов/файлов для воспроизведения из обратного вызова до завершения текущего буфера/сегмента/файла, избегая пауз в воспроизведении звука. Это позволяет вам создать простой луп-плеер без особых усилий. Вот пример:

class Latch {
    var value : Bool = true
}

func loopWholeFile(file : AVAudioFile, player : AVAudioPlayerNode) -> Latch {
    let looping = Latch()
    let frames = file.length

    let sampleRate = file.processingFormat.sampleRate
    var segmentTime : AVAudioFramePosition = 0
    var segmentCompletion : AVAudioNodeCompletionHandler!
    segmentCompletion = {
        if looping.value {
            segmentTime += frames
            player.scheduleFile(file, atTime: AVAudioTime(sampleTime: segmentTime, atRate: sampleRate), completionHandler: segmentCompletion)
        }
    }
    player.scheduleFile(file, atTime: AVAudioTime(sampleTime: segmentTime, atRate: sampleRate), completionHandler: segmentCompletion)
    segmentCompletion()
    player.play()

    return looping
}

Приведенный выше код дважды планирует весь файл перед вызовом player.play(). По мере того, как каждый сегмент приближается к завершению, он планирует следующий файл в будущем, чтобы избежать перерывов в воспроизведении. Чтобы остановить зацикливание, вы используете возвращаемое значение Latch, например:

let looping = loopWholeFile(file, player)
sleep(1000)
looping.value = false
player.stop()
person Patrick Beard    schedule 15.03.2016

Мой отчет об ошибке для этого был закрыт как «работает по назначению», но Apple указала мне на новые варианты методов scheduleFile, scheduleSegment и scheduleBuffer в iOS 11. Они добавляют аргумент завершенияCallbackType, который вы можете использовать, чтобы указать, что вы хотите обратный вызов завершения. когда воспроизведение завершено:

[self.audioUnitPlayer
            scheduleSegment:self.audioUnitFile
            startingFrame:sampleTime
            frameCount:(int)sampleLength
            atTime:0
            completionCallbackType:AVAudioPlayerNodeCompletionDataPlayedBack
            completionHandler:^(AVAudioPlayerNodeCompletionCallbackType callbackType) {
    // do something here
}];

В документации ничего не говорится о том, как это работает, но я проверил это, и это работает для меня.

Я использовал этот обходной путь для iOS 8-10:

- (void)playRecording {
    [self.audioUnitPlayer scheduleSegment:self.audioUnitFile startingFrame:sampleTime frameCount:(int)sampleLength atTime:0 completionHandler:^() {
        float totalTime = [self recordingDuration];
        float elapsedTime = [self recordingCurrentTime];
        float remainingTime = totalTime - elapsedTime;
        [self performSelector:@selector(doSomethingHere) withObject:nil afterDelay:remainingTime];
    }];
}

- (float)recordingDuration {
    float duration = duration = self.audioUnitFile.length / self.audioUnitFile.processingFormat.sampleRate;
    if (isnan(duration)) {
        duration = 0;
    }
    return duration;
}

- (float)recordingCurrentTime {
    AVAudioTime *nodeTime = self.audioUnitPlayer.lastRenderTime;
    AVAudioTime *playerTime = [self.audioUnitPlayer playerTimeForNodeTime:nodeTime];
    AVAudioFramePosition sampleTime = playerTime.sampleTime;
    if (sampleTime == 0) { return self.audioUnitLastKnownTime; } // this happens when the player isn't playing
    sampleTime += self.audioUnitStartingFrame; // if we trimmed from the start, or changed the location with the location slider, the time before that point won't be included in the player time, so we have to track it ourselves and add it here
    float time = sampleTime / self.audioUnitFile.processingFormat.sampleRate;
    self.audioUnitLastKnownTime = time;
    return time;
}
person arlomedia    schedule 18.11.2017

На сегодняшний день в проекте с целью развертывания 12.4 на устройстве под управлением 12.4.1 мы нашли способ успешно остановить узлы после завершения воспроизведения:

// audioFile and playerNode created here ...

playerNode.scheduleFile(audioFile, at: nil, completionCallbackType: .dataPlayedBack) { _ in
    os_log(.debug, log: self.log, "%@", "Completing playing sound effect: \(filePath) ...")

    DispatchQueue.main.async {
        os_log(.debug, log: self.log, "%@", "... now actually completed: \(filePath)")

        self.engine.disconnectNodeOutput(playerNode)
        self.engine.detach(playerNode)
    }
}

Основное отличие w.r.t. предыдущие ответы заключались в том, чтобы отложить отсоединение узла в основном потоке (который, я думаю, также является потоком рендеринга звука?), Вместо того, чтобы выполнять это в потоке обратного вызова.

person superjos    schedule 11.09.2019
comment
.dataPlayedBack у меня тоже работает. - person Pixel; 15.02.2021

Да, он вызывается немного раньше, чем файл (или буфер) завершается. Если вы вызовете [myNode stop] из обработчика завершения, файл (или буфер) не будет полностью завершен. Однако, если вы вызовете [myEngine stop], файл (или буфер) будет завершен до конца.

person Andrew Coad    schedule 12.03.2017