標籤:裁剪 鈴聲 CheapAAC
Android手機上設定鈴聲的操作是比較靈活的,一般讀者聽到一首喜歡的歌曲,馬上就可以對這首歌曲進行裁剪,裁剪到片段後,再通過系統的介面設定為鈴聲(電話鈴聲、鬧鐘鈴聲等)。
前提是,播放這首歌的APP,需要提供裁剪歌曲的功能。
那麼,怎麼樣實現截取音頻檔案的一個片段的功能呢?
小程很自然就想到使用FFmpeg命令來實現,之前介紹“從視頻中提取圖片”的內容就可以提取片段,比如:
ffmpeg -ss 10 -i audio.mp3 -t 5 out.mp3
上面的命令,從第10秒開始,提取5秒的片段。
讀者可以關注“廣州小程”公眾號,並查閱“音視頻->FFmpeg結構&應用”功能表項目的內容。
但是,FFmpeg命令在pc上可以很方便地使用,但在手機APP上,就不能直接使用了。
小程這裡針對Android平台,介紹另外的裁剪音頻檔案的辦法,並且,這裡假定原音頻檔案是m4a封裝格式。
本文介紹如何在Android平台上裁剪m4a音頻檔案,並得到一個音頻片段。
實現這個功能,基本有兩個方案:
- 一是解碼原音頻檔案,然後提取相應片段,再對這個片段進行編碼。
- 二是直接定位到裁剪的起點,提取出片段,再儲存成新的音頻檔案。
相比之下,第一個方案在效能上有更明顯的消耗,但這個方案可以通吃各種音頻格式(只要能解碼,並能最終編碼為固定格式即可)。
第二個方案,需要考慮不同格式(包括原音頻,以及最終音訊格式)的實現,但在效能上佔優,比第一個方案更省時間。
小程這裡介紹第二個方案的實現,並且只考慮m4a檔案的截取與產生。
第二個方案,概括來說,就是m4a格式的解析及m4a檔案的產生過程。
(一)m4a介紹
m4a檔案,實際是mp4檔案,一般只存放音頻流。m4a是蘋果公司起的名字,用來區分帶有視訊框架的一般的mp4檔案。
解析m4a檔案格式就是解析mp4檔案格式,這對於寫檔案也是同樣的道理。
要截取m4a的片段,有必要先解析m4a檔案格式,擷取相關資訊(比如採樣率、聲道數、一幀的樣本數、總幀數、每一幀的長度、每一幀的位移等等),而解析檔案格式,就需要理解mp4的檔案格式。
mp4以atom(或者叫box)構成,所有的資料(包括各種資訊以及裸的音頻資料)都放在atom中。
每個atom由三個欄位組成:
len(整個atom的長度,4Byte)、
type(atom的類型,4Byte)、
data(atom儲存的資料)。
atom可以嵌套。
atom的類型有很多,並不是所有類型都要存在才能組成有效mp4檔案。但有幾個類型的atom是一定要有的:
ftyp(標識檔案格式)、
stts(每一幀的樣本數)、
stsz(每一幀的長度)、
stsc(幀與chunk的關係表)、
mvhd(時間長度等資訊)、
mdat(裸資料)、
moov等。
具體的結構(包括每個atom的含意、每個欄位的大小與含意)可以查看網路上的資源(最好能看到atom的欄位表格)。
比如:
(二)方案實現
第二個方案的實現,可以使用ringdroid這個開源的項目。
ringdroid在git上維護,它最新的版本使用解碼再編碼的方案。
可以找回ringdroid早期的版本,裡面有CheapAAC、CheapMP3等,分別對不同格式的音頻作處理,並且是直接截取。
CheapAAC的ReadFile完成m4a檔案的解析,WriteFile完成新的m4a檔案的寫入。
CheapAAC還實現了增益的計算,可以用來顯示音訊波形圖。
對於截取,有幾個資訊是很重要的:{幀的長度即位元組數}、{幀的位移量},根據這兩個集合就可以實現截取。
幀的長度(以及總幀數)在解析stsz時確定,幀的位移在解析mdat時確定。
讀者可以詳細閱讀CheapAAC的代碼,來理解截取的過程。小程這裡只提一下CheapAAC存在的問題,也是讀者可能遇到的問題。
(1)不相容neroAacEnc編碼的m4a檔案
對於neroAacEnc編碼出來的m4a檔案,CheapAAC在parseMdat時,不能正常解析裸資料,原因是neroAacEnc在裸資料之前多加了8個位元組,這8個位元組會使得計算出來的每一幀的位移都不對,導致後繼WriteFile時寫出來的每一幀的資料都不對。
可以考慮跳過8個位元組來解決這個問題(在判斷為nero編碼出來的m4a時):
if (mMdatOffset > 0 && mMdatLength > 0) { final int neroAACFrom = 570; int neroSkip = 0; if (mMdatOffset - neroAACFrom > 0) { FileInputStream cs = new FileInputStream(mInputFile); cs.skip(mMdatOffset - neroAACFrom); final int flagSize = 14; byte[] buffer = new byte[flagSize]; cs.read(buffer, 0, flagSize); if (buffer[0] == ‘N‘ && buffer[1] == ‘e‘ && buffer[2] == ‘r‘ && buffer[3] == ‘o‘ && buffer[5] == ‘A‘ && buffer[6] == ‘A‘ && buffer[7] == ‘C‘ && buffer[9] == ‘c‘ && buffer[10] == ‘o‘ && buffer[11] == ‘d‘ && buffer[12] == ‘e‘ && buffer[13] == ‘c‘) { neroSkip = 8; } cs.close(); } stream = new FileInputStream(mInputFile); mMdatOffset += neroSkip; // slip 8 Bytes if need stream.skip(mMdatOffset); mOffset = mMdatOffset; parseMdat(stream, mMdatLength); } else { throw new java.io.IOException("Didn‘t find mdat"); }
(2)截取片段的時間長度不對
截取出來的片段的時間長度沒有重新設定,仍使用原檔案的時間長度。
可以在WriteFile裡面重新設定片段的時間長度,但要注意,如果最終是使用mediaplayer來播放,則不能加以下代碼,因為mediaplayer解碼的處理跟FFmpeg等不一致。如果最終是交給FFmpeg等來解碼,則需要重新設定片段的時間長度。
// 在寫完stco之後,增加: long time = System.currentTimeMillis() / 1000; time += (66 * 365 + 16) * 24 * 60 * 60; // number of seconds between 1904 and 1970 byte[] createTime = new byte[4]; createTime[0] = (byte)((time >> 24) & 0xFF); createTime[1] = (byte)((time >> 16) & 0xFF); createTime[2] = (byte)((time >> 8) & 0xFF); createTime[3] = (byte)(time & 0xFF); long numSamples = 1024 * numFrames; long durationMS = (numSamples * 1000) / mSampleRate; if ((numSamples * 1000) % mSampleRate > 0) { // round the duration up. durationMS++; } byte[] numSaplesBytes = new byte[] { (byte)((numSamples >> 26) & 0XFF), (byte)((numSamples >> 16) & 0XFF), (byte)((numSamples >> 8) & 0XFF), (byte)(numSamples & 0XFF) }; byte[] durationMSBytes = new byte[] { (byte)((durationMS >> 26) & 0XFF), (byte)((durationMS >> 16) & 0XFF), (byte)((durationMS >> 8) & 0XFF), (byte)(durationMS & 0XFF) }; int type = kMDHD; Atom atom = mAtomMap.get(type); if (atom == null) { atom = new Atom(); mAtomMap.put(type, atom); } atom.data = new byte[] { 0, // version, 0 or 1 0, 0, 0, // flag createTime[0], createTime[1], createTime[2], createTime[3], // creation time. createTime[0], createTime[1], createTime[2], createTime[3], // modification time. 0, 0, 0x03, (byte)0xE8, // timescale = 1000 => duration expressed in ms. 1000為單位 durationMSBytes[0], durationMSBytes[1], durationMSBytes[2], durationMSBytes[3], // duration in ms. 0, 0, // languages 0, 0 // pre-defined; }; atom.len = atom.data.length + 8; type = kMVHD; atom = mAtomMap.get(type); if (atom == null) { atom = new Atom(); mAtomMap.put(type, atom); } atom.data = new byte[] { 0, // version, 0 or 1 0, 0, 0, // flag createTime[0], createTime[1], createTime[2], createTime[3], // creation time. createTime[0], createTime[1], createTime[2], createTime[3], // modification time. 0, 0, 0x03, (byte)0xE8, // timescale = 1000 => duration expressed in ms. 1000為單位 durationMSBytes[0], durationMSBytes[1], durationMSBytes[2], durationMSBytes[3], // duration in ms. 0, 1, 0, 0, // rate = 1.0 1, 0, // volume = 1.0 0, 0, // reserved 0, 0, 0, 0, // reserved 0, 0, 0, 0, // reserved 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // unity matrix for video, 36bytes 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x40, 0, 0, 0, 0, 0, 0, 0, // pre-defined 0, 0, 0, 0, // pre-defined 0, 0, 0, 0, // pre-defined 0, 0, 0, 0, // pre-defined 0, 0, 0, 0, // pre-defined 0, 0, 0, 0, // pre-defined 0, 0, 0, 2 // next track ID, 4bytes }; atom.len = atom.data.length + 8;
(三)其它概念
在CheapAAC中涉及到一些音頻概念,小程簡單解釋一下。讀者也可以關注“廣州小程”公眾號,查閱“音視頻”菜單下的文章。
track,即軌道(音頻或視頻),也叫流;
sample,理解為幀(跟樣本的概念不同),對於aac來說一幀包括的樣本數是固定的,都為1024個;
chunk,即塊,是幀的集合。
neroAcc命令使用樣本:
ffmpeg -i "1.mp3" -f wav - | neroAacEnc -br 32000 -ignorelength -if - -of "1.m4a"
-br 碼率
-lc/-he/-hev2 編碼方式,預設是he
-if 輸入檔案
-of 輸出檔案
-ignorelength 在以其它輸出(如ffmpeg)作為輸入時使用
至此,在Android平台裁剪m4a的實現就介紹完畢了。
總結一下,本文介紹了在Android平台上,使用CheapAAC來裁剪m4a得到片段檔案的實現辦法,同時也介紹了m4a結構的概念,以及可能遇到的問題。
Android平台上裁剪m4a