Skip to main content

AVAudioEngine详解

·1278 words·6 mins
IOS AVAudio
Table of Contents

iOS 音频引擎 AVAudioEngine
#

AVAudioEngine 是 AVFoundation 框架中的核心类,用于构建音频处理图,即一系列音频单元的连接,可以实现复杂的音频处理任务。在 AVAudioEngine 中,你可以使用各种内置的音频单元 (AVAudioUnit) 来添加各种音频效果。以下是 AVAudioEngine 中可用的一些主要音频效果单元及其详细说明:

1. AVAudioUnitTimePitch
#

AVAudioUnitTimePitch 是 AVFoundation 框架中的一个音频单元,用于在 AVAudioEngine 的音频处理图中实时改变音频的时间和音调。这个单元允许你独立地调整音频的速度和音高,这对于音乐制作、音频编辑和实时音频效果非常有用。

主要功能
#
  1. 时间拉伸: AVAudioUnitTimePitch 可以改变音频的播放速度,即时间拉伸,而不改变音高。这在需要调整音频长度以匹配特定时间线的情况下很有用。
  2. 音调变化: 同时,这个单元也可以改变音频的音调(音高),而不改变播放速度。这在需要调整音频的音高以匹配其他音轨或音阶时非常有用。
  3. 独立控制: 最重要的是,AVAudioUnitTimePitch 允许你独立控制时间拉伸和音调变化,这意味着你可以只改变音高而不改变速度,或者反之亦然。
属性
#

AVAudioUnitTimePitch 提供了几个关键属性来控制其行为:

  • pitch: 一个浮点数,表示音高的变化。1.0 表示原始音高,小于1.0表示降低音高,大于1.0表示提高音高。
  • timePitchAlgorithm: 一个枚举值,表示所使用的时间拉伸和音调变化算法。可以是 .varispeed(仅改变播放速度)、.advanced(高级算法,提供独立的时间和音高控制)等。
  • pitchCorrection: 一个整数值,表示音高的半音变化。0 表示无变化,正数表示升高音高,负数表示降低音高。
  • rate: 一个浮点数,表示播放速度的变化。1.0 表示正常速度,小于1.0表示慢速播放,大于1.0表示快速播放。
使用示例
#

下面是一个使用 AVAudioUnitTimePitch 的简单示例:

import AVFoundation

let audioEngine = AVAudioEngine()
let timePitchUnit = AVAudioUnitTimePitch()

// 设置音调变化和播放速度
timePitchUnit.pitch = 1.5 // 提升一个半音
timePitchUnit.rate = 1.0 // 正常速度播放

// 将时间音调单元添加到音频引擎
audioEngine.attach(timePitchUnit)

// 连接输入和输出节点
let inputNode = audioEngine.inputNode
let outputNode = audioEngine.outputNode
audioEngine.connect(inputNode, to: timePitchUnit, format: nil)
audioEngine.connect(timePitchUnit, to: outputNode, format: nil)

// 准备并启动音频引擎
do {
    try audioEngine.start()
} catch {
    print("Error starting audio engine: \(error)")
}

2. AVAudioUnitReverb(混响)
#

提供混响效果,可以模拟不同环境下的声音反射,用于营造空间感和深度感。

在音频处理领域,混响(Reverb)效果用于模拟声波在封闭空间中的反射,从而给声音添加深度和空间感。不同的混响算法和插件可以模拟各种环境,从小房间到大教堂,甚至是特殊的声学效果。在 AVAudioUnitReverb 类中,Apple 提供了几种预设的混响类型,但它们并不是所有可能的混响效果。下面列举的是 AVAudioUnitReverb 类中可用的几种预设混响类型:

  1. SmallRoom - 模拟一个小房间的声学环境。
  2. MediumRoom - 模拟一个中等大小房间的声学环境。
  3. LargeRoom - 模拟一个大房间的声学环境。
  4. MediumHall - 模拟一个中等大小的音乐厅的声学环境。
  5. LargeHall - 模拟一个大音乐厅的声学环境。
  6. Plate - 模拟早期的金属板混响器(plate reverb)效果。
  7. MediumChamber - 模拟一个中等大小的录音室的声学环境。
  8. LargeChamber - 模拟一个大录音室的声学环境。
  9. Cathedral - 模拟大教堂的声学环境,通常有很长的尾音。
  10. Room - 一个通用的房间混响效果,可以进一步定制。
  11. Hall - 一个通用的大厅混响效果,可以进一步定制。
  12. Chamber - 一个通用的小室混响效果,可以进一步定制。
  13. LiveRoom - 模拟现场表演场地的声学环境。

