マイクの音量をViewに反映させる

はじめに

iOS標準では入っているボイスレコーダのように音量に応じてViewを描画できるようなUIを作成したいと考え、その足掛かりとしてiPhoneのマイクの音量を取得してViewの大きさを変えるコードを試してみました。

【Tips】iOSでマイクの音を検知する マイクからの音量取得はObjective-Cで書かれた記事があったのでSwiftに書き直して一部改造しました

MicrophoneVolumeViewをそのまま配置すればマイクの音量に応じてViewのサイズが変化します。

正規化

マイクから帰ってくるRMSの値は音の振幅の値で0を最大値として基本的にマイナス値が返ってきます。

Viewの大きさの変更にしようする際にマイナス値だと扱いにくいので1.0から0.0の値に正規化をしています。

let normalizationValue = (CGFloat(levelMeter.mAveragePower) - minVol) / (maxVol - minVol)

※注意点

AudioQueueLevelMeterStateのmAveragePowerの値は最大値が0で最小値が-160まで取れるとドキュメントにあったのですが、手元では最大値0.7、最小値 -60くらいの値がそれぞれMaxだったので最小値の正規化計算しているところをいじった方がviewの動きの大きさは大きくなると思います。

コード

class MicrophoneLebelManager: ObservableObject {
    @Published var volume: CGFloat = 0

    var queue: AudioQueueRef!
    var recordingTimer: Timer!
    
    func startUpdatingVolume() {
        // 録音データを記録するフォーマット
        var dataFormat = AudioStreamBasicDescription(
            mSampleRate: 44100.0,
            mFormatID: kAudioFormatLinearPCM,
            mFormatFlags: AudioFormatFlags(kLinearPCMFormatFlagIsBigEndian |
                                            kLinearPCMFormatFlagIsSignedInteger |
                                            kLinearPCMFormatFlagIsPacked),
            mBytesPerPacket: 2,
            mFramesPerPacket: 1,
            mBytesPerFrame: 2,
            mChannelsPerFrame: 1,
            mBitsPerChannel: 16,
            mReserved: 0)
        
        // 新しい録音オーディオキューオブジェクトを作成
        AudioQueueNewInput(&dataFormat,
                           AudioQueueInputCallback,
                           UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()),
                           .none,
                           .none,
                           0,
                           &queue)
        
        // オーディオの再生または録音の開始
        AudioQueueStart(self.queue, nil)
        
        var enabledLevelMeter: UInt32 = 1
        // オーディオキューのプロパティ値を設定
        AudioQueueSetProperty(self.queue,
                              kAudioQueueProperty_EnableLevelMetering,
                              &enabledLevelMeter,
                              UInt32(MemoryLayout<UInt32>.size))
        
        self.recordingTimer = Timer.scheduledTimer(timeInterval: 1 / 60,
                                                   target: self,
                                                   selector: #selector(self.detectVolume(timer:)),
                                                   userInfo: nil,
                                                   repeats: true)
        self.recordingTimer?.fire()
    }
    
    func stopUpdatingVolume() {
        // Finish observation
        
        self.recordingTimer.invalidate()
        self.recordingTimer = nil
        AudioQueueFlush(self.queue)
        AudioQueueStop(self.queue, false)
        AudioQueueDispose(self.queue, true)
    }
    
    @objc private func detectVolume(timer: Timer) {
        // オーディオキューの現在のレベル情報の
        var levelMeter = AudioQueueLevelMeterState()
        var propertySize = UInt32(MemoryLayout<AudioQueueLevelMeterState>.size)
        
        AudioQueueGetProperty(
            self.queue,
            kAudioQueueProperty_CurrentLevelMeterDB,
            &levelMeter,
            &propertySize)
        
        let minVol: CGFloat = -160
        let maxVol: CGFloat = 0
        // min: -60, max: -0 くらいが手元の環境では取れたのでそっちの方が綺麗に動く
        let normalizationValue = (CGFloat(levelMeter.mAveragePower) - minVol) / (maxVol - minVol)
        
        self.volume = normalizationValue
    }
}


struct MicrophoneVolumeView: View {
    @State var rectangleHeight: CGFloat = 100
    @ObservedObject var microphoneLebelManager = MicrophoneLebelManager()
    
    var body: some View {
        
        VStack {
            Rectangle()
                .foregroundColor(.gray)
                .frame(width: 10, height: 100 + rectangleHeight * microphoneLebelManager.volume)
        }
        .onAppear() {
            microphoneLebelManager.startUpdatingVolume()
        }.onDisappear() {
            microphoneLebelManager.stopUpdatingVolume()
        }
    }
}