1. 程式人生 > >多媒體開發(11):Android平臺上裁剪m4a

多媒體開發(11):Android平臺上裁剪m4a

Android手機上設定鈴聲的操作比較靈活,你聽到一首喜歡的歌曲,馬上就可以對這首歌曲進行裁剪,裁剪到片段後,再通過系統的介面設定為鈴聲(電話鈴聲、鬧鐘鈴聲等)。前提是,播放這首歌的APP,需要提供裁剪歌曲的功能。

那麼,怎麼樣實現擷取音訊檔案的功能呢?

基於之前的介紹,你可能很自然就想到使用FFmpeg命令來實現,比如:

ffmpeg -ss 10 -i audio.mp3 -t 5 out.mp3

上面的命令,從第10秒開始,提取5秒的片段,於是就成功截取了一個片段。但是,FFmpeg命令在pc上可以很方便地使用,但在手機APP上,就不能直接使用了(其實,也是可以的,可以在APP中直接呼叫ffmpeg命令,但這個不是這裡的重點)。

這裡針對Android平臺,介紹裁剪音訊檔案的辦法,並且,這裡假定原音訊檔案是m4a封裝格式。

本文介紹如何在Android平臺上裁剪m4a音訊檔案,並得到一個音訊片段。

實現這個功能,基本有兩個方案:

  • 一是解碼原音訊檔案,然後提取相應片段,再對這個片段進行編碼。
  • 二是直接定位到裁剪的起點,提取出片段,再儲存成新的音訊檔案。

相比之下,第一個方案在效能上有更明顯的消耗,但這個方案可以通吃各種音訊格式(只要能解碼,並能最終編碼為固定格式即可)。

第二個方案,需要考慮不同格式(包括原音訊,以及最終音訊的格式)的實現,但在效能上佔優,比第一個方案更省時間。

小程這裡介紹第二個方案的實現,並且只考慮m4a檔案的擷取與生成。第二個方案,概括來說,就是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存在的問題,也是你可能遇到的問題。