除了上述预设,AVAudioUnitReverb 还允许你自定义混响的参数,例如:

  • Wet/Dry Mix - 控制原始干声和混响湿声的混合比例。
  • Decay Time - 控制混响尾音消失的时间长度。
  • Diffusion - 控制声波的散射程度,影响混响的密度。
  • Density - 控制早期反射的数量和分布。
  • High Frequency Damping - 控制高频混响的衰减程度。
  • Lowpass Filter - 控制混响信号的高频上限。

除了 AVAudioUnitReverb 提供的预设,还有许多第三方插件和软件提供了更为专业和多样化的混响效果。这些插件通常在专业音频工作站(DAWs)中使用,如 Logic Pro、Pro Tools、Ableton Live 等,提供了更高级的控制和仿真效果,例如:

  • Convolution Reverb - 基于真实空间的声学响应(Impulse Response)来模拟混响。
  • Modulated Reverb - 添加调制效果的混响,如颤音或镶边。
  • Algorithmic Reverb - 使用数学算法而非真实空间的声学响应来模拟混响。
  • Spring Reverb - 模拟早期弹簧混响器的效果。
  • Ambience - 专注于模拟环境声音,如风声、雨声等。

每种混响类型都有其独特的声学特性,选择哪种类型取决于你希望达到的特定声学效果。

参数:wetDryMix 是混响效果的湿干比参数。它表示原始信号(干信号)和混响信号(湿信号)的混合比例。值范围是 0 到 100,其中 0 表示完全干信号(无混响),100 表示完全湿信号(只有混响)。

3. AVAudioUnitDistortion(失真效果)
#

用于添加失真效果,可以产生摇滚音乐中常见的吉他放大器失真效果。

4. AVAudioUnitEQ(均衡器)
#

均衡器单元,允许你调整音频的频率响应,通过增加或减少特定频率的增益,可以调整音色和清晰度。

AVAudioUnitEQ 是一个强大的多频段均衡器类,提供了丰富的属性和方法来调整音频信号的频率响应。以下是 AVAudioUnitEQ 及其关键属性的详细说明:

AVAudioUnitEQ 是 AVAudioUnitEffect 的子类,用于实现多频段均衡器。它包含多个 AVAudioUnitEQFilterParameters 对象,每个对象代表一个频段。

关键属性
#

bands
#
  • 类型: [AVAudioUnitEQFilterParameters]

  • 说明: 这是一个数组,包含均衡器的所有频段。每个频段由 AVAudioUnitEQFilterParameters 对象表示。

  • 示例:

let eq = AVAudioUnitEQ(numberOfBands: 10)

let bands = eq.bands

globalGain
#
  • 类型: Float

  • 说明: 设置均衡器的全局增益,以分贝 (dB) 为单位。可以用来整体提升或降低音量。

  • 示例: eq.globalGain = 1.0 // 增加全局增益1dB

AVAudioUnitEQFilterParameters
#

AVAudioUnitEQFilterParameters 是 AVAudioUnitEQ 的一个子类,表示均衡器的单个频段。它包含以下关键属性:

filterType
#
  • 类型: AVAudioUnitEQFilterType

  • 说明: 设置滤波器的类型。可选值包括 .parametric, .lowPass, .highPass, .resonantLowPass, .resonantHighPass, .bandPass, .bandStop, .lowShelf, .highShelf, .resonantLowShelf, .resonantHighShelf。

  • 示例: band.filterType = .parametric

