マイクの音量を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()
}
}
}