Android手機上設置鈴聲的操做比較靈活,你聽到一首喜歡的歌曲,立刻就能夠對這首歌曲進行裁剪,裁剪到片斷後,再經過系統的接口設置爲鈴聲(電話鈴聲、鬧鐘鈴聲等)。前提是,播放這首歌的APP,須要提供裁剪歌曲的功能。java
那麼,怎麼樣實現截取音頻文件的功能呢?git
基於以前的介紹,你可能很天然就想到使用FFmpeg命令來實現,好比:express
ffmpeg -ss 10 -i audio.mp3 -t 5 out.mp3網絡
上面的命令,從第10秒開始,提取5秒的片斷,因而就成功截取了一個片斷。可是,FFmpeg命令在pc上能夠很方便地使用,但在手機APP上,就不能直接使用了(其實,也是能夠的,能夠在APP中直接調用ffmpeg命令,但這個不是這裏的重點)。ide
這裏針對Android平臺,介紹裁剪音頻文件的辦法,而且,這裏假定原音頻文件是m4a封裝格式。性能
本文介紹如何在Android平臺上裁剪m4a音頻文件,並獲得一個音頻片斷。編碼
實現這個功能,基本有兩個方案:atom
相比之下,第一個方案在性能上有更明顯的消耗,但這個方案能夠通吃各類音頻格式(只要能解碼,並能最終編碼爲固定格式便可)。code
第二個方案,須要考慮不一樣格式(包括原音頻,以及最終音頻的格式)的實現,但在性能上佔優,比第一個方案更省時間。視頻
小程這裏介紹第二個方案的實現,而且只考慮m4a文件的截取與生成。第二個方案,歸納來講,就是m4a格式的解析及m4a文件的生成過程。
m4a文件,實際是mp4文件(mp4a),通常只存放音頻流。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存在的問題,也是你可能遇到的問題。
對於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"); }
截取出來的片斷的時長沒有從新設置,仍使用原文件的時長。
能夠在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結構的概念,以及可能遇到的問題。