今回もiOSアプリ制作シリーズです。これまで学んだ録音・再生・フーリエ変換・オーバーラップ・窓関数・聴感補正・dB変換を全部盛りにしてSwiftUIによる平均化FFTを実行するアプリを作ってみます。まずは個別のコードを紹介し、最後にコピペ可能な全コードを載せて実行結果を説明します。
こんにちは。wat(@watlablog)です。ここではSwiftによる平均化FFT計算を使った音声処理アプリ制作の例を紹介します!
これまでのあらすじ
この記事は過去記事の内容をふんだんに引き継いでいます。WATLABブログでは過去に以下の記事で録音や再生、フーリエ変換やその他雑多な処理を紹介してきました。
・SwiftでiOSデバイスのマイクを使って録音機能を追加する方法
・SwiftUIとAVFoundationで音声を再生する方法
・Swiftで高速フーリエ変換(FFT)を実装する方法
今回はこのページで平均化FFTを行うためのまとまったコードを紹介してみます!
SwiftUIによる平均化FFTアプリの例
Audio関係のクラス
Audio.swiftの内容は「SwiftUIとAVFoundationで音声を再生する方法」と変更はありません。
詳細はこちらの記事をご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
import Foundation import AVFoundation class Recorder: NSObject, ObservableObject { // 録音クラス // オプショナル型としてAVAudioRecoderを定義 var audioRecorder: AVAudioRecorder? // 録音の状態を管理するプロパティ @Published var isRecording = false // 録音データの変数を宣言 @Published var waveformData: [Float] = [] // サンプリングレート var sampleRate: Float = 12800 // 再生用のAVAudioPlayerを宣言 private var player: AVAudioPlayer? // audioFileURL をプロパティとして宣言(クラス全体でアクセスするため:再生用) private var audioFileURL: URL? // カスタムクラスのコンストラクタを定義 override init() { super.init() setUpAudioRecorder() } private func setUpAudioRecorder() { // 録音の設定 let recordingSession = AVAudioSession.sharedInstance() // エラーを確認 do { try recordingSession.setCategory(.playAndRecord, mode: .default) try recordingSession.setActive(true) // 辞書型で設定値を変更 let settings: [String: Any] = [ AVFormatIDKey: Int(kAudioFormatLinearPCM), AVSampleRateKey: sampleRate, AVNumberOfChannelsKey: 1, AVLinearPCMBitDepthKey: 16, AVLinearPCMIsBigEndianKey: false, AVLinearPCMIsFloatKey: false, AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue ] // wavファイルのパスを設定する(.wavはリアルタイムに書き込まれる) let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] audioFileURL = documentsPath.appendingPathComponent("recording.wav") // audioFileURLのnilチェック guard let url = audioFileURL else { print("Error: audioFileURL is nil") return } do { audioRecorder = try AVAudioRecorder(url: url, settings: settings) // audioRecorderがnilでない場合のみバッファ割当てや初期化、設定をする audioRecorder?.prepareToRecord() } catch { print("Error setting up audio recorder: \(error)") } audioRecorder = try AVAudioRecorder(url: url, settings: settings) // audioRecorderがnilでない場合のみバッファ割当てや初期化、設定をする audioRecorder?.prepareToRecord() } // エラーの場合 catch { print("Error setting up audio recorder: \(error)") } } func startRecording() { // 録音するメソッド audioRecorder?.record() isRecording = true } func stopRecording() { // 録音停止するメソッド audioRecorder?.stop() isRecording = false // 録音停止時にwavファイルのパスをコンソールに表示する if let audioFileURL = audioRecorder?.url { print(audioFileURL)} // 配列データとして取得する getWaveformData { waveformData in print("wave length=", waveformData.count) self.waveformData = waveformData } } func getWaveformData(completion: @escaping ([Float]) -> Void) { // 録音結果の配列データを取得するメソッド guard let audioFileURL = audioRecorder?.url else { return } do { let audioFile = try AVAudioFile(forReading: audioFileURL) let audioFormat = AVAudioFormat(standardFormatWithSampleRate: audioFile.processingFormat.sampleRate, channels: audioFile.processingFormat.channelCount) let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat!, frameCapacity: UInt32(audioFile.length)) try audioFile.read(into: audioBuffer!) let floatArray = Array(UnsafeBufferPointer(start: audioBuffer!.floatChannelData![0], count: Int(audioBuffer!.frameLength))) completion(floatArray) } catch { print("Error getting waveform data: \(error)") } } func playRecording() { guard let url = audioFileURL else { print("Audio file not found") return } do { try? AVAudioSession.sharedInstance().setCategory(.playAndRecord, options: .defaultToSpeaker) try? AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) try? AVAudioSession.sharedInstance().setActive(true) player = try AVAudioPlayer(contentsOf: url) player?.prepareToPlay() player?.play() } catch { print("Error playing audio: \(error)") } } } struct PointsData: Identifiable { // 点群データの構造体 var xValue: Float var yValue: Float var id = UUID() } |
信号処理関係のクラス
信号処理関係のクラスはDSP.swiftに集約していますが、ここに色々過去記事からの追加があります。ここでは改めて関数群を紹介します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
import Foundation import Accelerate class DSP{ // 信号処理クラス struct Complex { // 複素数型の構造体を定義 // re:実部、im:虚部 var re: Float var im: Float // Complex + Complexの場合の演算処理 static func +(lhs: Complex, rhs: Complex) -> Complex { return Complex(re: lhs.re + rhs.re, im: lhs.im + rhs.im) } // Complex * Complexの場合の演算処理 static func *(lhs: Complex, rhs: Complex) -> Complex { return Complex(re: lhs.re * rhs.re - lhs.im * rhs.im, im: lhs.re * rhs.im + lhs.im * rhs.re) } // Complex * Floatの場合の演算処理 static func *(lhs: Complex, rhs: Float) -> Complex { return Complex(re: lhs.re * rhs, im: lhs.im * rhs) } } static func isPowerOfTwo(_ n: Int) -> Bool { // 入力が2のべき乗であるかどうかを判断する関数 return (n != 0) && ((n & (n - 1)) == 0) } static func fft(_ x: [Float]) -> [Complex] { // 入力データの長さが2のべき乗であることを確認 let log2n = vDSP_Length(log2(Double(x.count)).rounded(.up)) let n = 1 << log2n // 入力データを複素数形式に変換 var real = x var imaginary = [Float](repeating: 0.0, count: n) var splitComplexInput = DSPSplitComplex(realp: &real, imagp: &imaginary) // FFTの設定 let fftSetup = vDSP_create_fftsetup(log2n, FFTRadix(kFFTRadix2))! // FFTを適用 vDSP_fft_zip(fftSetup, &splitComplexInput, 1, log2n, FFTDirection(kFFTDirection_Forward)) // 複素数の出力結果を作成 var output = [Complex]() for i in 0..<n { output.append(Complex(re: splitComplexInput.realp[i], im: splitComplexInput.imagp[i])) } // FFT設定を解放 vDSP_destroy_fftsetup(fftSetup) return output } static func ov(data: [Float], samplerate: Float, Fs: Int, overlap: Float) -> ([[Float]], Int) { // オーバーラップ処理 let Ts = Float(data.count) / samplerate let Fc = Float(Fs) / samplerate let x_ol = Float(Fs) * (1 - (overlap/100)) let N_ave = Int((Ts - (Fc * (overlap/100))) / (Fc * (1-(overlap/100)))) var array = [[Float]]() for i in 0..<N_ave { let ps = Int(x_ol * Float(i)) array.append(Array(data[ps..<ps+Fs])) } return (array, N_ave) } static func hanningWindow(N: Int) -> ([Float], Float) { // ハニング窓 let w = (0..<N).map { 0.5 - 0.5 * cos(2.0 * .pi * Float($0) / Float(N - 1)) } // ウィンドウ補正係数 let acf = 1 / (w.reduce(0, +) / Float(N)) return (w, acf) } static func averagedFFT(y: [Float], samplerate: Float, Fs: Int, overlapRatio: Float) -> ([Float], [Float]) { let dt: Float = 1.0 / samplerate // 時間波形をオーバーラップ処理 let (overlapData, _) = ov(data: y, samplerate: samplerate, Fs: Fs, overlap: overlapRatio) // 平均化FFT var fftResults: [[Complex]] = [] // 保存する配列を初期化 let fftSize = isPowerOfTwo(overlapData[0].count) ? overlapData[0].count : 1 << Int(ceil(log2(Double(overlapData[0].count)))) print("Frame size=", Fs) print("Overlap ratio=", Float(overlapRatio)) print("Num. of frames=", overlapData.count) // ハニングウィンドウ let hanning_window = hanningWindow(N: fftSize) let window = hanning_window.0 let acf = hanning_window.1 print("acf.han=", acf) for frame in overlapData { // 窓関数を適用してFFT let fftResult = fft(zip(frame, window).map(*)) fftResults.append(fftResult) } // 平均化FFT var averageAmplitude: [Float] = Array(repeating: 0.0, count: fftResults[0].count) let N = Float(fftResults[0].count) // 直流成分の平均化 for i in 0..<fftResults.count { averageAmplitude[0] += pow(fftResults[i][0].re, 2) } averageAmplitude[0] /= (N * Float(fftResults.count)) // 変動成分の平均化 for i in 1..<averageAmplitude.count { for j in 0..<fftResults.count { let amplitude = sqrt(pow(fftResults[j][i].re, 2) + pow(fftResults[j][i].im, 2)) averageAmplitude[i] += pow(amplitude, 2) } averageAmplitude[i] /= (2 * N * Float(fftResults.count)) } averageAmplitude = averageAmplitude.map{sqrt($0)} // 周波数軸を計算 let freq = Array(stride(from: 0.0, to: 1.0 / (2.0 * dt), by: 1.0 / (dt * Float(overlapData[0].count)))) return (averageAmplitude, freq) } static func db(x: [Float], dBref: Float) -> [Float] { // dB変換 print("dBref=", dBref) return x.map { 20 * log10($0 / dBref) } } static func aweightings(frequencies: [Float], dB: [Float]) -> [Float] { // Aスケール特性(聴感補正) let minDb = dB.min() ?? Float.leastNormalMagnitude return frequencies.enumerated().map { index, f in let f = f == 0 ? 1e-6 : f let term1 = pow(12194, 2) * pow(f, 4) let term2 = (pow(f, 2) + pow(20.6, 2)) let term3 = sqrt((pow(f, 2) + pow(107.7, 2)) * (pow(f, 2) + pow(737.9, 2))) let term4 = pow(f, 2) + pow(12194, 2) let ra = term1 / (term2 * term3 * term4) let aWeightedDb = 20 * log10(ra) + 2.00 // 元のdB値の下限値を下回る場合は、元のdB値の下限値に置き換える return aWeightedDb < dB[index] ? dB[index] : aWeightedDb } } } |
複素数演算
フーリエ変換には複素数の計算が必須ですが、Swiftは複素数型を持ちません。そのため以下の関数で複素数の演算方法を定義する必要があります。Swiftで新しい型を定義する時は構造体(struct)を使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
struct Complex { // 複素数型の構造体を定義 // re:実部、im:虚部 var re: Float var im: Float // Complex + Complexの場合の演算処理 static func +(lhs: Complex, rhs: Complex) -> Complex { return Complex(re: lhs.re + rhs.re, im: lhs.im + rhs.im) } // Complex * Complexの場合の演算処理 static func *(lhs: Complex, rhs: Complex) -> Complex { return Complex(re: lhs.re * rhs.re - lhs.im * rhs.im, im: lhs.re * rhs.im + lhs.im * rhs.re) } // Complex * Floatの場合の演算処理 static func *(lhs: Complex, rhs: Float) -> Complex { return Complex(re: lhs.re * rhs, im: lhs.im * rhs) } } |
FFT
FFTはAccelerateというライブラリで行います。AccelerateのvDSPメソッドを使って信号を高速フーリエ変換します。詳細は「Swiftで高速フーリエ変換(FFT)を実装する方法」をご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
static func fft(_ x: [Float]) -> [Complex] { // 入力データの長さが2のべき乗であることを確認 let log2n = vDSP_Length(log2(Double(x.count)).rounded(.up)) let n = 1 << log2n // 入力データを複素数形式に変換 var real = x var imaginary = [Float](repeating: 0.0, count: n) var splitComplexInput = DSPSplitComplex(realp: &real, imagp: &imaginary) // FFTの設定 let fftSetup = vDSP_create_fftsetup(log2n, FFTRadix(kFFTRadix2))! // FFTを適用 vDSP_fft_zip(fftSetup, &splitComplexInput, 1, log2n, FFTDirection(kFFTDirection_Forward)) // 複素数の出力結果を作成 var output = [Complex]() for i in 0..<n { output.append(Complex(re: splitComplexInput.realp[i], im: splitComplexInput.imagp[i])) } // FFT設定を解放 vDSP_destroy_fftsetup(fftSetup) return output } |
オーバーラップ
長時間信号の平均化FFTをするために、時間信号をフレームで抽出するオーバーラップ処理を実装します。オーバーラップ処理については「Swiftで時間波形をオーバーラップ抽出するコード」をご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
static func ov(data: [Float], samplerate: Float, Fs: Int, overlap: Float) -> ([[Float]], Int) { // オーバーラップ処理 let Ts = Float(data.count) / samplerate let Fc = Float(Fs) / samplerate let x_ol = Float(Fs) * (1 - (overlap/100)) let N_ave = Int((Ts - (Fc * (overlap/100))) / (Fc * (1-(overlap/100)))) var array = [[Float]]() for i in 0..<N_ave { let ps = Int(x_ol * Float(i)) array.append(Array(data[ps..<ps+Fs])) } return (array, N_ave) } |
dB変換
dB(デシベル)変換も実施します。dB変換は必ずしもなくても良いのですが、Chartでグラフ表示する場合に軸を対数に設定しなくても波形が見やすくなるというメリットを持ちます。Pythonの記事ですが、詳細は「Pythonで音圧のデシベル(dB)変換式と逆変換式!」をご覧ください。
1 2 3 4 5 |
static func db(x: [Float], dBref: Float) -> [Float] { // dB変換 print("dBref=", dBref) return x.map { 20 * log10($0 / dBref) } } |
聴感補正
こちらも特になくても良いのですが、せっかく音声処理をするということで、聴感補正(A特性)も実装してみましょう。聴感補正の詳細もPython記事ではありますが、「Pythonで聴感補正(A特性)の曲線を作る!」をご覧ください。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
static func aweightings(frequencies: [Float], dB: [Float]) -> [Float] { // Aスケール特性(聴感補正) let minDb = dB.min() ?? Float.leastNormalMagnitude return frequencies.enumerated().map { index, f in let f = f == 0 ? 1e-6 : f let term1 = pow(12194, 2) * pow(f, 4) let term2 = (pow(f, 2) + pow(20.6, 2)) let term3 = sqrt((pow(f, 2) + pow(107.7, 2)) * (pow(f, 2) + pow(737.9, 2))) let term4 = pow(f, 2) + pow(12194, 2) let ra = term1 / (term2 * term3 * term4) let aWeightedDb = 20 * log10(ra) + 2.00 // 元のdB値の下限値を下回る場合は、元のdB値の下限値に置き換える return aWeightedDb < dB[index] ? dB[index] : aWeightedDb } } |
窓関数
フーリエ変換時の漏れ誤差(リーケージエラー)を防ぐために窓関数処理を実装します。こちらも詳しくは「SwiftUIで時間波形に窓関数をかけてグラフ表示する方法」をご覧ください。
1 2 3 4 5 6 7 8 9 |
static func hanningWindow(N: Int) -> ([Float], Float) { // ハニング窓 let w = (0..<N).map { 0.5 - 0.5 * cos(2.0 * .pi * Float($0) / Float(N - 1)) } // ウィンドウ補正係数 let acf = 1 / (w.reduce(0, +) / Float(N)) return (w, acf) } |
平均化FFT
そして本記事のメインである平均化FFTのコードがこちらです。平均化FFTはPythonで一度作っているので、詳細は「PythonでFFT実装!SciPyのフーリエ変換まとめ」の記事に任せます。今回はそのSwift版というわけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
static func averagedFFT(y: [Float], samplerate: Float, Fs: Int, overlapRatio: Float) -> ([Float], [Float]) { let dt: Float = 1.0 / samplerate // 時間波形をオーバーラップ処理 let (overlapData, _) = ov(data: y, samplerate: samplerate, Fs: Fs, overlap: overlapRatio) // 平均化FFT var fftResults: [[Complex]] = [] // 保存する配列を初期化 let fftSize = isPowerOfTwo(overlapData[0].count) ? overlapData[0].count : 1 << Int(ceil(log2(Double(overlapData[0].count)))) print("Frame size=", Fs) print("Overlap ratio=", Float(overlapRatio)) print("Num. of frames=", overlapData.count) // ハニングウィンドウ let hanning_window = hanningWindow(N: fftSize) let window = hanning_window.0 let acf = hanning_window.1 print("acf.han=", acf) for frame in overlapData { // 窓関数を適用してFFT let fftResult = fft(zip(frame, window).map(*)) fftResults.append(fftResult) } // 平均化FFT var averageAmplitude: [Float] = Array(repeating: 0.0, count: fftResults[0].count) let N = Float(fftResults[0].count) // 直流成分の平均化 for i in 0..<fftResults.count { averageAmplitude[0] += pow(fftResults[i][0].re, 2) } averageAmplitude[0] /= (N * Float(fftResults.count)) // 変動成分の平均化 for i in 1..<averageAmplitude.count { for j in 0..<fftResults.count { let amplitude = sqrt(pow(fftResults[j][i].re, 2) + pow(fftResults[j][i].im, 2)) averageAmplitude[i] += pow(amplitude, 2) } averageAmplitude[i] /= (2 * N * Float(fftResults.count)) } averageAmplitude = averageAmplitude.map{sqrt($0)} // 周波数軸を計算 let freq = Array(stride(from: 0.0, to: 1.0 / (2.0 * dt), by: 1.0 / (dt * Float(overlapData[0].count)))) return (averageAmplitude, freq) } |
GUI関係(メインファイルのコード)
最後にメインのGUI関係をまとめたContentView.swiftを紹介します。3画面構成です。最初の画面に録音と再生機能、2ページ目に録音波形の平均化FFT結果表示、最後のページにFFT設定を配置しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
import SwiftUI import Charts struct ContentView: View { init(){ // タブ背景色を変更 UITabBar.appearance().backgroundColor = UIColor(red: 0.1, green: 0.1, blue: 0.1, alpha: 0.2) } // UIの宣言 @State private var selectedFs = 2048 let FsOptions = [1024, 2048, 4096, 8192] @State private var text_overlapRatio: String = "50" @State private var text_dbref: String = "2e-5" // データ構造関係の宣言 @State private var data: [PointsData] = [] @State private var x: [Float] = [] @State private var y: [Float] = [] @State private var dataFreq: [PointsData] = [] @State private var x_freq: [Float] = [] @State private var y_freq: [Float] = [] // 録音関係の宣言 @StateObject private var recorder = Recorder() private var dt: Float { Float(1.0 / recorder.sampleRate) } @State private var isDisplayingData = false // 平均化フーリエ変換の宣言 @State private var Fs: Int = 2048 @State private var overlapRatio: Float = 50 @State private var dbref: Float = 2e-5 var body: some View { // UIとイベント TabView() { VStack{ // 録音と再生の画面 Text("Amplitude[Lin.]") .font(.caption) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.all, 1) Chart { // データ構造からx, y値を取得して散布図プロット ForEach(data) { shape in // 折れ線グラフをプロット LineMark( x: .value("x", shape.xValue), y: .value("y", shape.yValue) ) } } .padding(.all, 10) Text("Time [s]") .font(.caption) .padding(.all, 1) HStack{ if recorder.isRecording { // 録音している時 Button(action: { // 停止ボタンが押されたらデータをChartsに表示させる // 録音の実行 print("Stop") recorder.stopRecording() // データ取得 y = recorder.waveformData // 時間波形 let samplePoints = Float(y.count) x = Array(stride(from: 0.0, to: samplePoints * dt, by: dt)) // 時間波形プロットデータの追加 data.removeAll() dataFreq.removeAll() data = zip(x, y).map { PointsData(xValue: $0, yValue: $1) } isDisplayingData = false // FFT用パラメータの読み込み guard let overlapRatio = Float(text_overlapRatio), let dbref = Float(text_dbref) else { print("Invalid input") isDisplayingData = false return } // バックグラウンドでデータ処理を行う DispatchQueue.global(qos: .userInitiated).async { // 平均化FFT let (averageAmplitude, freq) = DSP.averagedFFT(y: y, samplerate: Float(recorder.sampleRate), Fs: Fs, overlapRatio: overlapRatio) // dB変換 let dBAmplitudes = DSP.db(x: averageAmplitude, dBref: dbref) // Aスケール聴感補正 let correctedAmplitudes = DSP.aweightings(frequencies: freq, dB: dBAmplitudes).enumerated().map { dBAmplitudes[$0.offset] + $0.element } // メインと同期させる DispatchQueue.main.async { // FFT波形のプロット dataFreq = zip(freq, correctedAmplitudes).map { PointsData(xValue: $0, yValue: $1) } isDisplayingData = false } } }) { Text("Stop Recording") .padding() .background(Color.red) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } } else { // 録音していない時 Button(action: { print("Start") isDisplayingData = true recorder.startRecording() }) { Text("Start Recording") .padding() .background(Color.green) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } .opacity(isDisplayingData ? 0.5 : 1.0) .disabled(isDisplayingData) } Button("Play"){ // 音声を再生する recorder.playRecording() } .padding() .background(Color.blue) .foregroundColor(Color.white) .cornerRadius(10) .padding(.all, 10) } } .padding() .tabItem{ Image(systemName: "mic.circle.fill") Text("REC") } VStack{ // 周波数分析の画面 Text("Amplitude[dBA]") .font(.caption) .frame(maxWidth: .infinity, alignment: .trailing) .padding(.all, 1) Chart { // データ構造からx, y値を取得して散布図プロット ForEach(dataFreq) { shape in // 折れ線グラフをプロット LineMark( x: .value("x", shape.xValue), y: .value("y", shape.yValue) ) } } Text("Frequency [Hz]") .font(.caption) .padding(.bottom, 10) } .padding() .tabItem{ Image(systemName: "chart.bar.xaxis") Text("Freq.") } VStack{ // 設定画面 GroupBox(label: Text("Frame size").font(.headline)) { Picker("Select Fs", selection: $selectedFs) { ForEach(FsOptions, id: \.self) { Text("\($0)") } } .pickerStyle(MenuPickerStyle()) } .padding(.bottom, 10) GroupBox(label: Text("Overlap ratio[%]").font(.headline)) { TextField("Enter Overlap ratio[%].", text:$text_overlapRatio) .keyboardType(.default) .textFieldStyle(RoundedBorderTextFieldStyle()) } .padding(.bottom, 10) GroupBox(label: Text("dBref").font(.headline)) { TextField("Enter dBref.", text:$text_dbref) .keyboardType(.default) .textFieldStyle(RoundedBorderTextFieldStyle()) } } .padding() .tabItem{ Image(systemName: "gear") Text("Setting") } } .accentColor(.blue) .edgesIgnoringSafeArea(.top) } } struct ContentView_Previews: PreviewProvider { static var previews: some View { // プレビュー ContentView() } } |
実行結果
下図が実行結果です。Start Recordingボタンを押し、口笛を吹いてみた結果です。録音を止めるにはStop Recordingボタンを押します。
もちろんiOSデバイスでも動作します!
それにしても、このブログを始めてから口笛がうまくなったと思います。
まとめ
今回はここまで!
この記事のアプリはサンプリングレートが12800Hzで固定されていますが、変更したい場合はAudio.swiftのsampleRateを変更します。
実際に操作してみるとわかりますが、まだまだ動作が重いような。。もっとスムーズに動かせればリリースも視野に入れたいのですが、もう少しかかりそうですね。
とはいえこの記事ではSwiftによる平均化FFTのコード実装を扱いました。なかなか検索ではやりたいことがヒットせず苦労しましたが、少しずつできるようになってきたようです。
SwiftとPythonで平均化高速フーリエ変換を実装することができました!
Twitter(今はXと呼ぶ?)でも関連情報をつぶやいているので、wat(@watlablog)のフォローお待ちしています!