Improve audio visualizer and add processing status indicators

This commit is contained in:
Beingpax 2026-01-10 22:22:11 +05:45
parent c530367a04
commit db50b490d4
2 changed files with 123 additions and 110 deletions

View File

@ -4,110 +4,117 @@ struct AudioVisualizer: View {
let audioMeter: AudioMeter let audioMeter: AudioMeter
let color: Color let color: Color
let isActive: Bool let isActive: Bool
private let barCount = 12 private let barCount = 15
private let minHeight: CGFloat = 5 private let barWidth: CGFloat = 3
private let maxHeight: CGFloat = 32 private let barSpacing: CGFloat = 2
private let barWidth: CGFloat = 3.5 private let minHeight: CGFloat = 4
private let barSpacing: CGFloat = 2.3 private let maxHeight: CGFloat = 28
private let hardThreshold: Double = 0.3
private let phases: [Double]
private let sensitivityMultipliers: [Double]
@State private var heights: [CGFloat]
@State private var barHeights: [CGFloat]
@State private var targetHeights: [CGFloat]
init(audioMeter: AudioMeter, color: Color, isActive: Bool) { init(audioMeter: AudioMeter, color: Color, isActive: Bool) {
self.audioMeter = audioMeter self.audioMeter = audioMeter
self.color = color self.color = color
self.isActive = isActive self.isActive = isActive
self.sensitivityMultipliers = (0..<barCount).map { _ in // Create smooth wave phases
Double.random(in: 0.2...1.9) self.phases = (0..<barCount).map { Double($0) * 0.4 }
} _heights = State(initialValue: Array(repeating: minHeight, count: barCount))
_barHeights = State(initialValue: Array(repeating: minHeight, count: barCount))
_targetHeights = State(initialValue: Array(repeating: minHeight, count: barCount))
} }
var body: some View { var body: some View {
HStack(spacing: barSpacing) { HStack(spacing: barSpacing) {
ForEach(0..<barCount, id: \.self) { index in ForEach(0..<barCount, id: \.self) { index in
RoundedRectangle(cornerRadius: 1.7) RoundedRectangle(cornerRadius: barWidth / 2)
.fill(color) .fill(color.opacity(0.85))
.frame(width: barWidth, height: barHeights[index]) .frame(width: barWidth, height: heights[index])
} }
} }
.onChange(of: audioMeter) { _, newValue in .onChange(of: audioMeter) { _, newValue in
if isActive { updateWave(level: isActive ? newValue.averagePower : 0)
updateBars(with: Float(newValue.averagePower))
} else {
resetBars()
}
} }
.onChange(of: isActive) { _, newValue in .onChange(of: isActive) { _, active in
if !newValue { if !active { resetWave() }
resetBars() }
}
private func updateWave(level: Double) {
let time = Date().timeIntervalSince1970
let amplitude = max(0, min(1, level))
// Boost lower levels for better visibility
let boosted = pow(amplitude, 0.7)
withAnimation(.easeOut(duration: 0.08)) {
for i in 0..<barCount {
let wave = sin(time * 8 + phases[i]) * 0.5 + 0.5
let centerDistance = abs(Double(i) - Double(barCount) / 2) / Double(barCount / 2)
let centerBoost = 1.0 - (centerDistance * 0.4)
let height = minHeight + CGFloat(boosted * wave * centerBoost) * (maxHeight - minHeight)
heights[i] = max(minHeight, height)
} }
} }
} }
private func updateBars(with audioLevel: Float) { private func resetWave() {
let rawLevel = max(0, min(1, Double(audioLevel))) withAnimation(.easeOut(duration: 0.2)) {
let adjustedLevel = rawLevel < hardThreshold ? 0 : (rawLevel - hardThreshold) / (1.0 - hardThreshold) heights = Array(repeating: minHeight, count: barCount)
let range = maxHeight - minHeight
let center = barCount / 2
for i in 0..<barCount {
let distanceFromCenter = abs(i - center)
let positionMultiplier = 1.0 - (Double(distanceFromCenter) / Double(center)) * 0.4
// Use randomized sensitivity
let sensitivityAdjustedLevel = adjustedLevel * positionMultiplier * sensitivityMultipliers[i]
let targetHeight = minHeight + CGFloat(sensitivityAdjustedLevel) * range
let isDecaying = targetHeight < targetHeights[i]
let smoothingFactor: CGFloat = isDecaying ? 0.6 : 0.3 // Adjusted smoothing
targetHeights[i] = targetHeights[i] * (1 - smoothingFactor) + targetHeight * smoothingFactor
// Only update if change is significant enough to matter visually
if abs(barHeights[i] - targetHeights[i]) > 0.5 {
withAnimation(
isDecaying
? .spring(response: 0.4, dampingFraction: 0.8)
: .spring(response: 0.3, dampingFraction: 0.7)
) {
barHeights[i] = targetHeights[i]
}
}
}
}
private func resetBars() {
withAnimation(.easeOut(duration: 0.15)) {
barHeights = Array(repeating: minHeight, count: barCount)
targetHeights = Array(repeating: minHeight, count: barCount)
} }
} }
} }
struct StaticVisualizer: View { struct StaticVisualizer: View {
private let barCount = 12 // Match AudioVisualizer dimensions
private let barWidth: CGFloat = 3.5 private let barCount = 14
private let staticHeight: CGFloat = 5.0 private let barWidth: CGFloat = 3
private let barSpacing: CGFloat = 2.3 private let staticHeight: CGFloat = 4
private let barSpacing: CGFloat = 2
let color: Color let color: Color
var body: some View { var body: some View {
HStack(spacing: barSpacing) { HStack(spacing: barSpacing) {
ForEach(0..<barCount, id: \.self) { index in ForEach(0..<barCount, id: \.self) { _ in
RoundedRectangle(cornerRadius: 1.7) RoundedRectangle(cornerRadius: barWidth / 2)
.fill(color) .fill(color.opacity(0.5))
.frame(width: barWidth, height: staticHeight) .frame(width: barWidth, height: staticHeight)
} }
} }
} }
} }
// MARK: - Processing Status Display (Transcribing/Enhancing states)
struct ProcessingStatusDisplay: View {
enum Mode {
case transcribing
case enhancing
}
let mode: Mode
let color: Color
private var label: String {
switch mode {
case .transcribing:
return "Transcribing"
case .enhancing:
return "Enhancing"
}
}
var body: some View {
VStack(spacing: 4) {
Text(label)
.foregroundColor(color)
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
.minimumScaleFactor(0.5)
ProgressAnimation(color: color)
}
.frame(height: 28) // Match AudioVisualizer maxHeight for no layout shift
}
}

