1. 程式人生 > >Android直播開發之旅(4):MP3編碼格式分析與lame庫編譯封裝

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;
    }