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

引言

在 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 实现音频的播放与处理。虽然相较于 AVAudioPlayerAVPlayer,这种方式的实现稍显复杂,但它为我们提供了更灵活的音频处理能力,特别是在实时获取音频样本数据和绘制音频波形图方面。

通过利用 AVAudioEngine 的实时处理功能,我们能够精确地获取音频样本中的音量信息,并基于此动态绘制音频的波形图。这种方法不仅展现了 AVAudioEngine 的强大功能,也为开发者提供了实现复杂音频需求的有效途径。

相关推荐
Eric.Lee20212 个月前
使用 pydub 的 AudioSegment 获取音频时长 - python 实现
开发语言·python·音视频·音频处理
DogDaoDao3 个月前
开源音频处理项目推荐【持续更新】
人工智能·深度学习·大模型·音视频·音频·音频处理·语音大模型
io_T_T5 个月前
python 音频处理(2)——提取PPG特征之whisper库的使用(2.1)
python·音频处理
Mac分享吧6 个月前
IINA for Mac v1.3.5 音视频软件 安装教程(保姆级)
macos·音频播放·视频播放器·iina·音视频播放
hqin_5207 个月前
vue 播放aac格式音频文件
前端·vue.js·aac·音频播放
2301_7971647110 个月前
低功耗运放D722,具有9MHz的高增益带宽积,转换速率为8.5V/μs
单片机·嵌入式硬件·音频处理·a/d转换放大
老艾的AI世界1 年前
98秒转录2.5小时音频,最强音频翻译神器IFW下载部署
人工智能·深度学习·ai·语音识别·音频处理·音频翻译·音频文字提取·音频转录
刘悦的技术博客1 年前
本地训练,开箱可用,Bert-VITS2 V2.0.2版本本地基于现有数据集训练(原神刻晴)
人工智能·python·ai·音频处理·bert-vits2
刘悦的技术博客1 年前
不懂乐理,也能扒谱,基于openvpi将mp3转换为midi乐谱(Python3.10)
人工智能·python·ai·音频处理·背景音乐·扒谱