音视频探索(4):Mp3格式与Lame库编译移植

1. 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.1 MP3文件结构

 MP3文件有由帧(frame)构成的,帧是MP3文件最小的组成单位。MP3音频文件本身没有头部,当希望读取有关MP3音频文件的信息时,可以读取第一帧的头部信息,因此可以切割MP3音频文件的任何部分进行正确播放。整个MP3文件结构大体包括三部分,即TAG_V2(ID3V2)、Frame、TAG_V1(ID3V1),具体描述如下:

a.png

1.2 MP3帧格式

 每个帧都是独立的,它由帧头、附加信息和声音数据组成,其长度随位率的不同而不等,通常每个帧的播放时间为0.026秒。MP3帧结构如下:

b.png

 每帧的帧头占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);其他参数请看这篇文章

2. lame编译与封装

Lame是Mike Cheng于1998年发起的一个开源项目,是目前最好的MP3编码引擎。Lame编码出来的MP3音色纯厚、空间宽广、低音清晰、细节表现良好,它独创的心理音响模型技术保证了CD音频还原的真实性,配合VBR和ABR参数,音质几乎可以媲美CD音频,但文件体积却非常小。

2.1 Lame库编译与封装

c.png

2.1.1 移植Lame库到Android工程

(1) 解压lame

  • 解压lame-3.99.5,将源码中的libmp3lame目录拷贝到Android工程的cpp目录下;
  • 将libmp3lame重命名为lame,并删除i386目录、vector目录、depcomp、lame.rc、logoe.ico、Makefile.am、Makefile.in文件;
  • 拷贝源码中inlude目录下lame.h文件到Android工程cpp目录下lame目录中,lame.h头文件包含了所有调用函数的声明;
  • 配置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"
    }
}
复制代码

2.2 自定义开源库:Lame4Mp3

 Lame4Mp3是基于Lame库实现的开源项目,本项目结合Android官方提供的MediaCodec API,可以满足将PCM数据流编码为AAC或MP3格式数据,并且支持AAC和Mp3同时编码,适用于本地录制mp3/aac文件和在Android直播中进行边播边录(mp3)等场合。

2.2.1 添加依赖

(1) 在工程build.gradle中添加

allprojects {
   repositories {
    ...
   maven { url 'https://jitpack.io' }
  }
}
复制代码

(2) 在module的gradle中添加

dependencies {
   compile 'com.github.jiangdongguo:Lame4Mp3:v1.0.0'
}
复制代码

2.2.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();
复制代码

最后,有一点需要注意的是,当同时编码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;
    }
复制代码

GitHub地址:github.com/jiangdonggu… 欢迎大家star~( 附上LameToMp3 NDK工程)

猜你喜欢

转载自juejin.im/post/7032215926577561608