Audio Kit 查看文件波形的相关源代码,easy o

802 阅读2分钟

Audio Kit 查看文件波形的相关源代码,看了下,挺简单的

拿到音频采样数据,用视图去展现,完了

1,拿到音频采样数据

获取的浮点数数据,长这样

-0.014434814
-0.016998291
-0.0184021
-0.017547607
// ...
获取的浮点数数据,在 -1 到 1 之间

1.1 拿到音频文件


let url = Bundle.main.resourceURL?.appendingPathComponent("Samples/beat.aiff")
let file = try! AVAudioFile(forReading: url!)

1.2 拿到第一个声道的数据

    public
    convenience init?(file: AVAudioFile) {
        let size = Int(file.length)
        self.init(count: size)

        guard let data = file.toFloatChannelData() else { return nil }
        // 循环大法好
        for i in 0 ..< size {
            // 取第一个声道的数据
            self[i] = data[0][i]
        }
    }
1.2.1 获取声道的浮点数数据
 public func toFloatChannelData() -> FloatChannelData? {
        guard let pcmBuffer = toAVAudioPCMBuffer(),
            let data = pcmBuffer.toFloatChannelData() else { return nil }
        return data
    }

音频文件转 pcm buffer,

public func toAVAudioPCMBuffer() -> AVAudioPCMBuffer? {
        guard let buffer = AVAudioPCMBuffer(pcmFormat: processingFormat,
                                            frameCapacity: AVAudioFrameCount(length)) else { return nil }
        do {
            framePosition = 0
            // 文件的数据,直接读进 buffer
            try read(into: buffer)
        } catch let error as NSError {
        return buffer
    }

获取音频 pcm buffer 里面的浮点数数据

public func toFloatChannelData() -> FloatChannelData? {
        guard let pcmFloatChannelData = floatChannelData else {
            return nil
        }

        let channelCount = Int(format.channelCount)
        let frameLength = Int(self.frameLength)
        let stride = self.stride

        var result = Array(repeating: [Float](zeros: frameLength), count: channelCount)

        // 每一帧的数据,分为多个通道,
        // 立体声有左声道,和右声道
        for channel in 0 ..< channelCount {
            // 获取每一帧的数据
            for sampleIndex in 0 ..< frameLength {
                result[channel][sampleIndex] = pcmFloatChannelData[channel][sampleIndex * stride]
            }
        }

        return result
    }

2,拿到音频采样数据.用视图去展现

绘制,就是把一个点、一个点,连接起来

对于点,重点就是 x 和 y 坐标


public override func draw(_ rect: CGRect) {

        let width = Double(frame.width)
        let height = Double(frame.height) / 2.0
        let padding = 0.9

        // ...
        // 画边框、背景和中间线

        let bezierPath = UIBezierPath()
        // 开始一个点
        // 起点
        bezierPath.move(to: CGPoint(x: 0.0, y: (1.0 - Double(table[0]) / absmax) * height))

        for index in 1..<table.count {
            // 算坐标,连线
            let xPoint = Double(index) / Double(table.count) * width

            let yPoint = (1.0 - Double(table[index]) / absmax * padding) * height

            bezierPath.addLine(to: CGPoint(x: xPoint, y: yPoint))
        }
        
       // 结尾一个点
       // 终点
        bezierPath.addLine(to: CGPoint(x: Double(frame.width), y: (1.0 - Double(table[0]) / absmax * padding) * height))

        UIColor.green.setStroke()
        bezierPath.lineWidth = 1
        bezierPath.stroke()
    }

  • x 坐标, 比较好计算,

给了一个矩形,把点均匀分布

// width 固定,每一个点的 x 值,就按照比例来 
 let xPoint = Double(index) / Double(table.count) * width
  • 计算 y 坐标

height 是矩形高度的一半

let height = Double(frame.height) / 2.0

先移动到中线

1.0 * height

再翻转 y 值,因为我们习惯看到的 y 坐标向上增长,iOS 默认的 y 坐标,像下增长

- Double(table[index]) * height

对 y 值先放大,再规整小一些

absmax 是娶到点里面的最大值,

例子: 取到的点都在 0.1 ~ 0.2 之间,不放大,坐标会很难看

- Double(table[index]) * height * absmax

为了给顶部和底部留空,规整下

- Double(table[index]) * height * padding

2.1 ,SwiftUI 调用 UIView 对象

通过使用 UIViewRepresentable 协议的结构体封装


struct TableDataView: UIViewRepresentable {
    // TableView, 这里实际渲染的 View, 是一个 class
    typealias UIViewType = TableView
    var view: TableView


    // 静态界面,这么来一下,就好了
    func makeUIView(context: Context) -> TableView {
        view.backgroundColor = UIColor.black
        return view
    }

    func updateUIView(_ uiView: TableView, context: Context) {
        //
    }

}

另一个声道的波形

一般音频文件,左声道的数据与右声道的,一样

使用另一个声道的数据,看对应的波形

    public
    convenience init?(file: AVAudioFile) {
        let size = Int(file.length)
        self.init(count: size)

        guard let data = file.toFloatChannelData() else { return nil }
        // 循环大法好
        for i in 0 ..< size {
            // 取另一个声道的数据
            // 把 0 改为 1
            self[i] = data[1][i]
        }
    }


波形为什么一般长这样?

波形应该是一条折线,怎么一般都是一团一团的

本文例子中音频时长 8.78 秒,采样率 44100, 双声道,

有 387_072 帧,图中矩形宽度为 343,

一个点,会画 1128 条线,

所以会成一团

github repo