安卓不支援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結構圖
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 | 強調位 |
這個地方的內容較多,此處我不一一列舉,附上一個寫的比較詳細的部落格:
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]
主要有四點:
-
將libmp3lame資料夾下的所有內容拷貝到一個指定的地方,然後再將lame.h檔案考進來
-
找到
util.h
檔案,將其中的extern ieee754_float32_t fast_log2(ieee754_float32_t x);
替換為extern float fast_log2(float x);
-
找到
set_get.h
檔案。替換#include <lame.h>
為#include “lame.h”
-
假如出現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
複製程式碼
下邊是兩個檔案
- Application.mk
APP_PLATFORM := android-18
APP_ABI := all
APP_BUILD_SCRIPT := Android.mk
APP_CFLAGS += -DSTDC_HEADERS
複製程式碼
- 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文件:
-
獲取版本資訊(可選的) const char * get_lame_version(void);
-
錯誤資訊 預設情況下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);
}
複製程式碼
- 初始化編碼器 初始化編碼器並設定預設值:
#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。
- 設定引數
zret_code = lame_init_params(gfp);
複製程式碼
這個需要檢查錯誤,因為可能會有錯誤的引數。
- 編碼 輸出源時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.假如編碼不成功就會返回一個負數。
- 編碼結束 編碼器可能會持有最後幾個資料,需要呼叫這個函式:
int lame_encode_flush(lame_global_flags *,char *mp3buffer, int mp3buffer_size);
複製程式碼
函式的返回值是最後的資料,大多數情況下是0。
- 寫入tag
這個地方主要是寫入上邊提到的一些ID3等幀資訊
void lame_mp3_tags_fid(lame_global_flags *,FILE* fid);
複製程式碼
- 釋放資源 最後我們需要呼叫
void lame_close(lame_global_flags *);
複製程式碼
最後附上demo的github地址: github.com/rangaofei/A…
參考:
- 音視訊開發進階指南
- 維基百科-mp3
- developer.samsung.com/technical-d…
最後的最後,說一下最近自己的一點事。我普通211非計算機專業,11年畢業,畢業之後一直在央企工作,後來因為興趣原因轉行做安卓開發,已過而立之年,目前在江蘇一個小城市做安卓開發,沒有大公司工作背景,想去上海試一試機會,經歷了無數次失敗了,包括阿里內推,中通等,這些經歷都使我認清了自己現在的劣勢。這也是一個非常沮喪的過程,因為多年的努力被一個人輕輕鬆鬆否定確實很喪氣。不過我有我自己的優勢,我的技能不一定會匹配所有人的技能要求,運氣不在的時候需要練好內功,提升自己,現在的不認可不等於將來的不認可,留給我的時間不多了,但我的路還很長,希望和我一樣的小夥伴也能像我一樣,儘快調整過來。