使用 AVAudioEngine 点击音频输出

Tap audio output using AVAudioEngine

我正在尝试在我的应用程序上播放的输出音频上安装一个水龙头。我没有问题从麦克风输入中捕获缓冲区,但是当要捕获通过扬声器或听筒或任何输出设备的声音时,它不会成功。我错过了什么吗?

在我的示例中,我试图从 AVPLayer 正在播放的音频文件中捕获音频缓冲区。但是让我们假设我无法直接访问 AVPlayer 实例。

目标是对音频流执行语音识别。

func catchAudioBuffers() throws {
    let audioSession = AVAudioSession.sharedInstance()
    try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: .allowBluetooth)
    try audioSession.setActive(true)

    let outputNode = audioEngine.outputNode

    let recordingFormat = outputNode.outputFormat(forBus: 0)
    outputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
      // PROCESS AUDIO BUFFER
    }

    audioEngine.prepare()
    try audioEngine.start()

    // For example I am playing an audio conversation with an AVPlayer and a local file.
    player.playSound()
}

此代码生成:

AVAEInternal.h:76    required condition is false: [AVAudioIONodeImpl.mm:1057:SetOutputFormat: (_isInput)]
*** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'required condition is false: _isInput'

我遇到了同样的问题,在 2 天的头脑风暴中发现了以下问题。

Apple 表示对于 AVAudioOutputNode,tap 格式必须指定为 nil。我不确定它是否重要,但就我而言,它终于奏效了,格式为零。 您需要开始录制,不要忘记停止录制。

删除tap真的很重要,否则你将有无法打开的文件。

尝试使用您在源文件中使用的相同音频设置保存文件。

这是我终于可以使用的代码。部分取自这个问题 .

    func playSound() {
    let rate: Float? = effect.speed
    let pitch: Float? = effect.pitch
    let echo: Bool? = effect.echo
    let reverb: Bool? = effect.reverb

    // initialize audio engine components
    audioEngine = AVAudioEngine()

    // node for playing audio
    audioPlayerNode = AVAudioPlayerNode()
    audioEngine.attach(audioPlayerNode)

    // node for adjusting rate/pitch
    let changeRatePitchNode = AVAudioUnitTimePitch()
    if let pitch = pitch {
        changeRatePitchNode.pitch = pitch
    }
    if let rate = rate {
        changeRatePitchNode.rate = rate
    }
    audioEngine.attach(changeRatePitchNode)

    // node for echo
    let echoNode = AVAudioUnitDistortion()
    echoNode.loadFactoryPreset(.multiEcho1)
    audioEngine.attach(echoNode)

    // node for reverb
    let reverbNode = AVAudioUnitReverb()
    reverbNode.loadFactoryPreset(.cathedral)
    reverbNode.wetDryMix = 50
    audioEngine.attach(reverbNode)

    // connect nodes
    if echo == true && reverb == true {
        connectAudioNodes(audioPlayerNode, changeRatePitchNode, echoNode, reverbNode, audioEngine.mainMixerNode, audioEngine.outputNode)
    } else if echo == true {
        connectAudioNodes(audioPlayerNode, changeRatePitchNode, echoNode, audioEngine.mainMixerNode, audioEngine.outputNode)
    } else if reverb == true {
        connectAudioNodes(audioPlayerNode, changeRatePitchNode, reverbNode, audioEngine.mainMixerNode, audioEngine.outputNode)
    } else {
        connectAudioNodes(audioPlayerNode, changeRatePitchNode, audioEngine.mainMixerNode, audioEngine.outputNode)
    }

    // schedule to play and start the engine!
    audioPlayerNode.stop()
    audioPlayerNode.scheduleFile(audioFile, at: nil) {
        var delayInSeconds: Double = 0
        if let lastRenderTime = self.audioPlayerNode.lastRenderTime, let playerTime = self.audioPlayerNode.playerTime(forNodeTime: lastRenderTime) {
            if let rate = rate {
                delayInSeconds = Double(self.audioFile.length - playerTime.sampleTime) / Double(self.audioFile.processingFormat.sampleRate) / Double(rate)
            } else {
                delayInSeconds = Double(self.audioFile.length - playerTime.sampleTime) / Double(self.audioFile.processingFormat.sampleRate)
            }
        }

        // schedule a stop timer for when audio finishes playing
        self.stopTimer = Timer(timeInterval: delayInSeconds, target: self, selector: #selector(EditViewController.stopAudio), userInfo: nil, repeats: false)
        RunLoop.main.add(self.stopTimer!, forMode: RunLoop.Mode.default)
    }

    do {
        try audioEngine.start()
    } catch {
        showAlert(Alerts.AudioEngineError, message: String(describing: error))
        return
    }

    //Try to save
    let dirPaths: String = (NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)[0]) + "/sounds/"
    let tmpFileUrl = URL(fileURLWithPath: dirPaths + "effected.caf")

    //Save the tmpFileUrl into global varibale to not lose it (not important if you want to do something else)
    filteredOutputURL = URL(fileURLWithPath: filePath)
        do{
            print(dirPaths)
            let settings = [AVSampleRateKey : NSNumber(value: Float(44100.0)),
            AVFormatIDKey : NSNumber(value: Int32(kAudioFormatMPEG4AAC)),
            AVNumberOfChannelsKey : NSNumber(value: 1),
            AVEncoderAudioQualityKey : NSNumber(value: Int32(AVAudioQuality.medium.rawValue))]
            self.newAudio = try! AVAudioFile(forWriting: tmpFileUrl as URL, settings: settings)
            let length = self.audioFile.length
            audioEngine.mainMixerNode.installTap(onBus: 0, bufferSize: 4096, format: nil) {
                (buffer: AVAudioPCMBuffer?, time: AVAudioTime!) -> Void in
                //Let us know when to stop saving the file, otherwise saving infinitely
                if (self.newAudio.length) <= length {
                    do{
                        try self.newAudio.write(from: buffer!)

                    } catch _{
                        print("Problem Writing Buffer")
                    }
                } else {
                    //if we dont remove it, will keep on tapping infinitely
                    self.audioEngine.mainMixerNode.removeTap(onBus: 0)
                }
            }
    }

    // play the recording!
    audioPlayerNode.play()

}

@objc func stopAudio() {
    if let audioPlayerNode = audioPlayerNode {
        let engine = audioEngine
        audioPlayerNode.stop()
        engine?.mainMixerNode.removeTap(onBus: 0)

    }
    if let stopTimer = stopTimer {
        stopTimer.invalidate()
    }
    configureUI(.notPlaying)
    if let audioEngine = audioEngine {
        audioEngine.stop()
        audioEngine.reset()
    }
    isPlaying = false
}