ffmpeg开发之旅(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; 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方法
public class LameMp3 { static { System.loadLibrary("LameMp3" ); } public native static void lameInit( int inSampleRate, int outChannel, int outSampleRate, int outBitRate, int quality); public native static int lameEncode( short [] letftBuf, short [] rightBuf, int sampleRate, byte [] mp3Buf); public native static int lameFlush( byte [] mp3buf); 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
#include <jni.h> #include "LameMp3.h" #include "lame/lame.h" 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; } gfp = lame_init(); LOGI("初始化lame库完成" ); lame_set_in_samplerate(gfp,inSampleRate); lame_set_num_channels(gfp,outChannelNum); lame_set_out_samplerate(gfp,outSampleRate); lame_set_mode(gfp,MPEG_mode); lame_set_brate(gfp,outBitRate); 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_); int resut = lame_encode_flush(gfp, mp3buf, len); (*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_); int result = lame_encode_buffer(gfp, letftBuf, rightBuf, sampleRate, mp3Buf, readSizes); 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(); mMp3Recorder.setAudioSource(Mp3Recorder.AUDIO_SOURCE_MIC); mMp3Recorder.setAudioSampleRare(Mp3Recorder.SMAPLE_RATE_8000HZ); mMp3Recorder.setAudioChannelConfig(Mp3Recorder.AUDIO_CHANNEL_MONO); mMp3Recorder.setAduioFormat(Mp3Recorder.AUDIO_FORMAT_16Bit); mMp3Recorder.setLameBitRate(Mp3Recorder.LAME_BITRATE_32); mMp3Recorder.setLameOutChannel(Mp3Recorder.LAME_OUTCHANNEL_1); mMp3Recorder.setMediaCodecBitRate(Mp3Recorder.ENCODEC_BITRATE_1600HZ); mMp3Recorder.setMediaCodecSampleRate(Mp3Recorder.SMAPLE_RATE_8000HZ); 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) 停止编码
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){ 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){ encodeBytes(audioBuffer,readBytes); } if (mode == MODE_MP3 || mode == MODE_BOTH){ if (fops == null ){ try { fops = new FileOutputStream(mp3Path); } catch (FileNotFoundException e) { e.printStackTrace(); } } 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){ int shortLen = readBytes / 2 ; ByteBuffer byteBuffer = ByteBuffer.wrap(data, 0 , readBytes); ShortBuffer shortBuffer = byteBuffer.order(ByteOrder.LITTLE_ENDIAN).asShortBuffer(); short [] shortData = new short [shortLen]; shortBuffer.get(shortData, 0 , shortLen); return shortData; }
GitHub地址:https://github.com/jiangdongguo/Lame4Mp3 欢迎大家star~( 附上LameToMp3 NDK工程 )