音訊(六)-安卓ndk將pcm轉換為mp3

saka發表於2018-07-18

安卓不支援mp3格式的錄製,但是可以解碼mp3格式檔案,lame庫是一個通用的編碼mp3庫,用c語言實現。這篇文章自制了lame庫的cmake指令碼,實現了在安卓上將PCM資料轉換為MP3。

關於mp3

Mp3曾經以它優秀的壓縮率和較低的失真一橫行音樂行業,在那個儲存介質昂貴的時代大放光彩,隨著技術的發展,儲存已經不是瓶頸了,現在的音樂愛好者也開始追求音質,出現了高保真音樂,復古黑膠唱片等。但是作為一個音訊開發者,基本的mp3知識還是需要掌握的。

MP3是一種有失真壓縮格式,對它進行解碼不能還原PCM。一般CD品質的音訊檔案是1411.2kbps(16bitpersample、*44100samplerate、*2channels),這個需要較高的頻寬才能保證傳輸的穩定性,但是經過MP3編碼後位元率基本結餘128kbps~320kbps,壓縮率為12:1-10:1,這樣回放的質量低了,但是檔案大小得到了控制。 本篇文章討論的並非是音樂播放器,而是一種編碼格式,並且以lame編碼器來講解文章格式,事實上lame編碼器被認為是最好的MP3編碼器。

MP3檔案格式

MP3一般包含3個主要部分ID3v2、frame、ID3v1。其形式如下:

說明
ID3v2 包含了作者,作曲,專輯資訊等,長度不固定,擴充套件了ID3v1的資訊量
Frame 一些列的幀,個數由檔案的大小和幀長度決定
每個frame包含幀頭和實體資料兩部分,幀頭記錄了mp3的位寬,取樣率,版本資訊等,每個幀之間相互獨立,但是每個幀的長度不固定,由bitrate決定
ID3v1 包含了作者,作曲,專輯等資訊,長度固定是123Byte

下面分別說一下各個格式的資訊

ID3v2結構圖

音訊(六)-安卓ndk將pcm轉換為mp3

ID3V2共有4個版本,但實際上用的最多的是ID3V2.3

資料塊 資料描述 位元組數(Byte) 內容
標籤頭 ID3V2標識 3 固定字元"ID3",表示是ID3v2標籤
ID3v2的子版本號 2 0x0300表示是主版本號為3,副版本號為0,也就是ID3v2.3
ID3v2標誌位 1 abc00000,a-非同步編碼,b-擴充套件標籤頭,c-測試指示位,當這三位置是1時表示有效,一般情況都是0
ID3v2大小 4 每個位元組只有後七位有效,size=byte0:70x200000+byte1:70x4000+byte2:7*0x80+byte3:7
擴充套件標籤頭 擴充套件標籤頭大小 4 size=byte00x200000+byte10x4000+byte2*0x80+byte3
擴充套件標誌位 2 xx
補空大小 4 可以在所有的標籤幀後邊新增補空的資料,也可以預留空間存放額外的幀,是的整個標籤大小比標籤頭的大小更大,一般不用
標籤幀 幀標識 4 固定四個字元,每個標籤幀都有一個10個自己的固定的頭和至少一個位元組的不固定長度的內容組成,也就是下邊的幀大小和幀標誌必須有,而幀資料的內容不得小於1.
幀大小 4 出去幀頭的所有長度,size=byte00x200000+byte10x4000+byte2*0x80+byte3
標誌 2 標誌位,只定義6bit,abc00000 ijk00000一般為0
幀資料 size 存放的資料
補空 補空大小

介紹一下常用的幀標識:

標識內容 描述
TIT2 標題
TPE1 作者
TALB 專輯
TRCK 音軌N/M格式
TYER 年代
TCON 型別
COMM 備註

有效資料幀

有效資料幀的編碼在lame共有三種,CBR、VBR和ABR。

  • CBR:幀長度固定,資料平均分配在各個幀,這種方式有利於計算播放時長,但是檔案稍微大
  • VBR:幀長度不固定,要獲取真個播放時長必須知道幀的總數,檔案較小
  • ABR:幀長度不固定,介於CBR和VBR之間

有效資料幀頭為四個位元組: 此處是1-32

偏移地址 位數(bits) 內容
1 12 幀同步標識,一般標識資料幀的開始,全部為1
13 1 MPEG音訊版本號
14 2 Layer版本
16 1 保護位
17 4 位元率
21 2 取樣率
23 1 補空位大小
24 1 不知道啥
25 2 模式
27 2 模式擴充位
29 1 版權位
30 1 原始位
31 2 強調位

