在 Swift 中使用 AVAudioEngine 点击麦克风输入

Posted

技术标签:

【中文标题】在 Swift 中使用 AVAudioEngine 点击麦克风输入【英文标题】:Tap Mic Input Using AVAudioEngine in Swift 【发布时间】:2015-01-27 23:54:40 【问题描述】:

我对新的 AVAudioEngine 感到非常兴奋。它似乎是围绕音频单元的一个很好的 API 包装器。不幸的是,到目前为止该文档不存在,而且我在让一个简单的图表工作时遇到了问题。

使用以下简单代码设置音频引擎图,从不调用点击块。它模仿了一些在网络上流传的示例代码,尽管这些也不起作用。

let inputNode = audioEngine.inputNode
var error: NSError?
let bus = 0
    
inputNode.installTapOnBus(bus, bufferSize: 2048, format: inputNode.inputFormatForBus(bus))  
    (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in
    println("sfdljk")

    
audioEngine.prepare()
if audioEngine.startAndReturnError(&error) 
    println("started audio")
 else 
    if let engineStartError = error 
        println("error starting audio: \(engineStartError.localizedDescription)")
    

我正在寻找的只是用于分析的原始 pcm 缓冲区。我不需要任何效果或输出。根据 WWDC 演讲“实践中的 502 音频引擎”,此设置应该可以工作。

现在如果你想从输入节点捕获数据,你可以安装一个节点水龙头,我们已经讨论过了。

但这个特定示例的有趣之处在于,如果我只想使用输入节点,比如从麦克风捕获数据并可能对其进行检查、实时分析或将其写入文件,我可以直接在输入节点上安装一个tap。

tap 将完成拉取数据输入节点的工作,将其填充到缓冲区中,然后将其返回给应用程序。

一旦你有了这些数据,你就可以用它做任何你需要做的事情。

以下是我尝试的一些链接:

    http://hondrouthoughts.blogspot.com/2014/09/avfoundation-audio-monitoring.html http://jamiebullock.com/post/89243252529/live-coding-audio-with-swift-playgrounds(在 Playground 中的 startAndReturnError SIGABRT)

编辑:这是基于 Thorsten Karrer 建议的实现。不幸的是,它不起作用。

class AudioProcessor 
    let audioEngine = AVAudioEngine()

    init()
        let inputNode = audioEngine.inputNode
        let bus = 0
        var error: NSError?
    
        inputNode.installTapOnBus(bus, bufferSize: 2048, format:inputNode.inputFormatForBus(bus)) 
            (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in
                println("sfdljk")
        
    
        audioEngine.prepare()
        audioEngine.startAndReturnError(nil)
        println("started audio")
    

【问题讨论】:

【参考方案1】:

这可能是因为您的 AVAudioEngine 超出范围并由 ARC 发布(“如果您喜欢它,那么您应该在其上添加保留...”)。

以下代码(引擎被移动到一个 ivar 并因此停留)触发水龙头:

class AppDelegate: NSObject, NSApplicationDelegate 

    let audioEngine  = AVAudioEngine()

    func applicationDidFinishLaunching(aNotification: NSNotification) 
        let inputNode = audioEngine.inputNode
        let bus = 0
        inputNode.installTapOnBus(bus, bufferSize: 2048, format: inputNode.inputFormatForBus(bus)) 
            (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in
            println("sfdljk")
        

        audioEngine.prepare()
        audioEngine.startAndReturnError(nil)
    

(为简洁起见,我删除了错误处理)

【讨论】:

我尝试了这个并用使用的代码编辑了我的问题。您的答案中的代码对您有用吗? Jup,为我工作。我没有在操场上测试过它,但是当从 Xcode 编译运行时它可以工作。连续调用点击块。 遗憾的是,我不得不承认它毕竟是一个未保留的对象。我的 AudioProcessor 类在方法中被实例化,而不是作为类属性。 您为什么选择带有“通知”参数的 appDelegate 方法?默认/事实上的一个是 didFinishLaunchingWithOptions: 原因是在我写答案的时候(2014),Xcode 会给我默认的通知参数。对于手头的问题,无论如何都无所谓。【参考方案2】:

更新:我已经实现了一个完整的记录麦克风输入的工作示例,在运行时应用一些效果(混响、延迟、失真),并将所有这些效果保存到输出文件中。

var engine = AVAudioEngine()
var distortion = AVAudioUnitDistortion()
var reverb = AVAudioUnitReverb()
var audioBuffer = AVAudioPCMBuffer()
var outputFile = AVAudioFile()
var delay = AVAudioUnitDelay()

//初始化音频引擎

func initializeAudioEngine() 

    engine.stop()
    engine.reset()
    engine = AVAudioEngine()

    isRealTime = true
    do 
        try AVAudiosession.sharedInstance().setCategory(AVAudioSessionCategoryPlayAndRecord)

        let ioBufferDuration = 128.0 / 44100.0

        try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(ioBufferDuration)

     catch 

        assertionFailure("AVAudioSession setup error: \(error)")
    

    let fileUrl = URLFor("/NewRecording.caf")
    print(fileUrl)
    do 

        try outputFile = AVAudioFile(forWriting:  fileUrl!, settings: engine.mainMixerNode.outputFormatForBus(0).settings)
    
    catch 

    

    let input = engine.inputNode!
    let format = input.inputFormatForBus(0)

    //settings for reverb
    reverb.loadFactoryPreset(.MediumChamber)
    reverb.wetDryMix = 40 //0-100 range
    engine.attachNode(reverb)

    delay.delayTime = 0.2 // 0-2 range
    engine.attachNode(delay)

    //settings for distortion
    distortion.loadFactoryPreset(.DrumsBitBrush)
    distortion.wetDryMix = 20 //0-100 range
    engine.attachNode(distortion)


    engine.connect(input, to: reverb, format: format)
    engine.connect(reverb, to: distortion, format: format)
    engine.connect(distortion, to: delay, format: format)
    engine.connect(delay, to: engine.mainMixerNode, format: format)

    assert(engine.inputNode != nil)

    isReverbOn = false

    try! engine.start()

//现在的录音功能:

func startRecording() 

    let mixer = engine.mainMixerNode
    let format = mixer.outputFormatForBus(0)

    mixer.installTapOnBus(0, bufferSize: 1024, format: format, block:
         (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in

            print(NSString(string: "writing"))
            do
                try self.outputFile.writeFromBuffer(buffer)
            
            catch 
                print(NSString(string: "Write failed"));
            
    )


func stopRecording() 

    engine.mainMixerNode.removeTapOnBus(0)
    engine.stop()

我希望这对你有帮助。谢谢!

【讨论】:

这行得通,但是有没有办法让它在没有实时从扬声器发出的效果的情况下工作? @user3344977 我目前正在研究一个我没有成功实施的类似模块。即带有效果的音频文件的离线渲染。这是我关注的帖子:[离线渲染] (***.com/a/15361251/2429443)。希望对你有帮助 这些值从何而来?:let ioBufferDuration = 128.0 / 44100.0 有用的代码。现在我有关于这个 AudioEngine 如何与 AudioUnits 之间的所有连接一起工作的代码。 效果很好。但是,如果添加 AVAudioUnitTimePitch 节点,它会崩溃:(【参考方案3】:

以上答案对我不起作用,但以下答案对我有用。我正在混音器节点上安装水龙头。

        mMixerNode?.installTapOnBus(0, bufferSize: 4096, format: mMixerNode?.outputFormatForBus(0),
    
        (buffer: AVAudioPCMBuffer!, time:AVAudioTime!) -> Void in
            NSLog("tapped")

    
    )

【讨论】:

【参考方案4】:

好话题

你好,布罗德尼

在您的主题中,我找到了我的解决方案。这是类似的主题Generate AVAudioPCMBuffer with AVAudioRecorder

参见 Wwdc 2014 502 讲座 - AVAudioEngine 在实践中捕获麦克风 => 在 20 分钟内使用点击代码创建缓冲区 => 在 21 .50 中

这里是 swift 3 代码

@IBAction func button01Pressed(_ sender: Any) 

    let inputNode = audioEngine.inputNode
    let bus = 0
    inputNode?.installTap(onBus: bus, bufferSize: 2048, format: inputNode?.inputFormat(forBus: bus)) 
        (buffer: AVAudioPCMBuffer!, time: AVAudioTime!) -> Void in

            var theLength = Int(buffer.frameLength)
            print("theLength = \(theLength)")

            var samplesAsDoubles:[Double] = []
            for i in 0 ..< Int(buffer.frameLength)
            
                var theSample = Double((buffer.floatChannelData?.pointee[i])!)
                samplesAsDoubles.append( theSample )
            

            print("samplesAsDoubles.count = \(samplesAsDoubles.count)")

    

    audioEngine.prepare()
    try! audioEngine.start()


停止音频

func stopAudio()
    

        let inputNode = audioEngine.inputNode
        let bus = 0
        inputNode?.removeTap(onBus: bus)
        self.audioEngine.stop()

    

【讨论】:

我也想出了一个类似的解决方案。事情只会工作一次,然后在 5 分钟内不会再次工作。当您的应用程序将要终止时(我正在从调试器中执行此操作),您需要移除水龙头并停止音频引擎。我还将 AudioSession 设置为非活动状态

以上是关于在 Swift 中使用 AVAudioEngine 点击麦克风输入的主要内容,如果未能解决你的问题,请参考以下文章

SWIFT - 是不是可以从 AVAudioEngine 或 AudioPlayerNode 保存音频?如果是,如何?

Swift AVAudioEngine - 如何使本地麦克风静音

在 Swift iOS 中播放 PCM 文件

在Swift iOS中播放PCM文件

AVAudioEngine MIDI 文件播放(当前进度+MIDI 结束回调)Swift

Swift AVAudioEngine:更改MacOS的音频输入设备