Change Audio input programmatically doesn’t change Audio engine input AVFoundation.

huangapple go评论58阅读模式
英文:

Change Audio input programmatically doesn't change Audio engine input AVFoundation

问题

我试图在所有可用设备之间更改音频输入(麦克风),使用 AVAudioSession.sharedInstance().availableInputs 来实现。我使用 AVAudioSession.routeChangeNotification 来在设备连接/断开时获取自动路由更改,并使用 setPreferredInput 来更改首选输入,然后重新启动我的 audioEngine,这样可以正常工作。

但是,当我尝试以编程方式更改首选输入时,它不会更改音频捕获输入节点(inputNode),而会保留上次连接的设备并继续捕获。即使 AVAudioSession.sharedInstance().currentRoute.inputs 更改了,但 audioEngine?.inputNodesetPreferredInput 调用后没有更改。

WhatsApp 似乎已经成功实现了这一点。

有任何建议或线索都会非常感激。谢谢。

以下是一些相关的代码片段。enableMic 在编程上是问题的部分。

private var audioEngine: AVAudioEngine?
private var inputNode: AVAudioNode!

private func setupAudioSession() {
    do {
        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.playAndRecord, options: [.defaultToSpeaker, .allowBluetooth])
        try session.setActive(true)
    } catch {
        print(error)
    }
}

func setupAudioEngine() {
    audioEngine?.stop()
    audioEngine?.reset()

    setupAudioSession()

    audioEngine = AVAudioEngine()
    inputNode = audioEngine?.inputNode

    guard let hardwareSampleRate = audioEngine?.inputNode.inputFormat(forBus: 0).sampleRate else { return }

    let format = AVAudioFormat(standardFormatWithSampleRate: hardwareSampleRate, channels: 1)

    let requiredBufferSize: AVAudioFrameCount = 4800

    inputNode.installTap(onBus: 0,
                         bufferSize: requiredBufferSize,
                         format: format
    ) { [self] (buffer: AVAudioPCMBuffer, _: AVAudioTime) in
        // 处理缓冲区
    }

    audioEngine?.prepare()
}

func startRecording() {
    do {
        try audioEngine?.start()
    } catch {
        print("Could not start audioEngine: \(error)")
    }
}

private func observeRouteChanges() {
    NotificationCenter.default.addObserver(self,
                                           selector: #selector(handleRouteChange),
                                           name: AVAudioSession.routeChangeNotification,
                                           object: nil)
}

@objc func handleRouteChange(notification: Notification) {
    guard let userInfo = notification.userInfo,
          let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
          let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
        return
    }

    switch reason {

    case .newDeviceAvailable, .oldDeviceUnavailable:
        // 使用当前路由 AVAudioSession.sharedInstance().currentRoute.inputs.first 启用麦克风

    default: ()
    }
}

private func enableMic(mic: AVAudioSessionPortDescription?) {
    let session = AVAudioSession.sharedInstance()
    do {
        try session.setPreferredInput(mic)
        // 重新设置音频引擎并启动
    } catch {
        print(error)
    }
}

这是代码的翻译部分。如有任何其他问题,请随时提出。

英文:

I'm trying to change the audio input (microphone) between all the available devices from AVAudioSession.sharedInstance().availableInputs. I'm using AVAudioSession.routeChangeNotification to get automatic route changes when devices get connected/disconnected and change the preferred input with setPreferredInput, then I restart my audioEngine and it works fine.

But when I try to change the preferred input programmatically It doesn't change the audio capture inputNode. But keeps the last connected device and capturing.

Even the AVAudioSession.sharedInstance().currentRoute.inputs changes but the audioEngine?.inputNode doesn't change to setPreferredInput call.

WhatsApp seems to have done that without any issues.
Change Audio input programmatically doesn’t change Audio engine input AVFoundation.

Any suggestions or leads are highly appreciated. Thanks.

These are some related code segments. enableMic programmatically is the issue.

    private var audioEngine: AVAudioEngine?
private var inputNode: AVAudioNode!
private func setupAudioSession() {
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, options: [.defaultToSpeaker, .allowBluetooth])
try session.setActive(true)
} catch {
print(error)
}
}
func setupAudioEngine() {
audioEngine?.stop()
audioEngine?.reset()
setupAudioSession()
audioEngine = AVAudioEngine()
inputNode = audioEngine?.inputNode
guard let hardwareSampleRate = audioEngine?.inputNode.inputFormat(forBus: 0).sampleRate else { return }
let format = AVAudioFormat(standardFormatWithSampleRate: hardwareSampleRate, channels: 1)
let requiredBufferSize: AVAudioFrameCount = 4800
inputNode.installTap(onBus: 0,
bufferSize: requiredBufferSize,
format: format
) { [self] (buffer: AVAudioPCMBuffer, _: AVAudioTime) in
// process the buffer
}
audioEngine?.prepare()
}
func startRecording() {
do {
try audioEngine?.start()
} catch {
print("Could not start audioEngine: \(error)")
}
}
private func observeRouteChanges() {
NotificationCenter.default.addObserver(self,
selector: #selector(handleRouteChange),
name: AVAudioSession.routeChangeNotification,
object: nil)
}
@objc func handleRouteChange(notification: Notification) {
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else {
return
}
switch reason {
case .newDeviceAvailable, .oldDeviceUnavailable:
// enableMic with current route AVAudioSession.sharedInstance().currentRoute.inputs.first
default: ()
}
}
private func enableMic(mic: AVAudioSessionPortDescription?) {
let session = AVAudioSession.sharedInstance()
do {
try session.setPreferredInput(mic)
// re-setup the audio engine and start
} catch {
print(error)
}
}

