Browse Source

fix: decibel logarithmic loudness metering and Apple Intelligence sapphire blue dynamic HUD redesign

main
Ali Alavi 2 weeks ago
parent
commit
dd0f3231c6
  1. 6
      src/AudioRecorder.swift
  2. 160
      src/HUDOverlay.swift

6
src/AudioRecorder.swift

@ -151,14 +151,16 @@ public final class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBuffer
// Stream live audio chunk to WebSocket immediately // Stream live audio chunk to WebSocket immediately
onAudioChunkAvailable?(data) onAudioChunkAvailable?(data)
// Calculate RMS level for HUD
// Calculate decibel loudness for perceptual, high-responsiveness audio meter
var sumSquare: Float = 0 var sumSquare: Float = 0
for i in 0..<frameLen { for i in 0..<frameLen {
let sample = Float(int16Ptr[i]) / 32768.0 let sample = Float(int16Ptr[i]) / 32768.0
sumSquare += sample * sample sumSquare += sample * sample
} }
let rms = sqrt(sumSquare / Float(frameLen)) let rms = sqrt(sumSquare / Float(frameLen))
let normalizedLevel = min(max(rms * 4.5, 0.0), 1.0)
// Convert to decibels (-50 dB noise floor to -12 dB peak speech)
let db = 20.0 * log10(max(rms, 0.00001))
let normalizedLevel = min(max((db + 48.0) / 36.0, 0.0), 1.0)
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.onAudioLevelUpdate?(normalizedLevel) self?.onAudioLevelUpdate?(normalizedLevel)

160
src/HUDOverlay.swift

