🚀

[Swift]AVAudioEngineを利用した録音とFFT

2021/10/03に公開

はじめに

この記事では、録音からFFTするまでの処理をまとめて書いてみます。

環境

  • Xcode 12.5
  • Swift 5.4

Info.plist

Info.plistにマイクを使用することを追記します

Privacy - Microphone Usage Description

録音

AVFoundationをimportします

import AVFoundation

AVAudioEngineとAVAudioSessionのインスタンスを生成します

let audioEngine = AVAudioEngine()
let session = AVAudioSession.sharedInstance()

録音モードに変更し、アクティブにします

try! session.setCategory(AVAudioSession.Category.record)
try! session.setActive(true)

録音を開始します

audioEngine.prepare()
if !audioEngine.isRunning {
    try! audioEngine.start()
}

audioEngineには、マイクを監視するinputNodeというノードが存在します。このinputNodeに対してinstallTapすることで、録音データを受け取ることができます。
録音時に行うこと(FFTなど)はinstallTap内に書きます。この場合4096毎処理が行われます

audioEngine.inputNode.installTap(onBus: 0, bufferSize: 4096, format: nil, block: {
    (buffer, time) in
    
    //FFTなどの処理を書く

})

録音を停止します

if audioEngine.isRunning {
    audioEngine.stop()
    // Tapを削除(登録したままにすると次に Installした時点でエラーになる
    audioEngine.inputNode.removeTap(onBus: 0)

    // Audio sessionを停止
    try! session.setActive(false)
}

FFT

FFTはこちらのソースコードを参考にさせていただきました。
FFTの流れは次のようになっています。
音声データに対して窓関数をかける→複素数を管理できるように型変換→FFT

import Foundation
import Accelerate

class FFT{
    
    private var mSpectrumAnalysis: FFTSetup?
    private var mDspSplitComplex: DSPSplitComplex
    private var mFFTNormFactor: Float32
    private var mFFTLength: vDSP_Length
    private var mLog2N: vDSP_Length
    
    
    private final var kAdjust0DB: Float32 = 1.5849e-13
    
    init(maxFramesPerSlice inMaxFramesPerSlice: Int) {
        mSpectrumAnalysis = nil
        mFFTNormFactor = 1.0/Float32(2*inMaxFramesPerSlice)
        mFFTLength = vDSP_Length(inMaxFramesPerSlice)/2
        //log2を求めている、leadingZeroBitCountは先頭から0の数を数える,1引いたものから数えて32から引くと、ビット的にlog2がわかる
        mLog2N = vDSP_Length(32 - UInt32((UInt32(inMaxFramesPerSlice) - 1).leadingZeroBitCount))
        mDspSplitComplex = DSPSplitComplex(
            realp: UnsafeMutablePointer.allocate(capacity: Int(mFFTLength)),
            imagp: UnsafeMutablePointer.allocate(capacity: Int(mFFTLength))
        )
        mSpectrumAnalysis = vDSP_create_fftsetup(mLog2N, FFTRadix(kFFTRadix2))
    }
    
    deinit {
        vDSP_destroy_fftsetup(mSpectrumAnalysis)
        mDspSplitComplex.realp.deallocate()
        mDspSplitComplex.imagp.deallocate()
    }
    
    //inAudioDataをFFTしたデータをoutFFTDataに格納
    func computeFFT(_ inAudioData: UnsafePointer<Float32>?, outFFTData: UnsafeMutablePointer<Float32>?) {
        guard
            let inAudioData = inAudioData,
            let outFFTData = outFFTData
        else { return }

        //make window (fft size)
        let mFFTFulLength: vDSP_Length = mFFTLength * 2
        typealias FloatPointer = UnsafeMutablePointer<Float32>
        let window = FloatPointer.allocate(capacity: Int(mFFTFulLength))
        
        //blackman window
        vDSP_blkman_window(window, mFFTFulLength, 0)
        //vDSP_hamm_window(window, mFFTFulLength, 0)
        //vDSP_hann_window(window, mFFTFulLength, 0)
        
        //windowing 
        var windowAudioData = UnsafeMutablePointer<Float32>.allocate(capacity: Int(mFFTFulLength))
        
        vDSP_vmul(inAudioData, 1, window, 1, windowAudioData, 1, mFFTFulLength)
   
        //Complex型に変換
        windowAudioData.withMemoryRebound(to: DSPComplex.self, capacity: Int(mFFTLength)) {inAudioDataPtr in
            vDSP_ctoz(inAudioDataPtr, 2, &mDspSplitComplex, 1, mFFTLength)
        }
	
        //Take the fft and scale appropriately
        //fft
        vDSP_fft_zrip(mSpectrumAnalysis!, &mDspSplitComplex, 1, mLog2N, FFTDirection(kFFTDirection_Forward))
        //実数と虚数にmFFTNormFactorをかける
        vDSP_vsmul(mDspSplitComplex.realp, 1, &mFFTNormFactor, mDspSplitComplex.realp, 1, mFFTLength)
        vDSP_vsmul(mDspSplitComplex.imagp, 1, &mFFTNormFactor, mDspSplitComplex.imagp, 1, mFFTLength)
        
        //Zero out the nyquist value
        mDspSplitComplex.imagp[0] = 0.0
        
        //Convert the fft data to dB  ルートの実数^2 + 虚数^2
        vDSP_zvmags(&mDspSplitComplex, 1, outFFTData, 1, mFFTLength)
        
        //In order to avoid taking log10 of zero, an adjusting factor is added in to make the minimum value equal -128dB
        
        //小さい値を足す
       // vDSP_vsadd(outFFTData, 1, &kAdjust0DB, outFFTData, 1, mFFTLength)
       // var one: Float32 = 1
        //リニア値から対数:dbに変換
        //vDSP_vdbcon(outFFTData, 1, &one, outFFTData, 1, mFFTLength, 0)
        
    }
    
}

録音と音声を組み合わせる

FFTクラス(mFFTHelper)とFFTした音声データを格納する変数(l_fftData)を用意して初期化します。

var mFFTHelper: FFT!
var l_fftData: UnsafeMutablePointer<Float32>!

init(inMaxFramesPerSlice:Int = 4096){
l_fftData = UnsafeMutablePointer.allocate(capacity: 2048)
bzero(l_fftData, size_t(2048 * MemoryLayout<Float32>.size))

mFFTHelper = FFT(maxFramesPerSlice: inMaxFramesPerSlice)
}

先ほどのinstallTap内でFFTを実行します

audioEngine.inputNode.installTap(onBus: 0, bufferSize: 4096, format: nil, block: {
    (buffer, time) in

    //録音データをFFTする
    let bfr = UnsafePointer(buffer.floatChannelData!.pointee)
    self.mFFTHelper.computeFFT(bfr, outFFTData: self.l_fftData)
   
})

おわりに

GitHubに全体のソースコードがあるのでそちらもぜひ
https://github.com/aba097/SwiftFFT

Discussion