| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- import SwiftUI
- // MARK: - LiveWaveformView
- /// A real-time audio waveform visualization rendered via SwiftUI `Canvas`.
- /// Uses a highly minimalist, monochrome, line-drawn style suitable for business applications.
- struct LiveWaveformView: View {
- /// Audio amplitude samples (0.0–1.0). Most recent sample is last.
- let samples: [Float]
- /// Height of the waveform view.
- var height: CGFloat = 80
- /// Width of each bar in points.
- private let barWidth: CGFloat = 1.0
- /// Spacing between bars in points.
- private let barSpacing: CGFloat = 2.0
- /// Minimum bar height to ensure visibility even at zero amplitude.
- private let minBarHeight: CGFloat = 1.0
- var body: some View {
- Canvas { context, size in
- let totalBarSlot = barWidth + barSpacing
- let maxBars = Int(size.width / totalBarSlot)
- let maxHeight = size.height
- let centerY = size.height / 2
- // Take the most recent N samples that fit on screen
- let visibleSamples: [Float]
- if samples.count > maxBars {
- visibleSamples = Array(samples.suffix(maxBars))
- } else {
- visibleSamples = samples
- }
- let barCount = visibleSamples.count
- // Draw center reference line
- let centerLinePath = Path { path in
- path.move(to: CGPoint(x: 0, y: centerY))
- path.addLine(to: CGPoint(x: size.width, y: centerY))
- }
- context.stroke(
- centerLinePath,
- with: .color(Color.primary.opacity(0.1)),
- lineWidth: 1
- )
- // Draw bars right-aligned (most recent on the right)
- for index in 0..<barCount {
- let sample = CGFloat(max(visibleSamples[index], 0))
- let barHeight = max(sample * maxHeight * 0.75, minBarHeight)
- // Position: right-aligned
- let xOffset = size.width - CGFloat(barCount - index) * totalBarSlot
- let barRect = CGRect(
- x: xOffset,
- y: centerY - barHeight / 2,
- width: barWidth,
- height: barHeight
- )
- let barPath = Path(barRect)
- // Opacity fade on the left edge (older samples fade out)
- let fadeWidth = 0.25 // 25% of visible area fades
- let opacity: Double
- if barCount > 1 {
- let normalizedX = CGFloat(index) / CGFloat(barCount - 1)
- if normalizedX < fadeWidth {
- opacity = Double(normalizedX / fadeWidth)
- } else {
- opacity = 1.0
- }
- } else {
- opacity = 1.0
- }
- context.opacity = opacity
- // Simple monochrome fill (Color.primary)
- context.fill(barPath, with: .color(Color.primary.opacity(0.85)))
-
- context.opacity = 1.0 // Reset
- }
- }
- .frame(height: height)
- .background(
- RoundedRectangle(cornerRadius: 8)
- .fill(Color.primary.opacity(0.01)) // Semi-transparent minimal background
- )
- .businessBorder(cornerRadius: 8) // Thin outlined border
- }
- }
- // MARK: - Preview
- #Preview("Waveform — Active") {
- ZStack {
- Color.spaceBlack.ignoresSafeArea()
- VStack(spacing: 20) {
- LiveWaveformView(
- samples: (0..<80).map { _ in Float.random(in: 0.05...0.9) }
- )
- .padding(.horizontal)
- LiveWaveformView(
- samples: (0..<40).map { i in
- Float(sin(Double(i) * 0.3) * 0.4 + 0.5)
- }
- )
- .padding(.horizontal)
- }
- }
- }
|