Формат AVNudioEngine inputNode изменяется при воспроизведении AVAudioPlayerNode

Я начну с простого класса контроллера представления "детская площадка", который демонстрирует мою проблему:

class AudioEnginePlaygroundViewController: UIViewController {
    private var audioEngine: AVAudioEngine!
    private var micTapped = false
    override func viewDidLoad() {
        super.viewDidLoad()
        configureAudioSession()
        audioEngine = AVAudioEngine()
    }

    @IBAction func toggleMicTap(_ sender: Any) {
        guard let mic = audioEngine.inputNode else {
            return
        }
        if micTapped {
            mic.removeTap(onBus: 0)
            micTapped = false
            return
        }
        stopAudioPlayback()

        let micFormat = mic.inputFormat(forBus: 0)
        print("installing tap: \(micFormat.sampleRate) -- \(micFormat.channelCount)")
        mic.installTap(onBus: 0, bufferSize: 2048, format: micFormat) { (buffer, when) in
            print("in tap completion")
            let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
        }
        micTapped = true
        startEngine()
    }

    @IBAction func playAudioFile(_ sender: Any) {
        stopAudioPlayback()
        let playerNode = AVAudioPlayerNode()

        let audioUrl = Bundle.main.url(forResource: "test_audio", withExtension: "wav")!
        let audioFile = readableAudioFileFrom(url: audioUrl)
        audioEngine.attach(playerNode)
        audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
        startEngine()
        playerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
        playerNode.play()
    }

    // MARK: Internal Methods

    private func configureAudioSession() {
        do {
            try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.mixWithOthers, .defaultToSpeaker])
            try AVAudioSession.sharedInstance().setActive(true)
        } catch { }
    }

    private func readableAudioFileFrom(url: URL) -> AVAudioFile {
        var audioFile: AVAudioFile!
        do {
            try audioFile = AVAudioFile(forReading: url)
        } catch { }
        return audioFile
    }

    private func startEngine() {
        guard !audioEngine.isRunning else {
            return
        }

        do {
            try audioEngine.start()
        } catch { }
    }

    private func stopAudioPlayback() {
        audioEngine.stop()
        audioEngine.reset()
    }
}

Вышеупомянутый VC имеет один экземпляр AVAudioEngine и два действия UIButton: одно, которое воспроизводит аудиофайл, найденный по жестко запрограммированному URL, и другое, которое переключает установку / удаление касания на inputNode движка.

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

[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false: [AVAEGraphNode.mm:810:CreateRecordingTap: (IsFormatSampleRateAndChannelCountValid(format))]

что привело меня к проверке данных формата микрофона с помощью оператора log над вызовом installTap. Конечно, при установке крана перед воспроизведением я получаю ожидаемую частоту дискретизации 44100,0 и количество каналов 1. Но когда я сначала воспроизводю аудиофайл, а затем устанавливаю микрофонный сигнал, мой журнал показывает частоту дискретизации 0 и канал количество 2, которое дает мне ошибку, показанную выше.

Я попытался поработать с потоком запуска / сброса AVAudioEngine, попробовал различные комбинации категорий / режимов для моей AVAudioSession (см. Мой метод configureAudioSession) и попытался вручную создать формат нажатия следующим образом:

let micFormat = mic.inputFormat(forBus: 0)
var trueFormat: AVAudioFormat!
if micFormat.sampleRate == 0 {
    trueFormat = AVAudioFormat(standardFormatWithSampleRate: 44100, channels: 1)
} else {
    trueFormat = micFormat
}
print("installing tap: \(micFormat.sampleRate) -- \(micFormat.channelCount)")
mic.installTap(onBus: 0, bufferSize: 2048, format: trueFormat) { (buffer, when) in
    print("in tap completion")
    let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
}

что дает мне похожую, но другую ошибку:

[avae] AVAEInternal.h:70:_AVAE_Check: required condition is false: [AVAudioIONodeImpl.mm:896:SetOutputFormat: (IsFormatSampleRateAndChannelCountValid(hwFormat))]

Любая помощь по этому вопросу будет принята с благодарностью. Я не вижу никакой причины, по которой данные формата микрофона будут различаться в зависимости от того, воспроизводился ли AVAudioPlayerNode.

1 ответ

Решение

После некоторых поисков я нашел проблему. Проблема заключается в синглтоне inputNode аудио движка. Из документов:

Аудио движок создает одиночный по требованию при первом доступе к inputNode. Чтобы получить вход, подключите другой аудиоузел с выхода входного аудио узла или создайте на нем запись касания.

Плюс ссылка на проблему с форматом, с которой я столкнулся:

Проверьте формат ввода входного узла (в частности, аппаратный формат) на ненулевую частоту дискретизации и количество каналов, чтобы увидеть, включен ли ввод.

В моем классе игровой площадки поток для запуска воспроизведения аудиофайла никогда не обращается к inputNode движка, пока не создаст "активную цепочку" с:

audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)

Кажется, что вы должны получить доступ к inputNode AVAudioEngine перед запуском () его, если вы хотите, чтобы механизм внутренне конфигурировал себя для ввода. Даже остановка () и сброс () двигателя не приводят к доступу inputNode для перенастройки двигателя. (Я подозреваю, что ручное прерывание активной цепочки с помощью вызовов disconnectNode позволит выполнить внутреннюю реконфигурацию, но я пока точно не знаю).

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

import UIKit

class AudioEnginePlaygroundViewController: UIViewController {
    private var audioEngine: AVAudioEngine!
    private var mic: AVAudioInputNode!
    private var micTapped = false

    override func viewDidLoad() {
        super.viewDidLoad()
        configureAudioSession()
        audioEngine = AVAudioEngine()
        mic = audioEngine.inputNode!
    }

    @IBAction func toggleMicTap(_ sender: Any) {
        if micTapped {
            mic.removeTap(onBus: 0)
            micTapped = false
            return
        }

        let micFormat = mic.inputFormat(forBus: 0)
        mic.installTap(onBus: 0, bufferSize: 2048, format: micFormat) { (buffer, when) in
            let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
        }
        micTapped = true
        startEngine()
    }

    @IBAction func playAudioFile(_ sender: Any) {
        stopAudioPlayback()
        let playerNode = AVAudioPlayerNode()

        let audioUrl = Bundle.main.url(forResource: "test_audio", withExtension: "wav")!
        let audioFile = readableAudioFileFrom(url: audioUrl)
        audioEngine.attach(playerNode)
        audioEngine.connect(playerNode, to: audioEngine.outputNode, format: audioFile.processingFormat)
        startEngine()
        playerNode.scheduleFile(audioFile, at: nil, completionHandler: nil)
        playerNode.play()
    }

    // MARK: Internal Methods

    private func configureAudioSession() {
        do {
            try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord, with: [.mixWithOthers, .defaultToSpeaker])
            try AVAudioSession.sharedInstance().setActive(true)
        } catch { }
    }

    private func readableAudioFileFrom(url: URL) -> AVAudioFile {
        var audioFile: AVAudioFile!
        do {
            try audioFile = AVAudioFile(forReading: url)
        } catch { }
        return audioFile
    }

    private func startEngine() {
        guard !audioEngine.isRunning else {
            return
        }

        do {
            try audioEngine.start()
        } catch { }
    }

    private func stopAudioPlayback() {
        audioEngine.stop()
        audioEngine.reset()
    }
}
Другие вопросы по тегам