答案1

得分: 1

以下是翻译好的部分:

"Also new to AVFoundation. For your question"(对于你的问题,AVFoundation也是新的。)

"> Change Audio input programmatically doesn't change Audio engine input AVFoundation? Yes, it does not change the inputNode property even if you programmatically change the input device."(> 通过编程方式更改音频输入不会更改AVFoundation的音频引擎输入吗?是的,即使你以编程方式更改输入设备,它也不会更改inputNode属性。)

"Please take at Apple's documentation on this property:"(请查看苹果的文档关于这个属性:)

"The inputNode does not change. That's expected. The inputNode property of AVAudioEngine is a computed property, and it is evaluated lazily(on demand) when it is first accessed."(inputNode不会改变。这是预期的。AVAudioEngineinputNode属性是一个计算属性,当第一次访问它时,它会被延迟(按需)评估。)

"When you access the inputNode property for the first time, the AVAudioEngine creates a singleton instance of the AVAudioInputNode class – using the AVAudioEngine's default AVAudioNode render format – to represent the hardware's audio input connection. This instance handles audio input in the audio engine's processing graph, and the audio input format is automatically determined by the properties of your audio hardware."(当你第一次访问inputNode属性时,AVAudioEngine会创建AVAudioInputNode类的单例实例,使用AVAudioEngine的默认AVAudioNode渲染格式来表示硬件的音频输入连接。这个实例在音频引擎的处理图中处理音频输入,并且音频输入格式会根据你的音频硬件的属性自动确定。)

"Once the node is created, subsequent calls to input node will return the same instance."(一旦节点被创建,后续对input node的调用将返回相同的实例。)

"And to prove that I wrote a demo app, I think you have tried it. Take a look at this method, which is used to start tapping input node:"(为了证明我写了一个演示应用程序,我认为你已经尝试过了。看一下这个用于开始捕捉输入节点的方法:)

"public func startMonitoring(){ ... }"(public func startMonitoring(){ ... }

"If you try to call this method after the AVAudioEngineConfigurationChange notification, you will find the app crashes at audioEngine.inputNode.installTap(onBus:, bufferSize:,format:), especially when you are switching between AirPods and built-in microphone. That's because the inputNode does not update when the input device is physically changed. Just like the documentation says, 'it is created on demand when first accessed'."(如果你尝试在AVAudioEngineConfigurationChange通知后调用这个方法,你会发现在audioEngine.inputNode.installTap(onBus:, bufferSize:,format:)处应用程序会崩溃,特别是当你在AirPods和内置麦克风之间切换时。这是因为当输入设备物理更改时,inputNode不会更新。就像文档所说的那样,它在第一次访问时按需创建。)

"I hope that clarifies how the lazy instantiation works for the inputNode property of AVAudioEngine. Let me know if you have any further questions."(我希望这解释了AVAudioEngine的inputNode属性的惰性实例化是如何工作的。如果你有任何进一步的问题,请告诉我。)

"BTW, here is the demo repo link. The demo both have automatic audio input switching and manual audio input switch."(顺便说一下,这是演示的存储库链接link。演示同时具有自动音频输入切换和手动音频输入切换。)

请注意,以上是翻译好的内容,不包括代码部分。

英文:

Also new to AVFoundation. For your question

> Change Audio input programmatically doesn't change Audio engine input AVFoundation?
Yes, it does not change the inputNode property even if you programmatically change the input device.

Please take at Apple's documentation on this property:
Change Audio input programmatically doesn’t change Audio engine input AVFoundation.

The inputNode does not change. That's expected. The inputNode property of AVAudioEngine is a computed property, and it is evaluated lazily(on demand) when it is first accessed.

When you access the inputNode property for the first time, the AVAudioEngine creates a singleton instance of the AVAudioInputNode class – using the AVAudioEngine's default AVAudioNode render format – to represent the hardware's audio input connection. This instance handles audio input in the audio engine's processing graph, and the audio input format is automatically determined by the properties of your audio hardware.

Once the node is created, subsequent calls to input node will return the same instance.

And to prove that I wrote a demo app, I think you have tried it.
Take a look at this method, which is used to start tapping input node:

public func startMonitoring(){
/* with out this code, installTap will crash due to 'required condition is false: format.sampleRate == hwFormat.sampleRate' install tap
audioEngine = AVAudioEngine()
*/
let inputNode = audioEngine.inputNode
let inputFormat = inputNode.outputFormat(forBus: 0)
// Install a tap on the audio engine with the buffer size and the input format.
debugPrint("inputFormat:",inputFormat)
audioEngine.inputNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(bufferSize), format: inputFormat) { buffer, _ in
self.audioMetering(buffer: buffer)
}
audioEngine.prepare()
do {
try audioEngine.start()
} catch {
debugPrint("\(error.localizedDescription)")
}
}

If you try to call this method after the AVAudioEngineConfigurationChange notification, you will find the app crashes at audioEngine.inputNode.installTap(onBus:, bufferSize:,format:), especially when you are switching between AirPods and built-in microphone. That's because the inputNode does not update when the input device is physically changed. Just like the documentation says, 'it is created on demand when first accessed'.

I hope that clarifies how the lazy instantiation works for the inputNode property of AVAudioEngine. Let me know if you have any further questions.

BTW, here is the demo repo link. The demo both have automatic audio input switching and manual audio input switch.

Change Audio input programmatically doesn’t change Audio engine input AVFoundation.

huangapple
  • 本文由 发表于 2023年5月30日 12:25:31
  • 转载请务必保留本文链接:https://go.coder-hub.com/76361619.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定