關鍵詞
AVAsset MP3 PCM 格式 音頻 采樣 AVAssetReader AVAssetWriter 輸出 轉換
本文所有示例代碼或Demo可以在此獲取:https://github.com/WillieWangWei/SampleCode_MP3ToPCM
如果本文對你有所幫助,請給個Star??
概述
本文僅講解所用技術的基本概念以及將MP3
轉成PCM
格式的實際應用,其他格式的相互轉換可以修改示例代碼實現。關于AVAsset
的其他使用場景可以參考這里,音頻相關的內容可以參考這里。
首先了解一些概念:
AVAsset
它包含于AVFoundation
,是一個不可變的抽象類,用來代表一個音視頻媒體。一個AVAsset
實例可能包含著一個或多個用來播放或處理的軌道,包含但不限于音頻、視頻、文本以及相關說明。但它并不是媒體資源本身,可以將它理解為時基媒體的容器。
AVAssetReader
我們可以使用一個AVAssetReader
實例從一個AVAsset
的實例中獲取媒體數據。
AVAssetReaderAudioMixOutput
它是AVAssetReaderOutput
的一個子類,我們可以將一個AVAssetReaderAudioMixOutput
的實例綁定到一個AVAssetReader
實例上,從而得到這個AVAssetReader
實例的asset
的音頻采樣數據。
AVAssetWriter
我們可以使用一個AVAssetWriter
實例將媒體數據寫入一個新的文件,并為其指定類型。
AVAssetWriterInput
我們可以將一個AVAssetWriterInput
的實例綁定到一個AVAssetWriter
實例上,從而將媒體采樣包裝成CMSampleBuffer
對象或者元數據集合,然后添加到輸出文件的單一通道上。
PCM
模擬音頻信號經模數轉換(A/D變換)直接形成的二進制序列,PCM
就是錄制聲音時保存的最原始的聲音數據格式。
WAV
格式的音頻其實就是給PCM
數據流加上一段header數據。而WAV
格式有時候之所以被稱為無損格式,就是因為它保存的是原始PCM
數據(也跟采樣率
和比特率
有關)。常見音頻格式比如MP3
,AAC
等等,為了節約占用空間都進行有損壓縮。
代碼
這里列舉兩種應用場景:
- 將
PCM
數據寫入磁盤保存成文件。 - 將
PCM
數據轉成NSDate
保存在內存中。
這兩種場景都需要先讀取MP3
的數據,然后創建AVAssetReader
和AVAssetReaderAudioMixOutput
實例,所以前半部分的處理邏輯的一樣的。
通用邏輯
0.導入頭文件
import AVFoundation
1.創建AVAsset實例
func readMp3File() -> AVAsset? {
guard let filePath = Bundle.main.path(forResource: "trust you", ofType: "mp3") else { return nil }
let fileURL = URL(fileURLWithPath: filePath)
let asset = AVAsset(url: fileURL)
return asset
}
2.創建AVAssetReader實例
func initAssetReader(asset: AVAsset) -> AVAssetReader? {
let assetReader: AVAssetReader
do {
assetReader = try AVAssetReader(asset: asset)
} catch {
print(error)
return nil
}
return assetReader
}
3.配置轉碼參數
var channelLayout = AudioChannelLayout()
memset(&channelLayout, 0, MemoryLayout<AudioChannelLayout>.size)
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo
let outputSettings = [
AVFormatIDKey : kAudioFormatLinearPCM, // 音頻格式
AVSampleRateKey : 44100.0, // 采樣率
AVNumberOfChannelsKey : 2, // 通道數 1 || 2
AVChannelLayoutKey : Data.init(bytes: &channelLayout, count: MemoryLayout<AudioChannelLayout>.size), // 聲音效果(立體聲)
AVLinearPCMBitDepthKey : 16, // 音頻的每個樣點的位數
AVLinearPCMIsNonInterleaved : false, // 音頻采樣是否非交錯
AVLinearPCMIsFloatKey : false, // 采樣信號是否浮點數
AVLinearPCMIsBigEndianKey : false // 音頻采用高位優先的記錄格式
] as [String : Any]
4.創建AVAssetReaderAudioMixOutput實例并綁定到assetReader上
let readerAudioMixOutput = AVAssetReaderAudioMixOutput(audioTracks: asset.tracks, audioSettings: nil)
if !assetReader.canAdd(readerAudioMixOutput) {
print("can't add readerAudioMixOutput")
return
}
assetReader.add(readerAudioMixOutput)
接來下兩種場景的處理邏輯就不一樣了,請注意區分。
保存成文件
5.創建一個AVAssetWriter實例
func initAssetWriter() -> AVAssetWriter? {
let assetWriter: AVAssetWriter
guard let outPutPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first else { return nil }
// 這里的擴展名'.wav'只是標記了文件的打開方式,實際的編碼封裝格式由assetWriter的fileType決定
let fullPath = outPutPath + "outPut.wav"
let outPutURL = URL(fileURLWithPath: fullPath)
do {
assetWriter = try AVAssetWriter(outputURL: outPutURL, fileType: AVFileTypeWAVE)
} catch {
print(error)
return nil
}
return assetWriter
}
6.創建AVAssetWriterInput實例并綁定到assetWriter上
if !assetWriter.canApply(outputSettings: outputSettings, forMediaType: AVMediaTypeAudio) {
print("can't apply outputSettings")
return
}
let writerInput = AVAssetWriterInput(mediaType: AVMediaTypeAudio, outputSettings: outputSettings)
// 是否讓媒體數據保持實時。在此不需要開啟
writerInput.expectsMediaDataInRealTime = false
if !assetWriter.canAdd(writerInput) {
print("can't add writerInput")
return
}
assetWriter.add(writerInput)
7.啟動轉碼
assetReader.startReading()
assetWriter.startWriting()
// 開啟session
guard let track = asset.tracks.first else { return }
let startTime = CMTime(seconds: 0, preferredTimescale: track.naturalTimeScale)
assetWriter.startSession(atSourceTime: startTime)
let mediaInputQueue = DispatchQueue(label: "mediaInputQueue")
writerInput.requestMediaDataWhenReady(on: mediaInputQueue, using: {
while writerInput.isReadyForMoreMediaData {
if let nextBuffer = readerAudioMixOutput.copyNextSampleBuffer() {
writerInput.append(nextBuffer)
} else {
writerInput.markAsFinished()
assetReader.cancelReading()
assetWriter.finishWriting(completionHandler: {
print("write complete")
})
break
}
}
})
轉成NSDate
5.啟動轉碼
assetReader.startReading()
var PCMData = Data()
while let nextBuffer = readerAudioMixOutput.copyNextSampleBuffer() {
var audioBufferList = AudioBufferList()
var blockBuffer: CMBlockBuffer?
// CMSampleBuffer 轉 Data
CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(nextBuffer,
nil,
&audioBufferList,
MemoryLayout<AudioBufferList>.size,
nil,
nil,
0,
&blockBuffer)
let audioBuffer = audioBufferList.mBuffers
guard let frame = audioBuffer.mData else { continue }
PCMData.append(frame.assumingMemoryBound(to: UInt8.self), count: Int(audioBuffer.mDataByteSize))
blockBuffer = nil
}
print("write complete")
注意問題
性能問題
轉碼是個很占用CPU資源的計算過程。
具體完成一個轉碼過程的時間取決于文件時長、轉碼配置、設備性能等多個條件。這是一個典型的耗時操作,務必要做好線程優化。另外,可以根據業務邏輯間歇調用readerAudioMixOutput.copyNextSampleBuffer()
及后續操作,降低CPU開銷峰值。
內存管理
以本文將MP3
轉成PCM
的代碼為例,一個時長4分半左右的MP3
對應的PCM
數據在55MB左右,這些數據占用了大量的內存或磁盤空間,注意釋放。你可以通過改變轉碼配置參數outputSettings
來調整輸出數據的大小。
在轉碼過程中,CMSampleBufferRef
、CMBlockBufferRef
的對象在使用后需要調用CFRelease
銷毀,以防內存泄漏。
其他格式的轉換
邏輯是一樣的,你可以修改讀取和輸出的參數實現。注意處理的格式必須是AVFoundation
所包含的,可以參考AudioFormatID
這個類以及AVMediaFormat.h
的File format UTIs
。更多音頻處理請參考Apple Developer Library :AVFoundation或第三方框架。
在macOS上轉換格式
macOS上可以使用一個強大的音視頻庫FFmpeg,它可以幫助你快速轉碼出需要的音頻格式作為調試素材。
macOS上編譯FFmpeg
請看這里。
將MP3
轉換成PCM
的命令:
ffmpeg mp3 => pcm ffmpeg -i xxx.mp3 -f s16le -ar 44100 -ac 2 xxx.pcm
總結
本文提供了將MP3
轉成PCM
的一種實現,中間涉及了一些音頻
、AVFoundation
和CoreMedia
的知識,這里就不展開了,有問題的同學可以在文章下留言討論。
本文所有示例代碼或Demo可以在此獲取:https://github.com/WillieWangWei/SampleCode_MP3ToPCM
如果本文對你有所幫助,請給個Star??
參考資料:
Apple Developer Library :AVFoundation
http://msching.github.io/blog/2014/07/07/audio-in-ios/