如何用SwiftUI实现随音频频谱变化的曲线音频波形UI?
问题描述
我希望实现的目标UI效果:
但目前实现的效果如下:
以下是我尝试实现的完整Swift代码:
struct Wave: Shape { var animatableData: Double { get { phase } set { self.phase = newValue } } var strength: Double var frequency: Double var phase: Double var curveAmount: Double func path(in rect: CGRect) -> Path { var path = Path() let width = Double(rect.width) let height = Double(rect.height) let midWidth = width / 2 let midHeight = height / 2 let start = CGPoint(x: rect.minX, y: midHeight) let end = CGPoint(x: rect.maxX, y: midHeight) let control = CGPoint(x: midWidth, y: rect.minY + strength * curveAmount) path.move(to: start) for x in stride(from: 0, through: width, by: 1) { let progress = x / width let parabolicY = QuadBezier(t: progress, start: start.y, c: control.y, end: end.y) let sineWave = sin(progress * frequency * 2 * .pi + phase) * (strength * 0.5) let y = parabolicY + sineWave path.addLine(to: CGPoint(x: x, y: y)) } return path } private func QuadBezier(t: Double, start: CGFloat, c: CGFloat, end: CGFloat) -> Double { let invT = 1 - t let b1 = 3 * t * (invT * invT) let b2 = 3 * (t * t) * invT let b3 = t * t * t let part1 = Double(start) * Double(invT * invT * invT) let part2 = Double(c) * Double(b1 + b2) let part3 = Double(end) * Double(b3) return part1 + part2 + part3 } } struct ContentView: View { @State private var phase = 0.0 var body: some View { ZStack { ForEach(0..<20) { i in Wave(strength: 10, frequency: 4, phase: self.phase, curveAmount: 30) .stroke(Color.white.opacity(Double(i) / 10), lineWidth: 5) .offset(y: CGFloat(i) * 1.2) } Wave(strength: 10, frequency: 0, phase: 0, curveAmount: 30) .stroke(Color.white, lineWidth: 5) .offset(y: 7 * 5) } .background(Color.blue) .edgesIgnoringSafeArea(.all) .onAppear { withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) { self.phase = .pi * 2 } } } }
我需要实现目标UI,同时让这个UI能根据麦克风输入的音频频谱变化呈现对应动画,请问该怎么修改代码?
解决方案
1. 调整波形形状,匹配目标UI的对称弧线
目标UI的波形是对称的向下凸起弧线,原代码的贝塞尔曲线计算逻辑有误,且叠加正弦波导致波形杂乱。修改Wave的路径生成逻辑:
struct Wave: Shape { var animatableData: Double { get { amplitude } set { self.amplitude = newValue } } // 控制弧线凸起幅度,后续关联音频强度 var amplitude: Double // 控制波形宽度比例(可按需调整) var widthRatio: Double = 0.8 func path(in rect: CGRect) -> Path { var path = Path() let width = Double(rect.width) let height = Double(rect.height) let centerX = width / 2 let centerY = height / 2 // 生成对称的左右控制点,绘制向下凸起的弧线 let controlOffsetX = width * widthRatio / 2 let controlY = centerY + amplitude let startPoint = CGPoint(x: rect.minX, y: centerY) let leftControl = CGPoint(x: centerX - controlOffsetX, y: controlY) let rightControl = CGPoint(x: centerX + controlOffsetX, y: controlY) let endPoint = CGPoint(x: rect.maxX, y: centerY) path.move(to: startPoint) // 用三次贝塞尔曲线绘制对称弧线 path.addCurve(to: endPoint, control1: leftControl, control2: rightControl) return path } }
2. 调整波形层级与透明度,模拟目标UI的渐变效果
目标UI是从下到上幅度递减、透明度降低的堆叠效果,修改ContentView的布局逻辑:
struct ContentView: View { // 后续关联麦克风输入的音频幅度值 @State private var audioAmplitude: Double = 15 let waveCount = 12 var body: some View { ZStack(alignment: .bottom) { Color.blue.edgesIgnoringSafeArea(.all) // 从下到上叠加波形,实现渐变效果 ForEach(0..<waveCount) { index in let progress = Double(index) / Double(waveCount) // 层级越高,波形幅度越小 let amplitude = audioAmplitude * (1 - progress * 0.6) // 层级越高,透明度越低 let opacity = 1 - progress * 0.8 Wave(amplitude: amplitude) .stroke(Color.white.opacity(opacity), lineWidth: 3) .offset(y: -CGFloat(index) * 4) } } } }
3. 接入麦克风音频频谱,实现动态响应
使用AVAudioEngine获取麦克风输入的音频功率数据,驱动波形动态变化:
import AVFoundation class AudioManager: ObservableObject { @Published var averageAmplitude: Double = 0 private let audioEngine = AVAudioEngine() private let mixerNode = AVAudioMixerNode() init() { setupAudioEngine() } func setupAudioEngine() { let inputNode = audioEngine.inputNode let inputFormat = inputNode.inputFormat(forBus: 0) audioEngine.attach(mixerNode) audioEngine.connect(inputNode, to: mixerNode, format: inputFormat) mixerNode.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) { buffer, time in guard let channelData = buffer.floatChannelData else { return } let channelDataArray = Array(UnsafeBufferPointer(start: channelData[0], count: Int(buffer.frameLength))) // 计算音频平均功率,转换为UI可用的幅度值 let sumSquares = channelDataArray.map { $0 * $0 }.reduce(0, +) let rms = sqrt(sumSquares / Double(buffer.frameLength)) // 限制幅度范围,避免UI过度变形 let amplitude = min(rms * 200, 50) DispatchQueue.main.async { self.averageAmplitude = amplitude } } do { try audioEngine.start() } catch { print("音频引擎启动失败:\(error.localizedDescription)") } } }
在ContentView中集成音频管理器:
struct ContentView: View { @StateObject private var audioManager = AudioManager() let waveCount = 12 var body: some View { ZStack(alignment: .bottom) { Color.blue.edgesIgnoringSafeArea(.all) ForEach(0..<waveCount) { index in let progress = Double(index) / Double(waveCount) let amplitude = audioManager.averageAmplitude * (1 - progress * 0.6) let opacity = 1 - progress * 0.8 Wave(amplitude: amplitude) .stroke(Color.white.opacity(opacity), lineWidth: 3) .offset(y: -CGFloat(index) * 4) } } } }
关键修改说明
- 替换原贝塞尔曲线逻辑,用三次贝塞尔生成对称的向下凸起弧线,匹配目标UI形状
- 调整波形堆叠规则,实现从下到上幅度递减、透明度降低的渐变效果
- 新增音频管理器,通过
AVAudioEngine获取麦克风输入的音频功率,驱动波形动态变化
内容的提问来源于stack exchange,提问作者steven lee
相关产品推荐
相关产品推荐

