Files
clawdbot/apps/macos/Sources/Clawdis/MicLevelMonitor.swift
2025-12-07 17:55:07 +01:00

82 lines
2.7 KiB
Swift

import AVFoundation
import SwiftUI
actor MicLevelMonitor {
private let engine = AVAudioEngine()
private var update: (@Sendable (Double) -> Void)?
private var running = false
private var smoothedLevel: Double = 0
func start(onLevel: @Sendable @escaping (Double) -> Void) async throws {
self.update = onLevel
if self.running { return }
let input = self.engine.inputNode
let format = input.outputFormat(forBus: 0)
input.removeTap(onBus: 0)
input.installTap(onBus: 0, bufferSize: 512, format: format) { [weak self] buffer, _ in
guard let self else { return }
let level = Self.normalizedLevel(from: buffer)
Task { await self.push(level: level) }
}
self.engine.prepare()
try self.engine.start()
self.running = true
}
func stop() {
guard self.running else { return }
self.engine.inputNode.removeTap(onBus: 0)
self.engine.stop()
self.running = false
}
private func push(level: Double) {
self.smoothedLevel = (self.smoothedLevel * 0.45) + (level * 0.55)
guard let update else { return }
let value = self.smoothedLevel
Task { @MainActor in update(value) }
}
private static func normalizedLevel(from buffer: AVAudioPCMBuffer) -> Double {
guard let channel = buffer.floatChannelData?[0] else { return 0 }
let frameCount = Int(buffer.frameLength)
guard frameCount > 0 else { return 0 }
var sum: Float = 0
for i in 0..<frameCount {
let s = channel[i]
sum += s * s
}
let rms = sqrt(sum / Float(frameCount) + 1e-12)
let db = 20 * log10(Double(rms))
let normalized = max(0, min(1, (db + 50) / 50))
return normalized
}
}
struct MicLevelBar: View {
let level: Double
let segments: Int = 12
var body: some View {
HStack(spacing: 3) {
ForEach(0..<self.segments, id: \.self) { idx in
let fill = self.level * Double(self.segments) > Double(idx)
RoundedRectangle(cornerRadius: 2)
.fill(fill ? self.segmentColor(for: idx) : Color.gray.opacity(0.35))
.frame(width: 14, height: 10)
}
}
.padding(4)
.background(
RoundedRectangle(cornerRadius: 6)
.stroke(Color.gray.opacity(0.25), lineWidth: 1))
}
private func segmentColor(for idx: Int) -> Color {
let fraction = Double(idx + 1) / Double(self.segments)
if fraction < 0.65 { return .green }
if fraction < 0.85 { return .yellow }
return .red
}
}