引言
在 iOS 平台中,实现音频播放有多种方式。AVAudioPlayer
是一个专门用于播放音频数据的类,易于使用,适合处理简单的音频播放需求。而 AVPlayer
则是一种更通用的播放器,既能播放视频资源,也能处理音频内容,非常适合流媒体和多媒体应用。
然而,当我们需要实现更复杂的音频功能,比如音频节点的连接、实时音频处理,或是其他更精细的音频控制时,AVAudioEngine
就成为了一个不可或缺的工具。AVAudioEngine
提供了一个高度可扩展的音频处理架构,能够满足各种高级音频需求。
在最近的一个项目中,我需要实现一个播放背景音乐并显示音乐波形的功能。为了满足对音频播放的精确控制以及实时音频数据的获取,我采用了 AVAudioEngine
结合 AVAudioPlayerNode
的方式来实现。接下来,我将详细介绍如何使用这些工具来完成这个任务。
音频播放和采样
播放
首先创建了一个继承自NSObject的PHAudioPlayer类,我们定义了四个属性代码如下:
Swift
/// 音频地址
private var audioURL: URL?
/// 音频引擎
private var audioEngine = AVAudioEngine()
/// 播放器节点
private var audioPlayerNode = AVAudioPlayerNode()
private var audioFile: AVAudioFile?
一个自定义的初始化方法,传递音频的URL。
Swift
init(audioURL: URL? = nil) {
super.init()
self.audioURL = audioURL
self.setupAudioEngine()
self.playAudioFile(url: self.audioURL!)
}
首先设置音频引擎,并添加同步捕捉音频数据:
Swift
private func setupAudioEngine() {
// 加载音频文件
guard let audioURL = audioURL else {
CSAssert(false, "音频地址为空")
return
}
do {
let mainMixer = audioEngine.mainMixerNode
audioEngine.attach(audioPlayerNode)
audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: nil)
// 捕获音频数据
mainMixer.installTap(onBus: 0, bufferSize: 1024, format: mainMixer.outputFormat(forBus: 0)) {[weak self] buffer, time in
guard let self = self else { return }
self.handleAudioSampleData(buffer: buffer)
}
try audioEngine.start()
} catch {
CSLog.error(module: "PHAudioPlayer", "加载音频文件失败")
}
}
开始播放
Swift
/// 播放
func playAudioFile(url: URL) {
do {
audioFile = try AVAudioFile(forReading: url)
guard let audioFile = audioFile else { return }
let length = AVAudioFrameCount(audioFile.length)
audioPlayerNode.scheduleFile(audioFile, at: nil, completionHandler: {
self.playFinished()
})
audioPlayerNode.play()
} catch {
print("Error loading audio file: \(error.localizedDescription)")
}
}
处理音频数据
在mainMixer的回调中开始处理音频的样本数据,我们单独创建了一个方法代码如下:
Swift
/// 处理音频样本数据
private func handleAudioSampleData(buffer:AVAudioPCMBuffer) {
guard let channelData = buffer.floatChannelData?[0] else { return }
let frameLength = Int(buffer.frameLength)
// 获取音频样本数据
let samples = stride(from: 0, to: frameLength, by: buffer.stride).map { channelData[$0] }
/// 计算振幅
let amplitude = calculateRMS(samples: samples)
// 使用样本数据绘制波形
DispatchQueue.main.async {
self.delegate?.audioPlayer(self, amplitude: amplitude)
}
}
Swift
private func calculateRMS(samples: [Float]) -> Float {
let squareSum = samples.reduce(0.0) { $0 + $1 * $1 }
return sqrt(squareSum / Float(samples.count))
}
销毁
监听到播放完成之后,手动进行了循环播放,并创建一个主动的销毁方法,当页面退出时主动调用。
Swift
/// 播放完成
func playFinished() {
playAudioFile(url: audioURL!)
}
/// 销毁
func destroy() {
audioEngine.stop()
audioEngine.reset()
}
音频波形图绘制
波形绘制采用了贝塞尔曲线+图层遮罩的方式来实现,底部图层采用了渐变图层这样的波形会有一个顶部颜色和底部颜色不同的样式。具体代码如下:
Swift
class SVLPAudioVolumeView: UIView,PHAudioPlayerDelegate {
/// 缓存点个数
private let bufferSize = 200
/// 缓冲区
private var buffer = [Float]()
/// 渐变
private var gradientView = CLGradientView(startColor: .red, endColor: .green, direction: .topToBottom)
/// shaplayer
private var shapeLayer = CAShapeLayer()
/// path
private var path = UIBezierPath()
override init(frame: CGRect) {
super.init(frame: frame)
// 加bufferSize个0
for _ in 0..<bufferSize {
buffer.append(0.0)
}
addSubview(gradientView)
gradientView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
self.layer.mask = shapeLayer
shapeLayer.fillColor = UIColor.cyan.cgColor
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 2.0
shapeLayer.lineJoin = .round
shapeLayer.lineCap = .round
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
/// 音量振幅
func audioPlayer(_ player: PHAudioPlayer, amplitude: Float) {
if buffer.count < bufferSize {
buffer.append(amplitude)
} else {
buffer.removeFirst()
buffer.append(amplitude)
}
updatePath()
}
/// 更新path
func updatePath() {
path.removeAllPoints()
let width = bounds.width / CGFloat(bufferSize - 1)
let height = bounds.height
path.move(to: CGPoint(x: 0, y: height))
for (index, value) in buffer.enumerated() {
let x = CGFloat(index) * width
let y = height - CGFloat(value) * height
path.addLine(to: CGPoint(x: x, y: y))
}
path.addLine(to: CGPoint(x: bounds.width, y: bounds.height))
path.close()
shapeLayer.path = path.cgPath
}
}
结语
在本篇博客中,我们探讨了一种更为复杂但功能强大的音频播放方式,即使用 AVAudioEngine
实现音频的播放与处理。虽然相较于 AVAudioPlayer
和 AVPlayer
,这种方式的实现稍显复杂,但它为我们提供了更灵活的音频处理能力,特别是在实时获取音频样本数据和绘制音频波形图方面。
通过利用 AVAudioEngine
的实时处理功能,我们能够精确地获取音频样本中的音量信息,并基于此动态绘制音频的波形图。这种方法不仅展现了 AVAudioEngine
的强大功能,也为开发者提供了实现复杂音频需求的有效途径。