@ -14,7 +14,7 @@ public final class HUDOverlayController {
public static let shared = HUDOverlayController() public static let shared = HUDOverlayController()
private var window: NSPanel? private var window: NSPanel?
private var orbView: SolidSiriOrbView?
private var orbView: AppleIntelligenceOrbView?
private var isVisible = false private var isVisible = false
// Large transparent canvas so audio-reactive halo never clips // Large transparent canvas so audio-reactive halo never clips
@ -44,10 +44,10 @@ public final class HUDOverlayController {
let container = NSView(frame: panel.contentView!.bounds) let container = NSView(frame: panel.contentView!.bounds)
container.wantsLayer = true container.wantsLayer = true
// Minimal Solid Orb centered in canvas
let orbDiameter: CGFloat = 50
// Professional Apple Intelligence Orb centered in canvas
let orbDiameter: CGFloat = 54
let orbOrigin = (canvasSize - orbDiameter) / 2.0 let orbOrigin = (canvasSize - orbDiameter) / 2.0
let orb = SolidSiriOrbView(frame: NSRect(x: orbOrigin, y: orbOrigin, width: orbDiameter, height: orbDiameter))
let orb = AppleIntelligenceOrbView(frame: NSRect(x: orbOrigin, y: orbOrigin, width: orbDiameter, height: orbDiameter))
container.addSubview(orb) container.addSubview(orb)
self.orbView = orb self.orbView = orb
@ -59,9 +59,9 @@ public final class HUDOverlayController {
private func reposition() { private func reposition() {
guard let window = self.window, let screen = NSScreen.main else { return } guard let window = self.window, let screen = NSScreen.main else { return }
let screenRect = screen.visibleFrame let screenRect = screen.visibleFrame
// Center horizontally, positioned comfortably higher above the dock
// Center horizontally, positioned comfortably higher above dock
let x = screenRect.midX - (canvasSize / 2.0) let x = screenRect.midX - (canvasSize / 2.0)
let y = screenRect.minY + 75
let y = screenRect.minY + 68
window.setFrameOrigin(NSPoint(x: x, y: y)) window.setFrameOrigin(NSPoint(x: x, y: y))
} }
@ -127,14 +127,15 @@ public final class HUDOverlayController {
} }
} }
// MARK: - Minimal Solid-Color Orb with 100% Dynamic Audio-Driven Stroke & Bloom Halo
// MARK: - Professional Apple Intelligence Orb View
final class SolidSiriOrbView: NSView {
final class AppleIntelligenceOrbView: NSView {
private let glowLayer = CALayer() private let glowLayer = CALayer()
private let solidCoreLayer = CALayer()
private let coreGradientLayer = CAGradientLayer()
private let strokeBorderLayer = CALayer() private let strokeBorderLayer = CALayer()
private let maxStrokeWidth: CGFloat = 8.0
private let minStrokeWidth: CGFloat = 2.0
private let maxStrokeWidth: CGFloat = 7.5
override init(frame frameRect: NSRect) { override init(frame frameRect: NSRect) {
super.init(frame: frameRect) super.init(frame: frameRect)
@ -155,75 +156,85 @@ final class SolidSiriOrbView: NSView {
let bounds = self.bounds let bounds = self.bounds
let radius = bounds.width / 2.0 let radius = bounds.width / 2.0
// 1. Dynamic Outer Atmospheric Glow (100% audio-driven; disappears completely in silence)
glowLayer.frame = bounds.insetBy(dx: -12, dy: -12)
glowLayer.cornerRadius = (bounds.width + 24) / 2.0
glowLayer.backgroundColor = NSColor.clear.cgColor
glowLayer.shadowColor = NSColor.white.cgColor
glowLayer.shadowRadius = 0
glowLayer.shadowOpacity = 0.0
// 1. Atmospheric Ambient Glow (Expands and blooms with decibel level)
glowLayer.frame = bounds.insetBy(dx: -10, dy: -10)
glowLayer.cornerRadius = (bounds.width + 20) / 2.0
glowLayer.backgroundColor = NSColor(red: 0.0, green: 0.70, blue: 1.0, alpha: 0.25).cgColor
glowLayer.shadowColor = NSColor(red: 0.0, green: 0.85, blue: 1.0, alpha: 0.85).cgColor
glowLayer.shadowRadius = 12
glowLayer.shadowOpacity = 0.45
glowLayer.shadowOffset = .zero glowLayer.shadowOffset = .zero
glowLayer.opacity = 0.0
layer.addSublayer(glowLayer) layer.addSublayer(glowLayer)
// 2. Chic, Minimal Solid-Color Core (Apple Obsidian Graphite Disc)
solidCoreLayer.frame = bounds
solidCoreLayer.cornerRadius = radius
solidCoreLayer.backgroundColor = NSColor(red: 0.12, green: 0.12, blue: 0.14, alpha: 0.98).cgColor
layer.addSublayer(solidCoreLayer)
// 3. Dynamic Voice-Reactive Stroke Ring (Disappears completely in silence)
// 2. Deep Sapphire/Midnight Blue Radial Core (Clean, minimal, luxury Apple design)
coreGradientLayer.frame = bounds
coreGradientLayer.cornerRadius = radius
coreGradientLayer.type = .radial
coreGradientLayer.colors = [
NSColor(red: 0.05, green: 0.55, blue: 0.95, alpha: 1.0).cgColor, // Vibrant Cerulean Center
NSColor(red: 0.12, green: 0.18, blue: 0.55, alpha: 0.98).cgColor, // Deep Royal Blue
NSColor(red: 0.06, green: 0.08, blue: 0.22, alpha: 0.98).cgColor // Midnight Obsidian Edge
]
coreGradientLayer.startPoint = CGPoint(x: 0.40, y: 0.40)
coreGradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
layer.addSublayer(coreGradientLayer)
// 3. Audio-Reactive Neon Stroke (Widening & Brightening dynamically with voice)
strokeBorderLayer.frame = bounds strokeBorderLayer.frame = bounds
strokeBorderLayer.cornerRadius = radius strokeBorderLayer.cornerRadius = radius
strokeBorderLayer.borderWidth = 0.0
strokeBorderLayer.borderColor = NSColor.white.cgColor
strokeBorderLayer.borderWidth = minStrokeWidth
strokeBorderLayer.borderColor = NSColor(red: 0.30, green: 0.90, blue: 1.0, alpha: 0.75).cgColor
strokeBorderLayer.backgroundColor = NSColor.clear.cgColor strokeBorderLayer.backgroundColor = NSColor.clear.cgColor
strokeBorderLayer.opacity = 0.0
layer.addSublayer(strokeBorderLayer) layer.addSublayer(strokeBorderLayer)
} }
/// Updates audio response: /// Updates audio response:
/// In silence (level <= 0.04): stroke and halo disappear completely.
/// When speaking: stroke widens powerfully (up to 8px) and halo blooms dynamically.
/// In silence (level ~ 0.0): crisp minimal 2px stroke, subtle soft glow.
/// When speaking: stroke powerfully thickens up to 7.5px, glow blooms to 42px, smooth gentle scale breathing.
func updateAudioLevel(_ level: Float, isLagging: Bool) { func updateAudioLevel(_ level: Float, isLagging: Bool) {
CATransaction.begin() CATransaction.begin()
CATransaction.setDisableActions(true) CATransaction.setDisableActions(true)
let rawClamped = CGFloat(min(max(level, 0.0), 1.0))
let silenceThreshold: CGFloat = 0.04
if rawClamped <= silenceThreshold {
// Complete silence: Stroke and Halo completely vanish
strokeBorderLayer.borderWidth = 0.0
strokeBorderLayer.opacity = 0.0
glowLayer.shadowOpacity = 0.0
glowLayer.shadowRadius = 0.0
glowLayer.opacity = 0.0
} else {
// Active speech: scale dynamic intensity noticeably
let norm = (rawClamped - silenceThreshold) / (1.0 - silenceThreshold)
let clamped = CGFloat(min(max(level, 0.0), 1.0))
if isLagging { if isLagging {
// Amber highlight for network degradation
let amber = NSColor(red: 1.0, green: 0.65, blue: 0.10, alpha: 0.95).cgColor
strokeBorderLayer.borderColor = amber
glowLayer.shadowColor = amber
// Amber warm warning for degraded internet
coreGradientLayer.colors = [
NSColor(red: 1.0, green: 0.75, blue: 0.15, alpha: 1.0).cgColor,
NSColor(red: 0.85, green: 0.40, blue: 0.05, alpha: 0.98).cgColor,
NSColor(red: 0.25, green: 0.10, blue: 0.02, alpha: 0.98).cgColor
]
let amberStroke = NSColor(red: 1.0, green: 0.80, blue: 0.25, alpha: 0.7 + (0.3 * clamped)).cgColor
strokeBorderLayer.borderColor = amberStroke
glowLayer.shadowColor = NSColor.orange.cgColor
} else { } else {
// Crisp luminous white glow
strokeBorderLayer.borderColor = NSColor.white.cgColor
glowLayer.shadowColor = NSColor.white.cgColor
// Standard crisp Apple Intelligence sapphire palette
coreGradientLayer.colors = [
NSColor(red: 0.05, green: 0.55 + (0.40 * clamped), blue: 0.95 + (0.05 * clamped), alpha: 1.0).cgColor,
NSColor(red: 0.12, green: 0.18, blue: 0.55, alpha: 0.98).cgColor,
NSColor(red: 0.06, green: 0.08, blue: 0.22, alpha: 0.98).cgColor
]
let neonStroke = NSColor(
red: 0.30 + (0.60 * clamped),
green: 0.90 + (0.10 * clamped),
blue: 1.0,
alpha: 0.65 + (0.35 * clamped)
).cgColor
strokeBorderLayer.borderColor = neonStroke
glowLayer.shadowColor = NSColor(red: 0.0, green: 0.85, blue: 1.0, alpha: 0.90).cgColor
} }
// 1. Noticeably powerful stroke expansion (from 0 to 8px)
let dynamicWidth = norm * maxStrokeWidth
// 1. Dynamic Stroke Expansion: thickens from 2.0px to 7.5px noticeably
let dynamicWidth = minStrokeWidth + (clamped * (maxStrokeWidth - minStrokeWidth))
strokeBorderLayer.borderWidth = dynamicWidth strokeBorderLayer.borderWidth = dynamicWidth
strokeBorderLayer.opacity = Float(0.40 + (0.60 * norm))
// 2. Dynamic bloom halo (expands up to 45px radius)
glowLayer.shadowRadius = 6.0 + (norm * 38.0)
glowLayer.shadowOpacity = Float(0.35 + (0.65 * norm))
glowLayer.opacity = Float(norm)
}
// 2. Dynamic Bloom Halo: blooms from 12px up to 42px
glowLayer.shadowRadius = 12.0 + (clamped * 30.0)
glowLayer.shadowOpacity = Float(0.45 + (0.50 * clamped))
// 3. Subtle organic breathing scale (1.0 to 1.14x max, smooth and stable without shaking)
let subtleScale = 1.0 + (clamped * 0.14)
self.layer?.sublayerTransform = CATransform3DMakeScale(subtleScale, subtleScale, 1.0)
CATransaction.commit() CATransaction.commit()
} }
@ -233,12 +244,10 @@ final class SolidSiriOrbView: NSView {
CATransaction.setDisableActions(false) CATransaction.setDisableActions(false)
CATransaction.setAnimationDuration(0.20) CATransaction.setAnimationDuration(0.20)
strokeBorderLayer.borderWidth = 4.0 strokeBorderLayer.borderWidth = 4.0
strokeBorderLayer.opacity = 1.0
strokeBorderLayer.borderColor = NSColor.orange.cgColor strokeBorderLayer.borderColor = NSColor.orange.cgColor
glowLayer.shadowColor = NSColor.orange.cgColor glowLayer.shadowColor = NSColor.orange.cgColor
glowLayer.shadowRadius = 24 glowLayer.shadowRadius = 24
glowLayer.shadowOpacity = 0.9 glowLayer.shadowOpacity = 0.9
glowLayer.opacity = 1.0
CATransaction.commit() CATransaction.commit()
} }
@ -246,12 +255,9 @@ final class SolidSiriOrbView: NSView {
CATransaction.begin() CATransaction.begin()
CATransaction.setDisableActions(true) CATransaction.setDisableActions(true)
strokeBorderLayer.borderWidth = 3.0 strokeBorderLayer.borderWidth = 3.0
strokeBorderLayer.opacity = 0.8
strokeBorderLayer.borderColor = NSColor.white.cgColor
glowLayer.shadowColor = NSColor.white.cgColor
glowLayer.shadowRadius = 16
glowLayer.shadowOpacity = 0.7
glowLayer.opacity = 0.8
strokeBorderLayer.borderColor = NSColor.cyan.cgColor
glowLayer.shadowRadius = 18
glowLayer.shadowOpacity = 0.8
CATransaction.commit() CATransaction.commit()
} }
@ -261,12 +267,10 @@ final class SolidSiriOrbView: NSView {
CATransaction.setAnimationDuration(0.18) CATransaction.setAnimationDuration(0.18)
let green = NSColor(red: 0.15, green: 0.95, blue: 0.45, alpha: 1.0).cgColor let green = NSColor(red: 0.15, green: 0.95, blue: 0.45, alpha: 1.0).cgColor
strokeBorderLayer.borderWidth = 4.0 strokeBorderLayer.borderWidth = 4.0
strokeBorderLayer.opacity = 1.0
strokeBorderLayer.borderColor = green strokeBorderLayer.borderColor = green
glowLayer.shadowColor = green glowLayer.shadowColor = green
glowLayer.shadowRadius = 22
glowLayer.shadowOpacity = 0.9
glowLayer.opacity = 1.0
glowLayer.shadowRadius = 24
glowLayer.shadowOpacity = 0.95
CATransaction.commit() CATransaction.commit()
} }
@ -276,23 +280,21 @@ final class SolidSiriOrbView: NSView {
CATransaction.setAnimationDuration(0.18) CATransaction.setAnimationDuration(0.18)
let red = NSColor(red: 1.0, green: 0.20, blue: 0.20, alpha: 1.0).cgColor let red = NSColor(red: 1.0, green: 0.20, blue: 0.20, alpha: 1.0).cgColor
strokeBorderLayer.borderWidth = 4.0 strokeBorderLayer.borderWidth = 4.0
strokeBorderLayer.opacity = 1.0
strokeBorderLayer.borderColor = red strokeBorderLayer.borderColor = red
glowLayer.shadowColor = red glowLayer.shadowColor = red
glowLayer.shadowRadius = 22
glowLayer.shadowOpacity = 0.9
glowLayer.opacity = 1.0
glowLayer.shadowRadius = 24
glowLayer.shadowOpacity = 0.95
CATransaction.commit() CATransaction.commit()
} }
func reset() { func reset() {
CATransaction.begin() CATransaction.begin()
CATransaction.setDisableActions(true) CATransaction.setDisableActions(true)
strokeBorderLayer.borderWidth = 0.0
strokeBorderLayer.opacity = 0.0
glowLayer.shadowRadius = 0.0
glowLayer.shadowOpacity = 0.0
glowLayer.opacity = 0.0
self.layer?.sublayerTransform = CATransform3DIdentity
strokeBorderLayer.borderWidth = minStrokeWidth
strokeBorderLayer.borderColor = NSColor(red: 0.30, green: 0.90, blue: 1.0, alpha: 0.75).cgColor
glowLayer.shadowRadius = 12
glowLayer.shadowOpacity = 0.45
CATransaction.commit() CATransaction.commit()
} }
} }
Loading…
Cancel
Save