You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用SwiftUI实现随音频频谱变化的曲线音频波形UI?

问题描述

我希望实现的目标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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 21:30:56