音频播放+音频采样(绘制音波)

引言

在 iOS 平台中,实现音频播放有多种方式。AVAudioPlayer 是一个专门用于播放音频数据的类,易于使用,适合处理简单的音频播放需求。而 AVPlayer 则是一种更通用的播放器,既能播放视频资源,也能处理音频内容,非常适合流媒体和多媒体应用。

然而,当我们需要实现更复杂的音频功能,比如音频节点的连接、实时音频处理,或是其他更精细的音频控制时,AVAudioEngine 就成为了一个不可或缺的工具。AVAudioEngine 提供了一个高度可扩展的音频处理架构,能够满足各种高级音频需求。

在最近的一个项目中,我需要实现一个播放背景音乐并显示音乐波形的功能。为了满足对音频播放的精确控制以及实时音频数据的获取,我采用了 AVAudioEngine 结合 AVAudioPlayerNode 的方式来实现。接下来,我将详细介绍如何使用这些工具来完成这个任务。

音频播放和采样

播放

首先创建了一个继承自NSObject的PHAudioPlayer类,我们定义了四个属性代码如下:

    /// 音频地址
    private var audioURL: URL?
    /// 音频引擎
    private var audioEngine = AVAudioEngine()
    /// 播放器节点
    private var audioPlayerNode = AVAudioPlayerNode()
    
    private var audioFile: AVAudioFile?

一个自定义的初始化方法,传递音频的URL。

    init(audioURL: URL? = nil) {
        super.init()
        self.audioURL = audioURL
        self.setupAudioEngine()
        self.playAudioFile(url: self.audioURL!)
    }

首先设置音频引擎,并添加同步捕捉音频数据:

    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", "加载音频文件失败")
        }
    }

开始播放

    /// 播放
    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的回调中开始处理音频的样本数据,我们单独创建了一个方法代码如下:

    /// 处理音频样本数据
    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)
        }
    }
    private func calculateRMS(samples: [Float]) -> Float {
        let squareSum = samples.reduce(0.0) { $0 + $1 * $1 }
        return sqrt(squareSum / Float(samples.count))
    }

销毁

监听到播放完成之后,手动进行了循环播放,并创建一个主动的销毁方法,当页面退出时主动调用。

    /// 播放完成
    func playFinished() {
        playAudioFile(url: audioURL!)
    }
    
    /// 销毁
    func destroy() {
        audioEngine.stop()
        audioEngine.reset()
    }

音频波形图绘制

波形绘制采用了贝塞尔曲线+图层遮罩的方式来实现,底部图层采用了渐变图层这样的波形会有一个顶部颜色和底部颜色不同的样式。具体代码如下:

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 的强大功能,也为开发者提供了实现复杂音频需求的有效途径。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值