public enum AVAudioUnitEQFilterType :Int{
  case Parametric 参量均衡器 可以通过设置一些参量,来调节咱们均衡器的频点
  case LowPass 低通滤波器 衰弱高频
  case HighPass 高通滤波器 衰弱低频
  case ResonantLowPass  可以引发共鸣的 低通滤波器
  case ResonantHighPass 可以引发共鸣的  高通滤波器
  case BandPass 带通滤波器  提升某一频率附近的信号 忽略过高  过低的 部分
  case BandStop 与上面的相反  忽略某一频率附近的信号
  case LowShelf 低架 降低整体
  case HighShelf 高架 提升整体
  case ResonantLowShelf  可以引发共鸣的 低架
  case ResonantHighShelf可以引发共鸣的 高架
}
frequency
#
  • 类型: Float

  • 说明: 设置滤波器的中心频率,以赫兹 (Hz) 为单位。

  • 示例: band.frequency = 1000.0 // 设置中心频率为1000Hz

bandwidth
#
  • 类型: Float

  • 说明: 设置滤波器的带宽,以八度 (octaves) 为单位。仅适用于参数均衡器类型。

  • 示例: band.bandwidth = 1.0 // 设置带宽为1个八度

gain
#
  • 类型: Float

  • 说明: 设置滤波器的增益,以分贝 (dB) 为单位。正值表示提升,负值表示衰减。

  • 示例: band.gain = 5.0 // 增加增益5dB

bypass
#

  • 类型: Bool

  • 说明: 设置是否绕过此频段。true 表示绕过,false 表示启用。

  • 示例: band.bypass = false // 启用此频段

示例代码
#

import AVFoundation

// 创建音频引擎
let audioEngine = AVAudioEngine()

// 创建均衡器,设置为10个频段
let eq = AVAudioUnitEQ(numberOfBands: 10)

// 配置每个频段
for i in 0..<eq.bands.count {
    let band = eq.bands[i]
    band.filterType = .parametric
    band.frequency = Float(32 * pow(2.0, Double(i))) // 设置频率
    band.bandwidth = 0.5 // 设置带宽
    band.gain = 0.0 // 设置增益
    band.bypass = false // 启用频段
}

// 设置全局增益
eq.globalGain = 1.0

// 将均衡器添加到音频引擎
audioEngine.attach(eq)

// 连接均衡器到主混音器节点
audioEngine.connect(eq, to: audioEngine.mainMixerNode, format: nil)

// 启动音频引擎
do {
    try audioEngine.start()
} catch {
    print("音频引擎启动失败: \(error)")
}

// 调整某个频段的增益
eq.bands[0].gain = 5.0 // 增加第一个频段的增益

5. AVAudioUnitDelay
#

延迟效果器,可以创建回声效果,常用于电子音乐制作和空间感增强。

6. AVAudioUnitCompressor(压缩器)
#

压缩器,用于控制音频的动态范围,可以防止峰值过载,同时使较弱的声音更加清晰。

7. AVAudioUnitFlanger
#

镶边器,可以产生一种类似于旋转扬声器的效果,常用于创造特殊的声音质感。

8. AVAudioUnitPhaser
#

相位器,通过对音频信号的不同频率进行相位移,可以产生扫频效果。

9. AVAudioUnitDistortionFactory(失真效果)
#

AVAudioUnitDistortionFactory 是 AVAudioEngine 中的一个类,它提供了一系列预设的失真效果单元。这些失真效果可以应用于音频信号,以产生不同的音色变化,尤其是在音乐制作中非常有用,例如在电吉他音轨上添加失真效果。