View File

@ -123,29 +123,46 @@ struct ProcessingIndicator: View {
// MARK: - Progress Animation Component // MARK: - Progress Animation Component
struct ProgressAnimation: View { struct ProgressAnimation: View {
let color: Color
let animationSpeed: Double
private let dotCount = 5
private let dotSize: CGFloat = 3
private let dotSpacing: CGFloat = 2
@State private var currentDot = 0 @State private var currentDot = 0
@State private var timer: Timer? @State private var timer: Timer?
let animationSpeed: Double
init(color: Color = .white, animationSpeed: Double = 0.3) {
self.color = color
self.animationSpeed = animationSpeed
}
var body: some View { var body: some View {
HStack(spacing: 2) { HStack(spacing: dotSpacing) {
ForEach(0..<5, id: \.self) { index in ForEach(0..<dotCount, id: \.self) { index in
Circle() RoundedRectangle(cornerRadius: dotSize / 2)
.fill(Color.white.opacity(index <= currentDot ? 0.8 : 0.2)) .fill(color.opacity(index <= currentDot ? 0.85 : 0.25))
.frame(width: 3.5, height: 3.5) .frame(width: dotSize, height: dotSize)
} }
} }
.onAppear { .onAppear {
timer = Timer.scheduledTimer(withTimeInterval: animationSpeed, repeats: true) { _ in startAnimation()
currentDot = (currentDot + 1) % 7
if currentDot >= 5 { currentDot = -1 }
}
} }
.onDisappear { .onDisappear {
timer?.invalidate() timer?.invalidate()
timer = nil timer = nil
} }
} }
private func startAnimation() {
timer?.invalidate()
currentDot = 0
timer = Timer.scheduledTimer(withTimeInterval: animationSpeed, repeats: true) { _ in
currentDot = (currentDot + 1) % (dotCount + 2)
if currentDot > dotCount { currentDot = -1 }
}
}
} }
// MARK: - Prompt Button Component // MARK: - Prompt Button Component
@ -276,35 +293,21 @@ struct RecorderStatusDisplay: View {
let currentState: RecordingState let currentState: RecordingState
let audioMeter: AudioMeter let audioMeter: AudioMeter
let menuBarHeight: CGFloat? let menuBarHeight: CGFloat?
init(currentState: RecordingState, audioMeter: AudioMeter, menuBarHeight: CGFloat? = nil) { init(currentState: RecordingState, audioMeter: AudioMeter, menuBarHeight: CGFloat? = nil) {
self.currentState = currentState self.currentState = currentState
self.audioMeter = audioMeter self.audioMeter = audioMeter
self.menuBarHeight = menuBarHeight self.menuBarHeight = menuBarHeight
} }
var body: some View { var body: some View {
Group { Group {
if currentState == .enhancing { if currentState == .enhancing {
VStack(spacing: 2) { ProcessingStatusDisplay(mode: .enhancing, color: .white)
Text("Enhancing") .transition(.opacity)
.foregroundColor(.white)
.font(.system(size: 11, weight: .medium, design: .default))
.lineLimit(1)
.minimumScaleFactor(0.5)
ProgressAnimation(animationSpeed: 0.15)
}
} else if currentState == .transcribing { } else if currentState == .transcribing {
VStack(spacing: 2) { ProcessingStatusDisplay(mode: .transcribing, color: .white)
Text("Transcribing") .transition(.opacity)
.foregroundColor(.white)
.font(.system(size: 11, weight: .medium, design: .default))
.lineLimit(1)
.minimumScaleFactor(0.5)
ProgressAnimation(animationSpeed: 0.12)
}
} else if currentState == .recording { } else if currentState == .recording {
AudioVisualizer( AudioVisualizer(
audioMeter: audioMeter, audioMeter: audioMeter,
@ -312,10 +315,13 @@ struct RecorderStatusDisplay: View {
isActive: currentState == .recording isActive: currentState == .recording
) )
.scaleEffect(y: menuBarHeight != nil ? min(1.0, (menuBarHeight! - 8) / 25) : 1.0, anchor: .center) .scaleEffect(y: menuBarHeight != nil ? min(1.0, (menuBarHeight! - 8) / 25) : 1.0, anchor: .center)
.transition(.opacity)
} else { } else {
StaticVisualizer(color: .white) StaticVisualizer(color: .white)
.scaleEffect(y: menuBarHeight != nil ? min(1.0, (menuBarHeight! - 8) / 25) : 1.0, anchor: .center) .scaleEffect(y: menuBarHeight != nil ? min(1.0, (menuBarHeight! - 8) / 25) : 1.0, anchor: .center)
.transition(.opacity)
} }
} }
.animation(.easeInOut(duration: 0.2), value: currentState)
} }
} }