【原创】Swift分析音频文件

过去半年参与的一个项目中有遇到并解决了一系列关于音频文件的操作方式。相关资料在网上很难找到,所以先整理下以方便今后查漏补缺。

1.如果你需要分析的音频文件是在服务端,那需要先将文件下载在本地,所以我们先实现根据URL地址下载音频文件的功能。

import AVKit

typealias AudioDownloadedCallback = (URL?) -> ()

extension AVAsset {
    static func downloadAudioToLocal(audioURL:URL,downloadCallback:@escaping AudioDownloadedCallback) {
        
        // create your destination file url
        let destinationUrl = FileManager.getDocumentPathWithFileName(sFileName: audioURL.lastPathComponent)
        
        // to check if it exists before downloading it
        if FileManager.default.fileExists(atPath: destinationUrl.path) {
            //The file already exists at path
            downloadCallback(destinationUrl)
        } else {
            //download audio file.
            URLSession.shared.downloadTask(with: audioURL, completionHandler: { (location, response, error) -> Void in
                guard let location = location, error == nil else { return }
                do {
                    //File moved to documents folder
                    try FileManager.default.moveItem(at: location, to: destinationUrl)
                    downloadCallback(destinationUrl)
                } catch let error as NSError {
                    print(error.localizedDescription)
                    
                    downloadCallback(nil)
                }
            }).resume()
        }
    }
}

extension FileManager {
    static func getDocumentPathWithFileName(sFileName:String) -> URL {
        let fileMgr = FileManager.default
        
        let dirPaths = fileMgr.urls(for: .documentDirectory,
                                    in: .userDomainMask).first!
        
        let filePath = dirPaths.appendingPathComponent(sFileName)
        return filePath
    }
}

文件有了后可以开始读取文件。这里有两种方式:
1.通过AVAssetReader进行读取并用NSMutableData记录下来。
2.生成AVAudioFile音频实例,配合AVAudioPCMBuffer进行分段读取。
两者的区别在于:第一种方式是整体读取,当AVAssetReader开始执行读取后,读取的内容段并不受控制,每段读取的长度由CMBlockBufferGetDataLength()计算好了返回。而第二种方式可以自己定义读取的起始点和步长,你甚至可以选择性的重复读取。具体使用哪个看自己的业务场景。

整体读取AVAssetReader + NSMutableData

import UIKit
import AVFoundation
import AVKit

extension AVURLAsset {
    func readAudioBuffer() -> NSMutableData {
        var reader:AVAssetReader?
        do {
            reader = try AVAssetReader(asset: self)
        } catch let error{
            print("Create asset reader failed.\(error.localizedDescription)")
        }
        let sampleData = NSMutableData()
        
        if reader != nil {
            //we only read one tracks from the audio file. If your file is not mone and you need to read them all. just duplicate operation once again.
            let songTrack:AVAssetTrack = self.tracks[0]
            let readerOutputSettings: [String: Int] = [ AVFormatIDKey: Int(kAudioFormatLinearPCM),
                                                        AVLinearPCMBitDepthKey: 16,
                                                        AVLinearPCMIsBigEndianKey: 0,
                                                        AVLinearPCMIsFloatKey: 0,
                                                        AVLinearPCMIsNonInterleaved: 0]
            let output = AVAssetReaderTrackOutput(track: songTrack, outputSettings: readerOutputSettings)
            
            reader?.add(output)
            
            reader?.startReading()
            
            while reader?.status == AVAssetReader.Status.reading {
                if let sampleBufferRef = output.copyNextSampleBuffer() {
                    
                    if let blockBufferRef = CMSampleBufferGetDataBuffer(sampleBufferRef) {
                        
                        let bufferLength = CMBlockBufferGetDataLength(blockBufferRef)
                        
                        let data = NSMutableData(length: bufferLength)
                        
                        CMBlockBufferCopyDataBytes(blockBufferRef, atOffset: 0, dataLength: bufferLength, destination: (data?.mutableBytes)!)
                        
                        let samples = UnsafeMutablePointer<Int16>(OpaquePointer(UnsafeMutableRawPointer(data!.mutableBytes)))
                        sampleData.append(samples, length: bufferLength)
                        CMSampleBufferInvalidate(sampleBufferRef)
                    }
                }
            }
        }
        return sampleData
    }
}