以下是在 AVAudioUnitDistortionFactory 中可获得的所有预设失真单元:

  1. Overdrive - 这种失真类型通常模仿真空管放大器的自然过载特性,产生温暖而柔和的失真效果。
  2. Fuzz - Fuzz 失真通常会产生更加粗糙和饱和的音色,它会将输入信号的波峰削平,导致大量谐波的产生。
  3. Distortion - Distortion 类型的失真通常比 Overdrive 更加激进,它可以产生强烈的非线性失真效果,适合硬摇滚和重金属风格的音乐。
  4. Saturator - Saturator 效果模拟了磁带饱和或数字剪切的特性,可以产生更细腻的失真,通常用于轻微增强音色。
  5. Tube - 这个预设模拟了真空管设备的特性,产生温暖且富有音乐性的失真。
  6. Soft Clip - Soft clip 效果会平滑地限制信号的峰值,避免突然的音量尖峰,同时保留信号的基本音色。
  7. Hard Clip - Hard clip 则会更剧烈地剪切信号峰值,产生更明显的失真和削波效果。
  8. Foldback - Foldback 失真会在信号超过一定阈值时,将信号反向折叠,产生独特的音色变化。
  9. Wave Shaper - Wave shaper 允许对信号波形进行任意形状的变换,可以产生从轻微到极端的各种失真效果。
  10. Bit Crusher - Bit crusher 效果通过降低音频的比特深度来产生数字噪声和失真,通常用于创造电子音乐中的粗糙质感。
  11. Sample and Hold - Sample and Hold 效果会在随机或定期的时间间隔内“采样”信号的瞬时值,并保持该值直到下一次采样,产生断断续续的音色变化。

10. AVAudioUnitChorus
#

合唱效果器,通过轻微延迟和调制信号的叠加,可以模仿多个演奏者的合奏效果。

11. AVAudioUnitAudioUnit
#

这个单元允许你加载外部的 Audio Unit 插件,极大地扩展了 AVAudioEngine 的功能,可以使用第三方音频效果和乐器。

12. AVAudioUnitSampler
#

采样器单元,用于加载和播放采样音频,可以用来构建自己的乐器。

13. AVAudioUnitInstrument
#

乐器单元,可以加载和播放 MIDI 信息,用于合成音乐。

14. AVAudioUnitVarispeed(播放速度)
#

变速单元,可以改变音频的速度和播放时间,但不会改变音调。

15. AVAudioUnitRecorder
#

录音单元,用于录制音频流到文件,可以实现实时录音功能。

16. AVAudioUnitTimeStretch
#

时间拉伸单元,可以改变音频的播放时间而不改变音调,适用于节奏调整。

17. AVAudioUnitMIDI
#

MIDI 单元,用于处理 MIDI 信息,可以将 MIDI 控制信息转换为音频输出。

18. AVAudioUnitSampler
#

虽然已经提过一次,但是值得注意的是,AVAudioUnitSampler 可以作为复杂的乐器引擎使用,它支持多种触发模式和多种音色切换。

19. AVAudioUnitGenerator
#

音频生成单元,可以生成特定的音频波形,如正弦波、方波等。

20. AVAudioUnitMixer
#

混音器单元,用于混合多个音频流,可以调整各路音频的音量和平衡。

21. AVAudioUnitEffect
#

一个通用效果单元,可以作为其他效果单元的基类,但通常不会直接实例化。

22. AVAudioUnitEQFilter
#

虽然 AVAudioUnitEQ 提供了均衡器功能,但 AVAudioUnitEQFilter 提供了更细粒度的滤波器控制,可以单独设置每个频段的参数。

使用示例
#

使用这些单元时,你需要将它们添加到 AVAudioEngine 中,并通过 connect 方法将它们连接起来形成一个处理链。例如,下面的代码展示了如何将均衡器和混响效果添加到音频引擎中:

let audioEngine = AVAudioEngine()
let eq = AVAudioUnitEQ(numberOfBands: 3)
let reverb = AVAudioUnitReverb()

// 添加单元
audioEngine.attach(eq)
audioEngine.attach(reverb)

// 连接单元
let inputNode = audioEngine.inputNode
let outputNode = audioEngine.outputNode
audioEngine.connect(inputNode, to: eq, format: nil)
audioEngine.connect(eq, to: reverb, format: nil)
audioEngine.connect(reverb, to: outputNode, format: nil)

注意事项
#

在音频处理链中,效果的顺序会影响最终的音频输出。以下是一些常见的音频效果及其通常的排列顺序:

