如何使用Swift在iOS中捕获音频样本?

17

我在网上找到了很多有关在iOS中处理音频的示例,但大部分都已经过时了,不能适用于我想要实现的目标。我的项目是:

我需要从两个来源捕获音频样本-麦克风输入和存储的音频文件。我需要对这些样本执行FFT以产生整个剪辑的“指纹”,并应用一些其他滤波器。最终目标是构建一种类似于Shazam等歌曲识别软件。

在iOS 8中捕获单独的音频样本以执行快速傅里叶变换的最佳方法是什么? 我想最终得到一个大的数组,但我怀疑它可能不完全像那样工作。 其次,如何使用Accelerate框架处理音频? 它似乎是在iOS中执行复杂音频分析的最有效方法。

我看到的所有在线示例都使用旧版本的iOS和Objective-C,并且我无法成功将它们转化为Swift。 iOS 8是否提供了一些新的框架来处理这种情况?


你可以先看看苹果自己的例子。它们可能是用Objective-C写的,但API没有改变。无论如何,所有的vDSP_xx函数都有C API,而且实际上,你项目中的分析部分可能最好用C或C++编写(顺便说一下,这是苹果工程师在今年WWDC上为编写音频处理/渲染处理程序提供的建议)。至于音频指纹识别,这是一个非常复杂的问题,也太广泛了,不适合在SO上讨论。 - marko
你找到了什么吗? - hoangpx
2个回答

16

AVAudioEngine是最适合做这个的。根据苹果文档:

  • 如果需要播放和录制单个音轨,请使用AVAudioPlayer和AVAudioRecorder。
  • 如果需要进行更复杂的音频处理,请使用AVAudioEngine。 AVAudioEngine包括AVAudioInputNode和AVAudioOutputNode,用于音频输入和输出。您还可以使用AVAudioNode对象将处理和混合效果与音频混合在一起。

我坦白地告诉你:AVAudioEngine是一个非常棘手的API,文档模糊,错误信息也很少有帮助,并且几乎没有在线代码示例演示超过最基本任务的内容。但是,如果你花时间克服学习曲线,你可以相对容易地做出一些神奇的事情。

我已经构建了一个简单的“playground”视图控制器,演示了麦克风和音频文件采样同时工作的情况:

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!
    }

    static func getController() -> AudioEnginePlaygroundViewController {
        let me = AudioEnginePlaygroundViewController(nibName: "AudioEnginePlaygroundViewController", bundle: nil)
        return me
    }

    @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) {
            playerNode .removeTap(onBus: 0)
        }
        playerNode.installTap(onBus: 0, bufferSize: 4096, format: playerNode.outputFormat(forBus: 0)) { (buffer, when) in
            let sampleData = UnsafeBufferPointer(start: buffer.floatChannelData![0], count: Int(buffer.frameLength))
        }
        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()
    }
}

音频样本通过installTap的完成处理程序提供给您,该处理程序在实时传递经过点击节点(麦克风或音频文件播放器)的音频时不断调用。 您可以通过索引我在每个块中创建的sampleData指针来访问单个样本。


3
这正是OP在寻找的。感谢您在问题提出多年后添加此答案。 - BigHeadCreations
1
@BigHeadCreations 很高兴解决了 AVAudioEngine 信息的缺失。感谢您的提及 :) - WongWray
@WongWray 很棒的回答。这方面真的很少有例子。我正在尝试在调用ML预测器之前对输入数据进行采样率转换。我需要4秒钟的采样数据,就像一个环形缓冲区,以便我可以调用预测。如果我为此创建一个问题,你会有兴趣回答吗?谢谢。 - Spring
@WongWray 如何使音频引擎立即播放从麦克风录制的音频? - Roman Samoilenko

7

Swift

iOS中的录音:

  • 创建并维护一个AVAudioRecorder实例,例如var audioRecorder: AVAudioRecorder? = nil
  • 使用URL和一些录音设置初始化AVAudioRecorder

录音会话过程:

  1. 调用prepareToRecord()
  2. 调用record()
  3. 调用stop()

完整的Swift/AVAudioRecorder示例

在您的录音方法中,核心代码可以是:

func record() {
    self.prepareToRecord()
    if let recorder = self.audioRecorder {
        recorder.record()
    }
}

为了准备录制(流媒体到文件),你可以进行以下操作:

func prepareToRecord() {
    var error: NSError?
    let documentsPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as! NSString
    let soundFileURL: NSURL? = NSURL.fileURLWithPath("\(documentsPath)/recording.caf")
    
    self.audioRecorder = AVAudioRecorder(URL: soundFileURL, settings: recordSettings as [NSObject : AnyObject], error: &error)
    if let recorder = self.audioRecorder {
        recorder.prepareToRecord()
    }
}

最后,要停止录制,请使用以下内容:

func stopRecording() {
    if let recorder = self.audioRecorder {
        recorder.stop()
    }
}

上面的示例还需要导入AVFoundation和一些recordSettings,具体内容可以根据您的选择进行。一个recordSettings的示例可能如下所示:
let recordSettings = [
    AVFormatIDKey: kAudioFormatAppleLossless,
    AVEncoderAudioQualityKey : AVAudioQuality.Max.rawValue,
    AVEncoderBitRateKey : 320000,
    AVNumberOfChannelsKey: 2,
    AVSampleRateKey : 44100.0
]

按照这样做,你就完成了。


您可能还想查看这个Stack Overflow答案,其中包括一个演示项目


2
这些信息很有帮助,但我如何从录音中提取单独的音频样本?我需要原始数据 - 最好是一个浮点数数组,以便我可以进行分析。同样的问题也适用于已经存在于磁盘上的文件。 - Hundley
1
假设您使用上面的kAudioFormatAppleLossless格式,样本存储在一个CAF文件中,该文件在https://developer.apple.com/library/ios/documentation/MusicAudio/Reference/CAFSpec/CAF_overview/CAF_overview.html#//apple_ref/doc/uid/TP40001862-CH209-TPXREF101上有记录。从这样的文件中读取样本的方法在http://stackoverflow.com/questions/13996236/how-to-convert-wav-caf-files-sample-data-to-byte-array中有解答。 - SwiftArchitect
1
我发现你的 http://swiftarchitect.com/recipes/#SO-32342486 很有用。谢谢。 - vivin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接