PS:代码中注释处标明这段代码只是读第0条音轨。如果你需要分析的文件是立体声的(也就是存在两条或以上的音轨),需要针对不同的音轨进行调整。
效果:


3211552369372_.pic_hd.jpg

拿到结果后可以根据自己需求进行再加工,比如我自己实现了一个根据UI要求的高宽来对音频数据进行偏移处理。

extension NSMutableData {
    func toInt16Sequence(size:CGSize) -> [Float] {
        var filteredSamplesMA:[Float] = [Float]()
        let sampleCount = self.length / MemoryLayout<Int16>.size
        let binSize = Float(sampleCount) / (Float(size.width) * 0.5)
        var i = 0
        while i < sampleCount {
            let rangeData = self.subdata(with: NSRange(location: i, length: 1))
            let item = rangeData.withUnsafeBytes({ (ptr: UnsafePointer<Int>) -> Int in
                return ptr.pointee
            })
            filteredSamplesMA.append(Float(item))
            i += Int(binSize)
        }
        let result = NSMutableData.trackScale(size: size, source: filteredSamplesMA)
        return result
    }
    
    private static func trackScale(size: CGSize, source: [Float]) -> [Float] {
        if let max = source.max() {
            let k = Float(size.height) / max
            return source.map{ $0 * k }
        }
        return source
    }
}

最终效果:


3221552370149_.pic_hd.jpg

有了这些数据后可以去UI层进行相应的绘制,或者保存下来进行其他方式的分析。

接下来介绍第二种方式 AVAudioFile + AVAudioPCMBuffer
这种方式不同于第一种整体读取,而是可以自己指定每次读取的起始位置和长度,相比较而言更加灵活一些。

import AVFoundation

extension AVAudioFile {
    static func readAmplitude(audioURL:URL) -> NSMutableData {
        let asset = AVURLAsset(url: audioURL)
        var file:AVAudioFile!
        do{
            file = try AVAudioFile(forReading: audioURL)
        } catch let error{
            print("AVAudioFile create failed \(error.localizedDescription)")
        }
        let amplitudes:NSMutableData = NSMutableData()
        //FPS
        let frameCountPeiSecond = 30.0
        
        //Calculate totoal frame of the audio file.
        let frameCountTotal = asset.duration.seconds * frameCountPeiSecond
        
        //merge samples of each frame
        let sampleForEachFrame = Double(file.length.magnitude) / frameCountTotal
        
        var sampleReadIndex:Double = 0.0
        
        while sampleReadIndex < Double(file.length) {
            
            let audioBuffer:AVAudioPCMBuffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: AVAudioFrameCount(sampleForEachFrame))!
            audioBuffer.frameLength = UInt32(sampleForEachFrame)
            
            //adjust frame position each time
            file.framePosition = AVAudioFramePosition(sampleReadIndex)
            
            do{
                //read same size buffer of each time
                try file.read(into: audioBuffer, frameCount: UInt32(sampleForEachFrame))
                amplitudes.append((audioBuffer.floatChannelData?.pointee)!, length: Int(sampleForEachFrame))
            } catch let error{
                print("read to buffer failed \(error.localizedDescription)")
            }
            //adding up
            sampleReadIndex = Double(sampleReadIndex + sampleForEachFrame)
        }
        return amplitudes
    }
}

结果打印


image.png

到此数据就都分析出来了,接下来可以随意选用适合自己场景的第三方框架显示出对应的分析图出来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容