chore: rename project to clawdbot
This commit is contained in:
81
apps/macos/Sources/Clawdbot/MicLevelMonitor.swift
Normal file
81
apps/macos/Sources/Clawdbot/MicLevelMonitor.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user