Audio Kit 檢視檔案波形的相關原始碼,easy o

dengjiangszhan發表於2020-12-29

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

相關文章