iOS - AVAudioPlayerNode.play () выполняется очень медленно - PullRequest
0 голосов
/ 29 сентября 2019

Я использую AVAudioEngine для аудио в игровом приложении iOS. Проблема, с которой я столкнулся, заключается в том, что AVAudioPlayerNode.play () занимает много времени для выполнения, что может быть проблемой в приложениях реального времени, таких как игры.

play () просто активирует узел игрока - выне нужно звонить каждый раз, когда вы играете звук. Как таковой, он не должен вызываться так часто, но он должен вызываться время от времени, например, чтобы активировать игрока изначально или после того, как он был деактивирован (что происходит в некоторых ситуациях). Даже если время от времени вызывается, длительное время выполнения может быть проблемой, особенно если вам нужно вызывать play () сразу для нескольких игроков.

Время выполнения play () кажется пропорциональным значениюAVAudioSession.ioBufferDuration, которую вы можете запросить об изменении, используя AVAudioSession.setPreferredIOBufferDuration (). Вот некоторый код, который я использую для проверки этого:

import AVFoundation
import UIKit

class ViewController: UIViewController {
    private let engine = AVAudioEngine()
    private let player = AVAudioPlayerNode()
    private let ioBufferSize = 1024.0 // Or 256.0

    override func viewDidLoad() {
        super.viewDidLoad()

        let audioSession = AVAudioSession.sharedInstance()

        try! audioSession.setPreferredIOBufferDuration(ioBufferSize / 44100.0)
        try! audioSession.setActive(true)

        engine.attach(player)
        engine.connect(player, to: engine.mainMixerNode, format: nil)

        try! engine.start()

        print("IO buffer duration: \(audioSession.ioBufferDuration)")
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if player.isPlaying {
            player.stop()
        } else {
            let startTime = CACurrentMediaTime()
            player.play()
            let endTime = CACurrentMediaTime()

            print("\(endTime - startTime)")
        }
    }
}

Вот несколько примеров таймингов для play (), которые я получил, используя размер буфера 1024 (который я считаю значением по умолчанию):

0.0218
0.0147
0.0211
0.0160
0.0184
0.0194
0.0129
0.0160

Вот некоторые примеры таймингов с использованием размера буфера 256:

0.0014
0.0029
0.0033
0.0023
0.0030
0.0039
0.0031
0.0032

Как вы можете видеть выше, для размера буфера 1024 время выполнения обычно составляет 15-20Диапазон мс (около полного кадра при 60 FPS). С размером буфера 256 это составляет около 3 мс - не так уж плохо, но все же дорого, когда у вас есть только ~ 17 мс на кадр для работы.

Это на iPad Mini 2 под управлением iOS 12.4.2,Это, очевидно, старое устройство, но результаты, которые я вижу на симуляторе, кажутся одинаково пропорциональными, поэтому, похоже, он больше связан с размером буфера и поведением самой функции, чем с используемым оборудованием. Я не знаю, что происходит под капотом, но кажется возможным, что блоки play () до начала следующего звукового цикла или что-то в этом роде.

Запрос меньшего размера буфера выглядит как частичныйрешение, но есть некоторые потенциальные недостатки. Согласно документации здесь , меньший размер буфера может означать больший доступ к диску при потоковой передаче из файла, и, независимо от этого, запрос может не выполняться вообще. Также, здесь , кто-то сообщает о проблемах воспроизведения, связанных с малым размером буфера. Принимая все это во внимание, я не склонен рассматривать это как решение.

Это оставляет мне время выполнения play () в диапазоне 15-20 мс, что обычно означает пропущенный кадр при 60 FPS. ,Если я устрою так, что за один раз будет сделан только один вызов play (), и только изредка, может быть, это будет незаметно, но это не идеально.

Я искал информацию и спрашивалоб этом в других местах, но, похоже, на практике такое поведение встречается не у многих, или для них это не проблема.

AVAudioEngine предназначен для использования в приложениях реального времени, так что если яПравильно, что AVAudioPlayerNode.play () блокируется в течение значительного времени, пропорционального размеру буфера, что кажется проблемой проектирования. Я понимаю, что, вероятно, это не та проблема, с которой сталкиваются многие, но я пишу здесь, чтобы спросить, сталкивался ли кто-либо с этой конкретной проблемой с AVAudioEngine, и если да, то есть ли какие-либо идеи, предложения или обходные пути, которые кто-либо может предложить.

1 Ответ

0 голосов
/ 01 ноября 2019

Я исследовал это довольно тщательно. Вот мои выводы.

Теперь я проверил поведение на различных устройствах и версиях iOS (включая последнюю версию на момент написания статьи, 13.2), а также заставил других протестировать это, мой текущийвывод заключается в том, что для AVAudioPlayerNode.play() характерны длительные времена выполнения и очевидного обходного пути нет. Как отмечалось в моем первоначальном посте, время выполнения может быть уменьшено путем запроса более низкой длительности буфера, но, как уже говорилось ранее, это не похоже на жизнеспособное решение.

Я слышал из достоверного источника, что вызов play() в фоновом потоке (например, с использованием Grand Central Dispatch) должно быть безопасным, и действительно, это был бы один из способов решения проблемы. Однако, хотя технически может быть безопасно вызывать play() (или другие AVAudioEngine связанные функции) в разных потоках, я скептически отношусь к тому, является ли это хорошей идеей (дальнейшее объяснение ниже).

В документации не говорится об этом, насколько я могу судить, но AVAudioEngine будет выбрасывать NSException при различных обстоятельствах, что без специальной обработки приведет к закрытию приложения в Swift.

Одна из вещей, которая вызовет выброс NSException, - это если вы позвоните AVAudioPlayerNode.play(), когда двигатель не работает. Очевидно, что если у вас есть только свой собственный код для беспокойства, вы можете принять меры, чтобы этого не происходило.

Однако сама iOS иногда останавливает механизм самостоятельно, например, при прерывании звукапроисходит. Если после этого и до перезапуска двигателя вы наберете play(), будет выдано NSException. Эту ошибку довольно легко избежать, если все ваши вызовы play() находятся в главном потоке, но многопоточность усложняет проблему и, похоже, может привести к случайному вызову play() после остановки двигателя. Хотя могут быть способы обойти это, многопоточность, кажется, представляет нежелательную сложность и хрупкость, поэтому я решил не использовать ее.

Моя текущая стратегия заключается в следующем. По причинам, рассмотренным ранее, я не использую многопоточность. Вместо этого я делаю все возможное, чтобы уменьшить количество вызовов до play(), как в целом, так и для каждого кадра. Это включает, помимо прочего, только поддержку стереозвука (по разным причинам поддержка как моно, так и стерео может привести к увеличению количества вызовов на play(), что нежелательно).

Наконец, я также исследовал альтернативы AVAudioEngine. OpenAL по-прежнему поддерживается на iOS, но не рекомендуется. Индивидуальная реализация с использованием низкоуровневых API, таких как Audio Queue Services или Audio Units, была бы возможной, но была бы нетривиальной. Я также рассмотрел некоторые решения с открытым исходным кодом, но варианты, которые я рассмотрел, используют AVAudioEngine под капотом и поэтому страдают от тех же проблем и / или имеют свои собственные недостатки или ограничения. Конечно, есть и коммерческие варианты, которые могут предоставить решение для некоторых разработчиков.

...