🚀
[Swift]AVAudioEngineを利用した録音とFFT
はじめに
この記事では、録音から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に全体のソースコードがあるのでそちらもぜひ
Discussion