- CGEvent cghidEventTap 全局监听 Fn 键 - Apple Speech 流式语音识别(zh-CN 默认) - 实时音频 RMS 电平驱动波形动画 - NSPanel 胶囊浮动窗口 - 剪贴板 + Cmd+V 文本注入,CJK 输入法自动切换 - OpenAI 兼容 LLM 纠错(DeepSeek 预设) - LSUIElement 菜单栏运行 - SPM + Makefile 构建
101 lines
3.1 KiB
Swift
101 lines
3.1 KiB
Swift
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()
|
||
}
|
||
}
|