このコードは、iOSアプリの画面上に音声レベルメーターを表示するためのSwiftUIビューを定義しています。 具体的には、AVFoundationフレームワークを使用して、デバイスのマイクからの入力を取得し、その音声レベルを取得しています。その後、音声レベルを示すProgressBarビューを表示しています。 AudioRecorderクラスは、AVAudioEngineをセットアップして入力ノードに接続し、音声レベルを取得するためにAVAudioPCMBufferを使用してバッファにインストールされたタップを開始します。 ContentViewビューは、audioRecorderオブジェクトを保持し、音声レベルを表示するためにテキストビューを使用し、音声レベルの値に基づいてProgressBarビューを更新しています。 このコードは、iOSアプリの開発者が、音声入力を使用してオーディオレベルを監視するための役立つサンプルとして利用できます。

import SwiftUI
import AVFoundation
import Combine

let minValue: Float = 0.00
let maxValue: Float = 0.005

struct ProgressBar: View {
    var value: Float
    
    var body: some View {
        GeometryReader { geometry in
            let clampedValue = min(max(value, minValue), maxValue)
            let normalizedValue = (clampedValue - minValue) / (maxValue - minValue)
            
            ZStack(alignment: .leading) {
                Rectangle()
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    .opacity(0.3)
                
                Rectangle()
                    .frame(width: geometry.size.width * CGFloat(normalizedValue), height: geometry.size.height)
                    .foregroundColor(progressBarColor(value: value))
            }
            .clipShape(RoundedRectangle(cornerRadius: 45.0))
            .animation(.linear(duration: 0.1))
        }
        .frame(height: 90) // メーターの高さ
        .padding()
    }
}

func progressBarColor(value: Float) -> Color {
    let clampedValue = min(max(value, minValue), maxValue)
    let normalizedValue = (clampedValue - minValue) / (maxValue - minValue)
    
    let green = max(1 - normalizedValue, 0)
    let red = max(normalizedValue, 0)
    return Color(red: Double(red), green: Double(green), blue: 0)
}

class AudioRecorder: NSObject, ObservableObject {
    private var audioEngine: AVAudioEngine!
    private var audioPlayerNode: AVAudioPlayerNode!
    private var levelTimer: Timer?
    @Published var audioLevel: Float = 0.0
    
    override init() {
        super.init()
        setupAudioEngine()
    }
    
    private func setupAudioEngine() {
        audioEngine = AVAudioEngine()
        audioPlayerNode = AVAudioPlayerNode()
        
        let audioSession = AVAudioSession.sharedInstance()
        
        do {
            try audioSession.setCategory(.playAndRecord, mode: .default, options: [])
            try audioSession.setActive(true, options: .init())
            
            let inputNode = audioEngine.inputNode
            let mixer = audioEngine.mainMixerNode
            
            let bus = 0
            let inputFormat = inputNode.inputFormat(forBus: bus)
            let outputFormat = mixer.outputFormat(forBus: bus)
            
            audioEngine.attach(audioPlayerNode)
            
            audioEngine.connect(inputNode, to: mixer, format: inputFormat)
            audioEngine.connect(audioPlayerNode, to: mixer, format: outputFormat)
            
            audioEngine.disconnectNodeOutput(inputNode)
            
            inputNode.installTap(onBus: bus, bufferSize: 1024, format: inputFormat) { [weak self] (buffer: AVAudioPCMBuffer, time: AVAudioTime) in
                guard let channelData = buffer.floatChannelData?[0] else { return }
                let channelDataValue = Array(UnsafeBufferPointer(start: channelData, count:Int(buffer.frameLength)))
                    .map{abs($0)}
                    .reduce(0, +) / Float(buffer.frameLength)
                self?.audioLevel = channelDataValue
            }
            
            try audioEngine.start()
            
            levelTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
                DispatchQueue.main.async {
                    self?.objectWillChange.send()
                }
            }
        } catch {
            print(error)
        }
    }
}


struct ContentView: View {
    @StateObject private var audioRecorder = AudioRecorder()
    
    
    var body: some View {
        VStack {
            Text("Audio Level")
                .font(.headline)
            Text(String(format: "%.2f", audioRecorder.audioLevel * 10000))
                .font(.largeTitle)
                .foregroundColor(.red)
            ProgressBar(value: audioRecorder.audioLevel)
                .frame(height: 20)
                .padding()
        }
    }
}