(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結構的概念,以及可能遇到的問題。


相關推薦

多媒體開發11Android平臺裁剪m4a

Android手機上設定鈴聲的操作比較靈活,你聽到一首喜歡的歌曲,馬上就可以對這首歌曲進行裁剪,裁剪到片段後,再通過系統的介面設定為鈴聲(電話鈴聲、鬧鐘鈴聲等)。前提是,播放這首歌的APP,需要提供裁剪歌曲的功能。 那麼,怎麼樣實現擷取音訊檔案的功能呢? 基於之前的介紹,你可能很自然就想到使用FFmpeg命令

多媒體開發7編譯Android與iOS平臺的FFmpeg

編譯FFmpeg,一個古老的話題,但我還是介紹一遍,就當記錄。之前介紹怎麼給視訊新增水印時,就已經提到FFmpeg的編譯,並且在編譯時指定了濾鏡的功能。 但是,在手機盛行的時代,你可能更需要的是能在iOS或Android平臺上執行的FFmpeg,而對於命令列的ffmpeg,你可以在個人電腦上面使用(因為它簡

Android多媒體開發2————使用Android NKD編譯原版FFmpeg

/********************************************************************************************  * author:[email protected]大鐘        

android開發0android studio的下載安裝與簡單使用 | sdk的安裝與編譯

ger 準備 開發環境 view 選擇 集成開發環境 alt 尋找 control android studio,簡稱AS,是集成開發環境,所謂集成,就是集編輯、編譯、調試、打包等於一體。簡單來說,通過AS,就可以開發出在android系統上運行的APP。 我使用的是mac

多媒體開發3直播

特點 nss ams 測試的 方式 input cat nginx 成功 之前介紹了如何錄制音視頻,以及相關的多媒體的概念。對於已經錄制的多媒體進行“就地”播放(參考前文),就是回放,除了“回放”這個流程,還有一個流程也會經常遇到,那就是“直播”。 本文介紹直播的實現。 “

多媒體開發6濾鏡實現各種圖片效果 | Video-Filters | 變色

命令行 let img 很多 保持 yuv 黑白 多媒體 ati 之前講過使用FFmpeg的drawtext濾鏡(把圖片或文字加到視頻上),而實際上,FFmpeg的濾鏡很強大,遠不止加字幕或加圖片的功能。濾鏡是很有趣的,可以把圖片變模糊、變色、縮放旋轉,等等。 本文介紹FF

多媒體開發8調試FFmpeg

run 包括 啟用 return tar.bz2 %d 參考 efi turn 編譯FFmpeg得到二進制文件,之後就是對二進制庫的調用,這時FFmpeg就像一個黑盒子。作為程序員,難道不想研究一下FFmpeg的具體實現?比如是怎麽拿到歌曲信息的、怎麽解碼的、怎麽推流的,等

多媒體開發9聲音采集的概念 | 振幅 | 頻率 | 共振 | 電平化

坐標 波形 上下 樣本 形狀 多少 為什麽不使用 dsd 運動 之前介紹通過ffmpeg程序來錄制聲音或圖像,這個辦法是一個操作的過程,很少涉及到概念上的東西。 而本文,要介紹的是聲音采集的一些流程與概念。 聲音的采集流程與概念,是枯燥的,你如果不想了解的話,到這裏就可以退

多媒體開發10從視訊中提取圖片

小白:提取視訊中的圖片嗎?那很簡單,播放視訊再截圖就行啦。 播放視訊再截圖的做法,當然也可以。但是,手動地截圖會太累而且無法保證準確度,特別是需要反覆提取圖片時,或者需要提取“105秒那一瞬間的美女圖片”時,或者我需要每秒出一張圖片時,那有別的辦法嗎? 本文介紹,如何使用FFmpeg實現從視訊中提取圖片的

多媒體開發12解碼aac到wav檔案

簡單來說,aac是一種音訊編碼格式,需要解碼後才能用於音訊輸出。aac編碼格式,已經是一種很常見的音訊編碼格式,以至於很多系統都支援aac的編解碼,比如iOS上的AudioConverterRef介面、Android上的MediaCodec介面等。 但是,不要以為用了系統的介面就是用了硬體解碼,因為,這個系統

多媒體開發14媒體格式的概念

之前講了一些音視訊的錄製操作,還有聲音採集的概念。採集只是多媒體操作流程中的一個環節,更多的環節可以看看這個圖: 聲音或視訊採集後,就是編碼、寫檔案或推流。不管是編碼還是寫“檔案”,你都能找到相應的程式(比如FFmpeg)來完成,一般加上自己的業務程式碼就能實現自己的功能需求。那就沒有東西好說的了? 沒東

多媒體開發15H264的常見概念

H264,是你常見的技術術語了吧。 那h264是什麼東西呢? H.264是視訊編碼標準,又是標準,得標準得天下啊。 在術語的拼寫上,小程以能理解為準。 本文介紹H264的常見概念。 預警,本文相對枯燥,你可隨時放棄閱讀。 (1)H264從哪裡來? 之前介紹媒體格式的概念時,有提到過國際標準化組織(ISO)

多媒體開發16幀率與位元速率的概念

為什麼說音視訊開發入門較難,因為涉及到很多概念,之前還專門講“媒體格式”、“h264概念”的東西。現在又來,“幀率”跟“位元速率”,這也是兩個常見的概念。你應該經常聽到“重新整理的幀率是多少”或“位元速率比較高所以網速要比較快”的表達吧。 本文介紹音視訊的幀率與位元速率的概念。 (1)幀率 幀率,表示的是頻率

多媒體開發18FFmpeg的常見結構體

除了之前講的avpacket跟avframe,FFmpeg還有其它一些結構經常在流程中出現。FFmpeg還有哪些常見的結構呢?先來看一下這個截圖: 這張圖中的主角,是AVFormatContext。AVFormatContext是FFmpeg的基本結構之一,對應於封裝格式(或容器格式)。 圍繞FFmpeg

多媒體開發2錄製視訊

上一節介紹了用ffplay來播放檔案(或url),這裡有一個概念,如果是播放已經存在的檔案,那叫“回放”,也就是Playback(從流媒體的角度也叫點播),如果播放的是正在錄製的資料(邊錄邊播),那叫直播。 不管是回放還是直播,都需要有媒體資料,那這個媒體資料是怎麼來的呢?從已有的檔案編輯而來是一個辦法,但

多媒體開發6用濾鏡實現各種圖片效果

之前講過使用FFmpeg的drawtext濾鏡(把圖片或文字加到視訊上),而實際上,FFmpeg的濾鏡很強大,遠不止加字幕或加圖片的功能。濾鏡很有趣,可以把圖片變模糊、變色、縮放旋轉,等等。 **本文介紹FFmpeg濾鏡的使用。目的是讓你感受一下FFmepg的濾鏡效果,這樣在實際需要某種效果時,可以考慮使用

多媒體開發8除錯FFmpeg

編譯FFmpeg得到二進位制檔案,之後就是對二進位制庫的呼叫,這時FFmpeg就像一個黑盒子。作為程式設計師,難道不想研究一下FFmpeg的具體實現?比如是怎麼拿到歌曲資訊的、怎麼解碼的、怎麼推流的,等等。 看原始碼是理解程式碼實現的一個辦法,而單步除錯能從另一個維度去幫到你。**本文介紹如何單步除錯FFm

多媒體開發9我是聲音

之前介紹通過ffmpeg程式來錄製聲音或影象,這個辦法是一個操作的過程,很少涉及到概念上的東西。而**本文,要介紹的是聲音採集的一些流程與概念。** 聲音的採集流程與概念,是枯燥的,但是,我也會盡量說一些有趣的現象來緩解這種枯燥。 聽得到的,或聽不到的聲音,抽象來說,都是模擬訊號,也可以形象一點,叫能量波

多媒體開發10提取圖片以及點陣圖儲存

> 小白:提取視訊中的圖片嗎?那很簡單,播放視訊再截圖就行啦。 播放視訊再截圖的做法,當然可以。但是,手動截圖會太累而且無法保證準確度,特別是需要反覆提取圖片時,或者需要提取“105秒那一瞬間的美女圖片”時,或者我需要每秒出一張圖片時,那有別的辦法嗎? **本文介紹,如何使用FFmpeg實現從視訊中

安卓開發學習筆記Android Stuidio無法引用Intent來創建對象,出現cannot resolve xxx

編譯器 port stact 消失 click first 紅色 xxx font 筆者在進行安卓開發時,發現自己的代碼語法完全沒有問題。尤其是創建intent對象的時候,語法完全是正確的,但是Android Stuidio卻顯示報錯,Intent類顯示為紅色,如圖所示: