Android直播開發之旅(4):MP3編碼格式分析與lame庫編譯封裝
轉載請宣告出處:http://blog.csdn.net/andrexpert/article/77683776
一、Mp3編碼格式分析
MP3,全稱MPEG Audio Layer3,是一種高效的計算機音訊編碼方案,它以較大的壓縮比(1:10至1:12)將音訊檔案轉換成較小的副檔名為.mp3的檔案,且能基本保持原檔案的音質。假如有一個4分鐘的CD音質的WAV音訊,其音訊引數為44.1kHz抽樣、立體聲、取樣精度為16位(2位元組),那麼該音訊所佔空間為441000*2(聲道)*2(位元組)*60(秒)*4(分鐘)=40.4MB,而對於MP3格式來說,MP3音訊只佔4MB左右,有利於儲存和網路傳輸。
1. MP3檔案結構
MP3檔案有由幀(frame)構成的,幀是MP3檔案最小的組成單位。MP3音訊檔案本身沒有頭部,當希望讀取有關MP3音訊檔案的資訊時,可以讀取第一幀的頭部資訊,因此可以切割MP3音訊檔案的任何部分進行正確播放。整個MP3檔案結構大體包括三部分,即TAG_V2(ID3V2)、Frame、TAG_V1(ID3V1),具體描述如下:
2. MP3幀格式
每個幀都是獨立的,它由幀頭、附加資訊和聲音資料組成,其長度隨位率的不同而不等,通常每個幀的播放時間為0.026秒。MP3幀結構如下:
每幀的幀頭佔4位元組(32位),幀頭後面可能有兩個位元組的CRC校驗,這兩個位元組的是否存在取決於幀頭部的第16bit,如果為0,則幀頭後面無校驗,為1則有校驗。幀頭結構如下:
typedefstruct-tagHeader{ unsigned int sync: 佔11位 //同步資訊 unsigned int version: 2; //版本 unsigned int layer: 2; //層 unsigned int error2protection: 1; //CRC校正 unsigned int bit2rate2index: 4; //位率索引 unsigned int sample2rate2index: 2; //取樣率索引 unsigned int padding: 1; //空白字 unsigned int extension: 1; //私有標誌 unsigned int channel2mode: 2; //立體聲模式 unsigned int modeextension: 2 ;//保留 unsigned int copyright: 1; //版權標誌 unsigned int original: 1; //原始媒體 unsigned int emphasis: 2 ;//強調方式 } HEADER;
其中,sync為同步資訊,佔11位,全部被設定為1;channel2mode為立體聲通道模式,佔2為,11表示Single立體聲(Mono);其他引數請看這篇文章。
二、lame編譯與封裝
1. Lame庫簡介
Lame是Mike Cheng於1998年發起的一個開源專案,是目前最好的MP3編碼引擎。Lame編碼出來的MP3音色純厚、空間寬廣、低音清晰、細節表現良好,它獨創的心理音響模型技術保證了CD音訊還原的真實性,配合VBR和ABR引數,音質幾乎可以媲美CD音訊,但檔案體積卻非常小。
最新版下載:https://sourceforge.net/projects/lame/files/lame/3.99/
2. Lame庫編譯與封裝
(1) 移植Lame庫到Android工程
a. 解壓lame-3.99.5,將原始碼中的libmp3lame目錄拷貝到Android工程的cpp目錄下;
b. 將libmp3lame重新命名為lame,並刪除i386目錄、vector目錄、depcomp、lame.rc、logoe.ico、Makefile.am、Makefile.in檔案;
c. 拷貝原始碼中inlude目錄下lame.h檔案到Android工程cpp目錄下lame目錄中,lame.h標頭檔案包含了所有呼叫函式的宣告;
d. 配置CMakeLists.txt檔案
set(SRC_DIR src/main/cpp/lame)
include_directories(src/main/cpp/lame)
aux_source_directory(src/main/cpp/lame SRC_LIST)
add_library(...... ${SRC_LIST})
(2) LameMp3.java,建立呼叫lame庫函式的native方法
/** JNI呼叫lame庫實現mp3檔案封裝
* Created by Jiangdg on 2017/6/9.
*/
public class LameMp3 {
// 靜態載入共享庫LameMp3
static {
System.loadLibrary("LameMp3");
}
/** 初始化lame庫,配置相關資訊
*
* @param inSampleRate pcm格式音訊取樣率
* @param outChannel pcm格式音訊通道數量
* @param outSampleRate mp3格式音訊取樣率
* @param outBitRate mp3格式音訊位元率
* @param quality mp3格式音訊質量,0~9,最慢最差~最快最好
*/
public native static void lameInit(int inSampleRate, int outChannel,int outSampleRate, int outBitRate, int quality);
/** 編碼pcm成mp3格式
*
* @param letftBuf 左pcm資料
* @param rightBuf 右pcm資料,如果是單聲道,則一致
* @param sampleRate 讀入的pcm位元組大小
* @param mp3Buf 存放mp3資料快取
* @return 編碼資料位元組長度
*/
public native static int lameEncode(short[] letftBuf, short[] rightBuf,int sampleRate, byte[] mp3Buf);
/** 儲存mp3音訊流到檔案
*
* @param mp3buf mp3資料流
* @return 資料流長度rty
*/
public native static int lameFlush(byte[] mp3buf);
/**
* 釋放lame庫資源
*/
public native static void lameClose();
}
講解一下:通過檢視Lame庫的API文件(lame-3.99.5\API)可知,使用Lame封裝Mp3需要經歷四個步驟,即初始化lame引擎、編碼pcm為mp3資料幀、寫入檔案、釋放lame引擎資源。因此,在LameMp3 .java中,我們定義與之對應的native方法以便java層呼叫,最終生成所需的mp3格式檔案。
(3) LameMp3.c
// 本地實現
// Created by jianddongguo on 2017/6/14.
#include <jni.h>
#include "LameMp3.h"
#include "lame/lame.h"
// 宣告一個lame_global_struct指標變數
// 可認為是一個全域性上下文
static lame_global_flags *gfp = NULL;
JNIEXPORT void JNICALL
Java_com_teligen_lametomp3_LameMp3_lameInit(JNIEnv *env, jclass type, jint inSampleRate,
jint outChannelNum, jint outSampleRate, jint outBitRate,
jint quality) {
if(gfp != NULL){
lame_close(gfp);
gfp = NULL;
}
// 初始化編碼器引擎,返回一個lame_global_flags結構體型別指標
// 說明編碼所需記憶體分配完成,否則,返回NULL
gfp = lame_init();
LOGI("初始化lame庫完成");
// 設定輸入資料流的取樣率,預設為44100Hz
lame_set_in_samplerate(gfp,inSampleRate);
// 設定輸入資料流的通道數量,預設為2
lame_set_num_channels(gfp,outChannelNum);
// 設定輸出資料流的取樣率,預設為0,單位KHz
lame_set_out_samplerate(gfp,outSampleRate);
lame_set_mode(gfp,MPEG_mode);
// 設定位元壓縮率,預設為11
lame_set_brate(gfp,outBitRate);
// 編碼質量,推薦2、5、7
lame_set_quality(gfp,quality);
// 配置引數
lame_init_params(gfp);
LOGI("配置lame引數完成");
}
JNIEXPORT jint JNICALL
Java_com_teligen_lametomp3_LameMp3_lameFlush(JNIEnv *env, jclass type, jbyteArray mp3buf_) {
jbyte *mp3buf = (*env)->GetByteArrayElements(env, mp3buf_, NULL);
jsize len = (*env)->GetArrayLength(env,mp3buf_);
// 重新整理pcm快取,以"0"填充保證最後幾幀的完整
// 重新整理mp3快取,返回最後的幾幀
int resut = lame_encode_flush(gfp, // 全域性上下文
mp3buf, // 指向mp3快取的指標
len); // 有效mp3資料長度
(*env)->ReleaseByteArrayElements(env, mp3buf_, mp3buf, 0);
LOG_I("寫入mp3資料到檔案,返回幀數=%d",resut);
return resut;
}
JNIEXPORT void JNICALL
Java_com_teligen_lametomp3_LameMp3_lameClose(JNIEnv *env, jclass type) {
// 釋放所佔記憶體資源
lame_close(gfp);
gfp = NULL;
LOGI("釋放lame資源");
}
JNIEXPORT jint JNICALL
Java_com_teligen_lametomp3_LameMp3_lameEncode(JNIEnv *env, jclass type, jshortArray letftBuf_,
jshortArray rightBuf_, jint sampleRate,
jbyteArray mp3Buf_) {
if(letftBuf_ == NULL || mp3Buf_ == NULL){
LOGI("letftBuf和rightBuf 或mp3Buf_不能為空");
return -1;
}
jshort *letftBuf = NULL;
jshort *rightBuf = NULL;
if(letftBuf_ != NULL){
letftBuf = (*env)->GetShortArrayElements(env, letftBuf_, NULL);
}
if(rightBuf_ != NULL){
rightBuf = (*env)->GetShortArrayElements(env, rightBuf_, NULL);
}
jbyte *mp3Buf = (*env)->GetByteArrayElements(env, mp3Buf_, NULL);
jsize readSizes = (*env)->GetArrayLength(env,mp3Buf_);
// 將PCM資料編碼為mp3
int result = lame_encode_buffer(gfp, // 全域性上下文
letftBuf, // 左通道pcm資料
rightBuf, // 右通道pcm資料
sampleRate, // 通道資料流取樣率
mp3Buf, // mp3資料快取起始地址
readSizes); // 快取地址中有效mp3資料長度
// 釋放資源
if(letftBuf_ != NULL){
(*env)->ReleaseShortArrayElements(env, letftBuf_, letftBuf, 0);
}
if(rightBuf_ != NULL){
(*env)->ReleaseShortArrayElements(env, rightBuf_, rightBuf, 0);
}
(*env)->ReleaseByteArrayElements(env, mp3Buf_, mp3Buf, 0);
LOG_I("編碼pcm為mp3,資料長度=%d",result);
return result;
}
講解一下:通過檢視lame.h原始碼,gfp 為結構體lame_global_struct的一個指標變數,該變數用於指向該結構體。lame_global_struct結構體聲明瞭編碼所需的各種引數,具體程式碼如下:
lame_global_flags *gfp = NULL;
typedef struct lame_global_struct lame_global_flags;
struct lame_global_struct {
unsigned int class_id;
unsigned long num_samples;
int num_channels;
int samplerate_in;
int samplerate_out; brate;
float compression_ratio;
.....
}
另外,在配置lame編碼引擎時,有一個lame_set_quality函式用來設定編碼的質量。也許你會問,音訊編碼質量一般不是由位元率決定的,為什麼還需要這個設定?嗯,位元率決定編碼質量是沒錯的,這裡的引數主要是用來選擇編碼處理的演算法,不同的演算法處理的效果和速度是不一樣的。比如,當quality為0時,選擇的演算法是最好的,但處理的速度是最慢的;當quality為9時,選擇的演算法是最差的,但是速度是最快的。通常,官方推薦以下三種設定,即:
quality= 2 質量接近最好,速度不是很慢;
quality=5 質量很好,速度還行;
quality=7 質量良好, 速度很快;
(4) CMakeList.txt
#指定所需的Cmake最低版本
cmake_minimum_required(VERSION 3.4.1)
#指定原始碼路徑,即將src/main/cpp/lame路徑賦值給SRC_DIR
set(SRC_DIR src/main/cpp/lame)
# 指定標頭檔案路徑
include_directories(src/main/cpp/lame)
# 將src/main/cpp/lame目錄下的所有檔名賦值給SRC_LIST
aux_source_directory(src/main/cpp/lame SRC_LIST)
# add_library:指定生成庫檔案,包括三個引數:
# LameMp3為庫檔案的名稱;SHARED表示動態連結庫;
# src/main/cpp/LameMp3.c和${SRC_LIST}指定生成庫檔案所需的原始檔
#其中,${}的作用是引入src/main/cpp/lame目錄下的所有原始檔
add_library(
LameMp3
SHARED
src/main/cpp/LameMp3.c ${SRC_LIST})
#在指定的目錄中搜索庫log,並將其路徑儲存到變數log-lib中
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# 將庫${log-lib} 連結到LameMp3動態庫中,包括兩個引數
#LameMp3為目標庫
# ${log-lib}為要連結的庫
target_link_libraries( # Specifies the target library.
LameMp3
# Links the target library to the log library
# included in the NDK.
${log-lib} )
講解一下:Cmake是一個跨平臺的編譯工具,它允許使用簡單的語句來描述所有平臺的編譯過程,並輸出各種型別的Makefile或Project檔案。Cmake所有的語句命令都寫在CMakeLists.txt檔案中,主要規則如下:
a. 在Cmake中,註釋由#字元開始到此行的結束;
b. 命令不區分大小寫,引數需區分大小寫;
c. 命令由命令名、引數列表組成,引數間使用空格進行分隔;
(5) build.gradle(Module app),選擇編譯平臺
android {
defaultConfig {
// ...程式碼省略
externalNativeBuild {
cmake {
cppFlags ""
}
}
// 選擇編譯平臺
ndk{
abiFilters 'x86', 'x86_64', 'armeabi', 'armeabi-v7a','arm64-v8a'
}
}
// ...程式碼省略
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
三、開源專案:Lame4Mp3
Lame4Mp3是基於Lame庫實現的開源專案,本專案結合Android官方提供的MediaCodec API,可以滿足將PCM資料流編碼為AAC或MP3格式資料,並且支援AAC和Mp3同時編碼,適用於本地錄製mp3/aac檔案和在Android直播中進行邊播邊錄(mp3)等場合。使用方法和原始碼分析如下:
1. 新增依賴
(1) 在工程build.gradle中新增
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
(2) 在module的gradle中新增
dependencies {
compile 'com.github.jiangdongguo:Lame4Mp3:v1.0.0'
}
2. Lame4Mp3使用方法
(1) 配置引數
Mp3Recorder mMp3Recorder = Mp3Recorder.getInstance();
// 配置AudioRecord引數
mMp3Recorder.setAudioSource(Mp3Recorder.AUDIO_SOURCE_MIC);
mMp3Recorder.setAudioSampleRare(Mp3Recorder.SMAPLE_RATE_8000HZ);
mMp3Recorder.setAudioChannelConfig(Mp3Recorder.AUDIO_CHANNEL_MONO);
mMp3Recorder.setAduioFormat(Mp3Recorder.AUDIO_FORMAT_16Bit);
// 配置Lame引數
mMp3Recorder.setLameBitRate(Mp3Recorder.LAME_BITRATE_32);
mMp3Recorder.setLameOutChannel(Mp3Recorder.LAME_OUTCHANNEL_1);
// 配置MediaCodec引數
mMp3Recorder.setMediaCodecBitRate(Mp3Recorder.ENCODEC_BITRATE_1600HZ);
mMp3Recorder.setMediaCodecSampleRate(Mp3Recorder.SMAPLE_RATE_8000HZ);
// 設定模式
// Mp3Recorder.MODE_AAC 僅編碼得到AAC資料流
// Mp3Recorder.MODE_MP3 僅編碼得到Mp3檔案
// Mp3Recorder.MODE_BOTH 同時編碼
mMp3Recorder.setMode(Mp3Recorder.MODE_BOTH);
(2) 開始編碼
mMp3Recorder.start(filePath, fileName, new Mp3Recorder.OnAACStreamResultListener() {
@Override
public void onEncodeResult(byte[] data, int offset, int length, long timestamp) {
Log.i("MainActivity","acc資料流長度:"+data.length);
}
});
(3) 停止編碼
mMp3Recorder.stop();
3. Lame4Mp3原始碼解析
Mp3Recorder.java中主要包括三個功能塊:PCM資料採集、AAC編碼、Mp3編碼,其中,PCM資料採集和AAC編碼在以前的博文中有詳細剖析,所以這裡只著重解析Mp3編碼,核心程式碼如下:
public void start(final String filePath, final String fileName,final OnAACStreamResultListener listener){
this.listener = listener;
new Thread(new Runnable() {
@Override
public void run() {
try {
if(!isRecording){
// 第一步:初始化lame引擎
initLameMp3();
initAudioRecord();
initMediaCodec();
}
int readBytes = 0;
byte[] audioBuffer = new byte[2048];
byte[] mp3Buffer = new byte[1024];
// 如果檔案路徑不存在,則建立
if(TextUtils.isEmpty(filePath) || TextUtils.isEmpty(fileName)){
Log.i(TAG,"檔案路徑或檔名為空");
return;
}
File file = new File(filePath);
if(! file.exists()){
file.mkdirs();
}
String mp3Path = file.getAbsoluteFile().toString()+File.separator+fileName+".mp3";
FileOutputStream fops = null;
try {
while(isRecording){
readBytes = mAudioRecord.read(audioBuffer,0,bufferSizeInBytes);
Log.i(TAG,"讀取pcm資料流,大小為:"+readBytes);
if(readBytes >0 ){
if(mode == MODE_AAC || mode == MODE_BOTH){
// 將PCM編碼為AAC
encodeBytes(audioBuffer,readBytes);
}
if(mode == MODE_MP3 || mode == MODE_BOTH){
// 開啟mp3檔案輸出流
if(fops == null){
try {
fops = new FileOutputStream(mp3Path);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
// 將byte[] 轉換為 short[]
// 將PCM編碼為Mp3,並寫入檔案
short[] data = transferByte2Short(audioBuffer,readBytes);
int encResult = LameMp3.lameEncode(data,null,data.length,mp3Buffer);
Log.i(TAG,"lame編碼,大小為:"+encResult);
if(encResult != 0){
try {
fops.write(mp3Buffer,0,encResult);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
// 錄音完畢
if(fops != null){
int flushResult = LameMp3.lameFlush(mp3Buffer);
Log.i(TAG,"錄製完畢,大小為:"+flushResult);
if(flushResult > 0){
try {
fops.write(mp3Buffer,0,flushResult);
} catch (IOException e) {
e.printStackTrace();
}
}
try {
fops.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}finally {
Log.i(TAG,"釋放AudioRecorder資源");
stopAudioRecorder();
stopMediaCodec();
}
}finally {
Log.i(TAG,"釋放Lame庫資源");
stopLameMp3();
}
}
}).start();
}
從程式碼可以看出,使用lame引擎編碼pcm得到mp3資料,將經歷四個步驟:初始化引擎、編碼、寫入檔案、釋放記憶體資源,這個過程與之前我們詳細分析的流程一致。但是,有一點需要注意的是,當同時編碼AAC和Mp3時,向MediaCodec和Lame引擎輸入PCM資料流的方式是不一樣的,前者只接受byte[]儲存的資料,後者接收short[]儲存的資料。也就是說,如果將採集的pcm資料以byte[]來儲存,我們需要將其轉換為short[],並且需要注意大小端的問題。具體程式碼如下:
private short[] transferByte2Short(byte[] data,int readBytes){
// byte[] 轉 short[],陣列長度縮減一半
int shortLen = readBytes / 2;
// 將byte[]陣列裝如ByteBuffer緩衝區
ByteBuffer byteBuffer = ByteBuffer.wrap(data, 0, readBytes);
// 將ByteBuffer轉成小端並獲取shortBuffer
// 小端:資料的高位元組儲存到記憶體的高地址中,資料的低位元組儲存到記憶體的低地址中
ShortBuffer shortBuffer = byteBuffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer();
short[] shortData = new short[shortLen];
shortBuffer.get(shortData, 0, shortLen);
return shortData;
}