VoiceInput/Sources/UI/WaveformView.swift
JOJO 1d9c9d5fa0 feat: macOS 菜单栏语音输入应用初始版本
- CGEvent cghidEventTap 全局监听 Fn 键
- Apple Speech 流式语音识别(zh-CN 默认)
- 实时音频 RMS 电平驱动波形动画
- NSPanel 胶囊浮动窗口
- 剪贴板 + Cmd+V 文本注入,CJK 输入法自动切换
- OpenAI 兼容 LLM 纠错(DeepSeek 预设)
- LSUIElement 菜单栏运行
- SPM + Makefile 构建
2026-04-24 16:17:57 +08:00

101 lines
3.1 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Cocoa
/// 5 RMS
final class WaveformView: NSView {
private let weights: [Float] = [0.5, 0.8, 1.0, 0.75, 0.55]
private var dots: [NSView] = []
private var smoothedLevels: [Float] = [0, 0, 0, 0, 0]
private var decayTimer: Timer?
private var rawLevel: Float = 0
private let dotSize: CGFloat = 4
private let maxExtra: CGFloat = 28
private let spacing: CGFloat = 6
private let silenceThreshold: Float = 0.02
override init(frame: NSRect) {
super.init(frame: frame)
setupDots()
wantsLayer = true
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupDots() {
dots.forEach { $0.removeFromSuperview() }
dots.removeAll()
let totalWidth = CGFloat(weights.count) * dotSize + CGFloat(weights.count - 1) * spacing
let startX = (bounds.width - totalWidth) / 2
for (index, _) in weights.enumerated() {
let x = startX + CGFloat(index) * (dotSize + spacing)
let dot = NSView(frame: NSRect(x: x, y: 0, width: dotSize, height: dotSize))
dot.wantsLayer = true
dot.layer?.cornerRadius = dotSize / 2
dot.layer?.backgroundColor = NSColor.white.withAlphaComponent(0.9).cgColor
addSubview(dot)
dots.append(dot)
}
decayTimer?.invalidate()
decayTimer = Timer.scheduledTimer(withTimeInterval: 0.04, repeats: true) { [weak self] _ in
self?.updateDecay()
}
}
func updateLevel(_ level: Float) {
rawLevel = level
for i in 0..<weights.count {
let target = level * weights[i]
if target > smoothedLevels[i] {
smoothedLevels[i] = smoothedLevels[i] * 0.55 + target * 0.45
}
}
applyLevels()
}
private func updateDecay() {
for i in 0..<weights.count {
let target = rawLevel * weights[i]
if target < smoothedLevels[i] {
smoothedLevels[i] = smoothedLevels[i] * 0.82 + target * 0.18
}
if smoothedLevels[i] < silenceThreshold {
smoothedLevels[i] = 0
}
}
applyLevels()
}
private func applyLevels() {
CATransaction.begin()
CATransaction.setAnimationDuration(0.06) //
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeOut))
for (i, dot) in dots.enumerated() {
let level = smoothedLevels[i]
let extra = CGFloat(level) * maxExtra
let height = dotSize + extra
let y = (bounds.height - height) / 2
dot.frame = NSRect(x: dot.frame.origin.x, y: y, width: dotSize, height: height)
dot.layer?.cornerRadius = min(dotSize, height) / 2
}
CATransaction.commit()
}
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
setupDots()
}
deinit {
decayTimer?.invalidate()
}
}