這個地方的內容較多,此處我不一一列舉,附上一個寫的比較詳細的部落格:

MP3檔案格式全解

LAME的使用

Lame是一個專門用編碼MP3的開源庫,它可以提供多種不同位元率的支援,並且提供了各個平臺下的編譯原始碼包,可以直接在SourceForge下載。

安卓平臺編譯

官方並沒有提供專門的編譯檔案,不過我們可以自己採用多種方式編譯:ndk-build和cmake,兩種方式都非常簡單。首先要下載原始碼,然後解壓到一個資料夾內。

ndk-build方式構建lame

我們需要編寫兩個檔案,Android.mk和Application.mk。一個參考網址可以少走一些坑(http://developer.samsung.com/technical-doc/view.do;jsessionid=32A9C99833A33F376D7DB8C787414B62?v=T000000090)[http://developer.samsung.com/technical-doc/view.do;jsessionid=32A9C99833A33F376D7DB8C787414B62?v=T000000090]

主要有四點:

  1. 將libmp3lame資料夾下的所有內容拷貝到一個指定的地方,然後再將lame.h檔案考進來

  2. 找到util.h檔案,將其中的extern ieee754_float32_t fast_log2(ieee754_float32_t x);替換為 extern float fast_log2(float x);

  3. 找到set_get.h檔案。替換 #include <lame.h>#include “lame.h”

  4. 假如出現bcopy unrefrence的錯誤,在Application.mk檔案中新增一個flag,最後新增一行,內容為APP_CFLAGS += -DSTDC_HEADERS

這樣就可以直接編譯生成so檔案了。 假如配置好了ndk的全域性變數,只需要執行ndk-build NDK_PROJECT_PATH=. NDK_APPLICATION_MK=Application.mk就生成了對應的so檔案了

.
├── arm64-v8a
│   └── libmp3lame.so
├── armeabi
│   └── libmp3lame.so
├── armeabi-v7a
│   └── libmp3lame.so
├── mips
│   └── libmp3lame.so
├── mips64
│   └── libmp3lame.so
├── x86
│   └── libmp3lame.so
└── x86_64
    └── libmp3lame.so
複製程式碼

下邊是兩個檔案

  1. Application.mk
APP_PLATFORM := android-18
APP_ABI := all
APP_BUILD_SCRIPT := Android.mk
APP_CFLAGS += -DSTDC_HEADERS
複製程式碼
  1. Android.mk
LOCAL_PATH := $(call my-dir)
 
		include $(CLEAR_VARS)
		 
		LOCAL_MODULE    	:= libmp3lame
		LOCAL_SRC_FILES 	:= \
		./libmp3lame/bitstream.c \
		./libmp3lame/encoder.c \
		./libmp3lame/fft.c \
		./libmp3lame/gain_analysis.c \
		./libmp3lame/id3tag.c \
		./libmp3lame/lame.c \
		./libmp3lame/mpglib_interface.c \
		./libmp3lame/newmdct.c \
		./libmp3lame/presets.c \
		./libmp3lame/psymodel.c \
		./libmp3lame/quantize.c \
		./libmp3lame/quantize_pvt.c \
		./libmp3lame/reservoir.c \
		./libmp3lame/set_get.c \
		./libmp3lame/tables.c \
		./libmp3lame/takehiro.c \
		./libmp3lame/util.c \
		./libmp3lame/vbrquantize.c \
		./libmp3lame/VbrTag.c \
		./libmp3lame/version.c
		
		LOCAL_LDLIBS := -llog
		
		include $(BUILD_SHARED_LIBRARY)
複製程式碼

cmake方式構建lame

cmake構建更加簡單,只需要將剛才的libmp3lame資料夾和lame.h檔案新增到src/main/cpp資料夾下,此處我和原始檔夾保持一致,起名為libmp3lame,然後編寫一個CMakeLists.txt檔案如下:

add_definitions("-DSTDC_HEADERS")
add_library(mp3lame bitstream.c
                     encoder.c
                     fft.c
                     gain_analysis.c
                     id3tag.c
                     lame.c
                     mpglib_interface.c
                     newmdct.c
                     presets.c
                     psymodel.c
                     quantize.c
                     quantize_pvt.c
                     reservoir.c
                     set_get.c
                     tables.c
                     takehiro.c
                     util.c
                     vbrquantize.c
                     VbrTag.c
                     version.c)
複製程式碼

然後在主資料夾下的CMakeList.txt中新增生成該庫的程式碼:

set(LIB_MP3 Mp3Codec)

include_directories(
             src/main/cpp/include #將lame.h檔案複製到這個資料夾下,更加清晰一些,可以作為一個介面檔案
            )
add_subdirectory(src/main/cpp/libmp3lame)
複製程式碼

假如要使用這個庫的話只需要假如target_link命令來連線即可。

lame轉碼pcm格式為mp3

我做了一個非常簡單的例項程式,首先是通過AudioRecorder錄製PCM資料,然後封裝為wav格式,這個格式在安卓手機上是可以直接播放的。然後在將wav檔案通過jni層的lame呼叫轉碼為MP3。

首先了解一下lame的api文件:

  1. 獲取版本資訊(可選的) const char * get_lame_version(void);

  2. 錯誤資訊 預設情況下lame會輸出錯誤資訊到標準錯誤流中,但是我們需要獲取錯誤資訊的話,可以呼叫如下方法來設定:

lame_set_errorf(gfp,error_handler_function);
lame_set_debugf(gfp,error_handler_function);
lame_set_msgf(gfp,error_handler_function);
複製程式碼

通過這種方式,就可以將除錯或者錯誤資訊傳送到我們自己的handler中。這個handler函式一般如下:

 void my_debugf(const char *format, va_list ap)
 {
     (void) vfprintf(stdout, format, ap);
 }
複製程式碼
  1. 初始化編碼器 初始化編碼器並設定預設值:
#include "lame.h"
   lame_global_flags *gfp;
   gfp = lame_init();

/*The default (if you set nothing) is a  J-Stereo, 44.1khz
128kbps CBR mp3 file at quality 5.  */

   lame_set_num_channels(gfp,2);
   lame_set_in_samplerate(gfp,44100);
   lame_set_brate(gfp,128);
   lame_set_mode(gfp,1);
   lame_set_quality(gfp,2);   /* 2=high  5 = medium  7=low */
複製程式碼

在lame.h檔案中定義了lame_glob_flags的一種簡寫形式:typedef lame_global_flags *lame_t;我們就可以使用lame_t。

  1. 設定引數
zret_code = lame_init_params(gfp);
複製程式碼

這個需要檢查錯誤,因為可能會有錯誤的引數。

  1. 編碼 輸出源時PCM資料,輸出時mp3的幀,我們需要先設定一個緩衝區,來存放編碼後的mp3資料,這個資料的大小可以根據取樣率和取樣數來計算。一個公式如下:
mp3buffer_size (in bytes) = 1.25*num_samples + 7200.
複製程式碼

接下來是將取樣資料生成為mp3資料,存入上邊分配的緩衝區:

int lame_encode_buffer(lame_global_flags *gfp,
     short int leftpcm[], short int rightpcm[],
     int num_samples,char *mp3buffer,int  mp3buffer_size);
複製程式碼

編碼成功的話會返回編碼的數量,有可能為0.假如編碼不成功就會返回一個負數。

  1. 編碼結束 編碼器可能會持有最後幾個資料,需要呼叫這個函式:
int lame_encode_flush(lame_global_flags *,char *mp3buffer, int mp3buffer_size);
複製程式碼

函式的返回值是最後的資料,大多數情況下是0。

  1. 寫入tag

這個地方主要是寫入上邊提到的一些ID3等幀資訊

void lame_mp3_tags_fid(lame_global_flags *,FILE* fid);
複製程式碼
  1. 釋放資源 最後我們需要呼叫
void lame_close(lame_global_flags *);
複製程式碼

最後附上demo的github地址: github.com/rangaofei/A…

參考:

  1. 音視訊開發進階指南
  2. 維基百科-mp3
  3. developer.samsung.com/technical-d…

最後的最後,說一下最近自己的一點事。我普通211非計算機專業,11年畢業,畢業之後一直在央企工作,後來因為興趣原因轉行做安卓開發,已過而立之年,目前在江蘇一個小城市做安卓開發,沒有大公司工作背景,想去上海試一試機會,經歷了無數次失敗了,包括阿里內推,中通等,這些經歷都使我認清了自己現在的劣勢。這也是一個非常沮喪的過程,因為多年的努力被一個人輕輕鬆鬆否定確實很喪氣。不過我有我自己的優勢,我的技能不一定會匹配所有人的技能要求,運氣不在的時候需要練好內功,提升自己,現在的不認可不等於將來的不認可,留給我的時間不多了,但我的路還很長,希望和我一樣的小夥伴也能像我一樣,儘快調整過來。

相關文章