动态处理(Dynamic Processing): 包括压缩器(Compressor)、限制器(Limiter)等。这些效果通常放在最前面,以控制音频信号的动态范围。

均衡器(Equalizer): 用于调整音频信号的频率响应。通常在动态处理之后,以便在处理动态范围后进行频率调整。

失真(Distortion): 包括过载(Overdrive)、失真(Distortion)等。这些效果通常放在均衡器之后,以便在频率调整后添加失真效果。

调制(Modulation): 包括合唱(Chorus)、镶边(Flanger)、相位(Phaser)等。这些效果通常放在失真之后,以便在失真处理后添加调制效果。

延迟(Delay): 用于添加回声效果。通常放在调制效果之后,以便在调制处理后添加延迟效果。

混响(Reverb): 用于模拟空间效果。通常放在延迟之后,以便在延迟处理后添加混响效果。

获取频谱信息
#

目标需求
#

如何获取音频数据的频谱信息,根据音乐的律动,实现频谱可视化柱状图振幅效果?

思路方案
#

要实现音频频谱可视化柱状图振幅效果,你需要从音频数据中提取频谱信息。可以使用 AVAudioEngine 和 AVAudioPCMBuffer 来获取音频数据,并使用快速傅里叶变换(FFT)来计算频谱。

以下是一个简化的示例,展示如何获取音频数据的频谱信息并实现频谱可视化柱状图振幅效果:

获取音频数据并计算频谱
#

使用 AVAudioEngine 和 AVAudioPCMBuffer 获取音频数据。

使用 vDSP 库中的 FFT 函数计算频谱。

import AVFoundation
import Accelerate

class SpectrumAnalyzer {
    private var fftSetup: FFTSetup?
    private var log2n: vDSP_Length
    var bufferSize: Int // 将访问级别更改为 internal
    private var window: [Float]
    private var outputBuffer: [Float]
    private var frequencyData: [Float]
    private var downsampleFactor: Int

    init(bufferSize: Int, downsampleFactor: Int = 10) { // 默认降采样因子为10
        self.bufferSize = bufferSize
        self.downsampleFactor = downsampleFactor
        self.log2n = vDSP_Length(log2(Float(bufferSize)))
        self.fftSetup = vDSP_create_fftsetup(log2n, Int32(kFFTRadix2))
        self.window = [Float](repeating: 0, count: bufferSize)
        vDSP_hann_window(&window, vDSP_Length(bufferSize), Int32(vDSP_HANN_NORM))
        self.outputBuffer = [Float](repeating: 0, count: bufferSize / 2)
        self.frequencyData = [Float](repeating: 0, count: bufferSize / 2)
    }

    deinit {
        if let fftSetup = fftSetup {
            vDSP_destroy_fftsetup(fftSetup)
        }
    }

    func analyze(buffer: AVAudioPCMBuffer) -> [Float] {
        guard let fftSetup = fftSetup else { return [] }

        let frameCount = buffer.frameLength
        var realp = [Float](repeating: 0, count: Int(frameCount / 2))
        var imagp = [Float](repeating: 0, count: Int(frameCount / 2))
        var magnitudes = [Float](repeating: 0.0, count: Int(frameCount / 2))

        realp.withUnsafeMutableBufferPointer { realpPtr in
            imagp.withUnsafeMutableBufferPointer { imagpPtr in
                var output = DSPSplitComplex(realp: realpPtr.baseAddress!, imagp: imagpPtr.baseAddress!)

                buffer.floatChannelData?.pointee.withMemoryRebound(to: DSPComplex.self, capacity: Int(frameCount)) { (inputData) in
                    vDSP_ctoz(inputData, 2, &output, 1, vDSP_Length(frameCount / 2))
                }

                vDSP_fft_zrip(fftSetup, &output, 1, log2n, FFTDirection(FFT_FORWARD))

                vDSP_zvmags(&output, 1, &magnitudes, 1, vDSP_Length(frameCount / 2))
            }
        }

        return downsample(magnitudes, factor: downsampleFactor)
    }

    private func downsample(_ data: [Float], factor: Int) -> [Float] {
        guard factor > 0 else { return data }
        let downsampledCount = data.count / factor
        var downsampledData = [Float](repeating: 0.0, count: downsampledCount)
        for i in 0..<downsampledCount {
            let start = i * factor
            let end = start + factor
            let sum = data[start..<end].reduce(0, +)
            downsampledData[i] = sum / Float(factor)
        }
        return downsampledData
    }
}

绑定AVAudioEngine
#

将频谱数据传递给 UI 层,更新柱状图的振幅。

以下是一个示例代码,展示如何实现这些步骤:

import AVFoundation
import MediaPlayer

class AudioEnginePlayer {
   //...
   var onSpectrumDataAvailable: (([Float]) -> Void)?
   //...
   private var playerNode: AVAudioPlayerNode
   private var spectrumAnalyzer: SpectrumAnalyzer
   //...
  
   /// 播放状态
    var isPlaying: Bool = false {
        didSet {
            if !isPlaying {
                // 如果没有播放,则所有频谱柱显示为 0 值
                onSpectrumDataAvailable?([Float](repeating: 0.0, count: 24))
            }
        }
    }
   
   init() {
       //...
        playerNode = AVAudioPlayerNode()
      //...
        spectrumAnalyzer = SpectrumAnalyzer(bufferSize: 1024, downsampleFactor: 10)
        //...
        setupAudioEngineTap()
    }
  
   //...
   private func setupAudioEngineTap() {
        audioEngine.mainMixerNode.installTap(onBus: 0, bufferSize: AVAudioFrameCount(spectrumAnalyzer.bufferSize), format: audioEngine.mainMixerNode.outputFormat(forBus: 0)) { [weak self] (buffer, time) in
            guard let self = self else { return }
            let magnitudes = self.spectrumAnalyzer.analyze(buffer: buffer)
            DispatchQueue.main.async {
                if self.isPlaying {
                    //print("\(magnitudes)\n\n\n")
                    self.onSpectrumDataAvailable?(magnitudes)
                }
            }
        }
    }
   //...
}

创建频谱显示视图
#

创建一个 VisualizerView 类,用于绘制频谱柱状图。

在 updateUI 方法中调用 VisualizerView 的更新方法。

import UIKit

class VisualizerView: UIView {
    private var magnitudes: [Float] = []

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = .black // 设置背景颜色
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.backgroundColor = .black // 设置背景颜色
    }

    func update(with magnitudes: [Float]) {
        self.magnitudes = magnitudes
        setNeedsDisplay()
    }

    override func draw(_ rect: CGRect) {
        guard !magnitudes.isEmpty else { return }

        let context = UIGraphicsGetCurrentContext()
        context?.clear(rect)

        let barWidth = rect.width / CGFloat(magnitudes.count)
        for (index, magnitude) in magnitudes.enumerated() {
            let barHeight = CGFloat(magnitude) * rect.height
            let barRect = CGRect(x: CGFloat(index) * barWidth, y: rect.height - barHeight, width: barWidth, height: barHeight)
            context?.setFillColor(UIColor.green.cgColor) // 设置填充颜色
            context?.fill(barRect)
        }

        // 添加调试日志
        //print("Drawing \(magnitudes.count) bars")
    }
}

将频谱数据添加到界面
#

在视图控制器中添加 VisualizerView 并将频谱数据传递给它

import UIKit

class ViewController: UIViewController {
    private var visualizerView: VisualizerView!
    lazy var audioEnginePlayer = AudioEnginePlayer()

    override func viewDidLoad() {
        super.viewDidLoad()

        visualizerView = VisualizerView(frame: CGRectMake(0, 0, view.bounds.width, 300))
        view.addSubview(visualizerView)
        view.sendSubviewToBack(visualizerView)
        
        audioEnginePlayer.onSpectrumDataAvailable = { [weak self] magnitudes in
            //print(magnitudes)
            self?.visualizerView.update(with: magnitudes)
        }
    }
}
Pin
Author
Pin
A little bit about you