NDK之LAME编码Mp3实战
LAME是目前非常优秀的一种MP3编码引擎,在业界转码成MP3格式的音频文件时,最常用的编码器就是LAME库。用LAME的源码通过交叉编译就能得到SO库(这一部分在交叉编译那篇文章已经有完整的过程了,此处不再赘述),现在只需要把SO库集成到我们自己的项目中即可,需要做的就是编写好接口,上层调用即可。
LAME
PCM当达到320Kbit/s以上时,LAME编码出来的音频质量几乎可以和CD的音质相媲美,并且还能保证整个音频文件的体积非常小,因此若要在移动端平台上编码MP3文件,使用LAME便成为唯一的选择。
MP3参数
mp3(MPEG Layer III)这种格式在生活中很常见,但是mp3有很多种参数,这里讨论一下mp3编码所必须知道的一些参数。
- 采样率(sampleRate):采样率越高声音的还原度越好。
- 比特率(bitrate):每秒钟的数据量,越高音质越好。
- 声道数(channels):声道的数量,通常只有单声道和双声道,双声道即所谓的立体声。
- 比特率控制模式:ABR、VBR、CBR,这3中模式含义很容易查询到,不在赘述。
MPEG有几个版本的协议,不同版本的协议能够支持的参数能力是不同的。编码库的使用者必须清楚不同版本的区别才能正确的设置参数。
有以下3个版本的协议,MPEG1、MPEG2、MPEG2.5。其中MPEG2.5是非官方的标准,但是流传广泛,所以基本也都支持。他们的区别主要集中在支持的比特率和采样率不同。
采样率支持(Hz)
MPEG1 | MPEG2 | MPEG2.5 |
---|---|---|
44100 | 22050 | 11025 |
48000 | 24000 | 12000 |
32000 | 16000 | 8000 |
比特率支持(bit/s)
MPEG1 | MPEG2 | MPEG2.5 |
---|---|---|
32 | 8 | 8 |
40 | 16 | 16 |
48 | 24 | 24 |
56 | 32 | 32 |
64 | 40 | 40 |
80 | 48 | 48 |
96 | 56 | 56 |
112 | 64 | 64 |
128 | 80 | |
160 | 96 | |
192 | 112 | |
224 | 128 | |
256 | 144 | |
320 | 160 |
编码Mp3基本流程
编码mp3基本上遵循以下的流程
初始化编码参数
lame_init
:初始化一个编码参数的数据结构,给使用者用来设置参数。
设置编码参数
lame_set_in_samplerate
:设置被输入编码器的原始数据的采样率。lame_set_out_samplerate
:设置最终mp3编码输出的声音的采样率,如果不设置则和输入采样率一样。lame_set_num_channels
:设置被输入编码器的原始数据的声道数。lame_set_mode
:设置最终mp3编码输出的声道模式,如果不设置则和输入声道数一样。参数是枚举,STEREO
代表双声道,MONO
代表单声道。lame_set_VBR
:设置比特率控制模式,默认是CBR,但是通常我们都会设置VBR。参数是枚举,vbr_off
代表CBR,vbr_abr
代表ABR(因为ABR不常见,所以本文不对ABR做讲解)vbr_mtrh
代表VBR。lame_set_brate
:设置CBR的比特率,只有在CBR模式下才生效。lame_set_VBR_mean_bitrate_kbps
:设置VBR的比特率,只有在VBR模式下才生效。
其中每个参数都有默认的配置,如非必要可以不设置。这里只介绍了几个关键的设置接口,还有其他的设置接口可以参考lame.h
(lame的文档里只有命令行程序的用法,没有库接口的用法)。
初始化编码器器
lame_init_params
:根据上面设置好的参数建立编码器
编码PCM数据
lame_encode_buffer
或lame_encode_buffer_interleaved
:将PCM数据送入编码器,获取编码出的mp3数据。这些数据写入文件就是mp3文件。- 其中
lame_encode_buffer
输入的参数中是双声道的数据分别输入的,lame_encode_buffer_interleaved
输入的参数中双声道数据是交错在一起输入的。具体使用哪个需要看采集到的数据是哪种格式的,不过现在的设备采集到的数据大部分都是双声道数据是交错在一起。 - 单声道输入只能使用
lame_encode_buffer
,把单声道数据当成左声道数据传入,右声道传NULL即可。 - 调用这两个函数时需要传入一块内存来获取编码器出的数据,这块内存的大小lame给出了一种建议的计算方式:采样率/20+7200。
销毁编码器
lame_close
销毁编码器,释放资源。
编码器参数
对于编码器的参数设置,所能接受的参数值并不是任意的。上面表格中列出了编码器器能够支持的参数值,如果我们设置的参数值不在其中,那么编码器会自动帮我们选择一个最近的值。但是每个版本支持的参数范围不一致,假如设置了MPEG2的比特率又设置了MPEG1的采样率那么会发生什么?
Lame库会优先服从输出采样率的设置,根据采样率选择协议版本,然后在这个版本所能支持的比特率中选一个和设置比特率最接近的。
/*
* 0: MPEG-2 LSF
* 1: MPEG-1
* 2: MPEG-2.5 LSF FhG extention (1995-07-11 shn)
*/
typedef enum {
MPEG_2 = 0,
MPEG_1 = 1,
MPEG_25 = 2
} MPEG_t;
const int bitrate_table[3][16] = {
{0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1}, /* MPEG 2 */
{0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1}, /* MPEG 1 */
{0, 8, 16, 24, 32, 40, 48, 56, 64, -1, -1, -1, -1, -1, -1, -1}, /* MPEG 2.5 */
};
const int samplerate_table[3][4] = {
{22050, 24000, 16000, -1}, /* MPEG 2 */
{44100, 48000, 32000, -1}, /* MPEG 1 */
{11025, 12000, 8000, -1}, /* MPEG 2.5 */
};
编译Mp3实践
准备PCM文件
需要先准备PCM文件放在手机的公共存储区,lame的源码包里有一个testcase.wav,其实这个wav去掉前面的提出44位的wav前缀,就是pcm文件,通过代码去除前44位的wav前缀,修改后缀名即可得到testcase.pcm
public class WAV2PCM {
public static void main(String[] args) {
wavToPcmFilePath("/Users/zchanglin/Downloads/lame-3.100/testcase.wav");
}
public static String wavToPcmFilePath(String wavFile){
try {
byte[] buffer= new byte[1024];
// wav和PCM的区别就是wav在pcm的前面多了44字节
byte[] preBuffer= new byte[44];
int readByte = 0;
FileInputStream fis = new FileInputStream(wavFile);
String new_audio = wavFile.substring(0,wavFile.lastIndexOf(".")+1)+"pcm";
FileOutputStream fos = new FileOutputStream(new_audio);
// 提出44位的wav前缀
if (fis.read(preBuffer)==-1) {
return null;
}
// 复制PCM内容
while((readByte = fis.read(buffer)) != -1) {
fos.write(buffer,0,readByte);
}
fos.flush();
fos.close();
fis.close();
return new_audio;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
现在得到了PCM文件,也就是原始音频文件。
编写JNI层代码
新建一个C++的Native工程,删除native-lib.cpp,开始编写我们自己的调用库,app目录树如下:
├── assert
├── build
├── build.gradle
└── src
├── androidTest
├── main
│ ├── AndroidManifest.xml
│ ├── cpp
│ │ ├── CMakeLists.txt
│ │ ├── Mp3Encoder.cpp
│ │ ├── include
│ │ │ ├── Mp3Encoder.h
│ │ │ └── lame.h
│ │ └── lib
│ │ ├── arm64-v8a
│ │ │ └── liblame.so
│ │ └── armeabi-v7a
│ │ └── liblame.so
│ ├── java
│ │ └── com
│ │ └── xxx
│ │ └── mp3encode
│ │ ├── MainActivity.java
│ │ └── Mp3Encoder.java
│ └── res
│ └── layout
│ └── activity_main.xml
│
└── test
lame的so库得拷贝过来,头文件同样必不可少。
Mp3Encoder.java
package com.xxx.mp3encode;
/**
* Mp3编码器
*/
public class Mp3Encoder {
/**
* 编码(PCM -> Mp3文件)
*/
public native void encode();
/**
* 初始化编码器
* @param pcmPath
* @param audioChannels
* @param bitRate
* @param sampleRate
* @param mp3Path
* @return
*/
public native int init(String pcmPath, int audioChannels,
int bitRate,
int sampleRate, String mp3Path);
/**
* 销毁编码器
*/
public native void destroy();
}
通过javah命令生成头文件Mp3Encoder.h,然后稍作修改:
#include <jni.h>
#include <cstdio>
// 引入Lame头文件
#include "lame.h"
extern "C"
JNIEXPORT void JNICALL
Java_com_xxx_mp3encode_Mp3Encoder_encode
(JNIEnv *env, jobject);
extern "C" JNIEXPORT jint JNICALL
Java_com_xxx_mp3encode_Mp3Encoder_init
(JNIEnv *, jobject, jstring, jint, jint, jint, jstring);
extern "C" JNIEXPORT void JNICALL
Java_com_xxx_mp3encode_Mp3Encoder_destroy
(JNIEnv *, jobject);
class Mp3Encoder {
private:
FILE* pcmFile;
FILE* mp3File;
lame_t lameClient;
public:
Mp3Encoder();
~Mp3Encoder();
// 初始化准备工作
int Init(const char* pcmFilePath, const char *mp3FilePath,
int sampleRate, int channels, int bitRate);
// 编码
void Encode();
// 销毁
void Destroy();
};
Mp3Encoder mp3Encoder;
Mp3Encoder.cpp
#include "include/Mp3Encoder.h"
#include <android/log.h>
#include <string>
//定义输出的TAG
#define LOG_TAG "MP3Encoder"
#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
extern "C"
JNIEXPORT void JNICALL
Java_com_tal_mp3encode_Mp3Encoder_encode
(JNIEnv *env, jobject){
mp3Encoder.Encode();
}
Mp3Encoder::Mp3Encoder(){
};
Mp3Encoder::~Mp3Encoder() {
mp3Encoder.Destroy();
}
int Mp3Encoder::Init(const char* pcmFilePath, const char* mp3FilePath,
int sampleRate, int channels,
int bitRate) {
int ret = -1;
// 打开PCM文件流
LOGI("打开PCM文件流, pcmFilePath = %s", pcmFilePath);
pcmFile = fopen("/storage/emulated/0/testcase.pcm", "rb");
if(pcmFile == nullptr){
LOGE("PCM文件指针为NULL");
return -1;
}
// 打开Mp3文件流
mp3File = fopen(mp3FilePath, "wb");
if(mp3File){
// 初始化LAME
lameClient = lame_init();
// 设置被输入编码器的原始数据的采样率,默认44100hz
lame_set_in_samplerate(lameClient, sampleRate);
// 设置最终mp3编码输出的声音的采样率,如果不设置则和输入采样率一样
lame_set_out_samplerate(lameClient, sampleRate);
// 设置被输入编码器的原始数据的声道数,默认是2
lame_set_num_channels(lameClient, channels);
// 设置压缩比,默认11
lame_set_brate(lameClient, bitRate / 1000);
// 根据上面设置好的参数建立编码器
lame_init_params(lameClient);
ret = 0;
}
return ret;
}
void Mp3Encoder::Encode() {
int bufferSize = 1024 * 256;
auto buffer = new short[bufferSize / 2];
auto leftBuffer = new short[bufferSize / 4];
auto rightBuffer = new short[bufferSize / 4];
auto mp3_buffer = new unsigned char [bufferSize];
size_t readBufferSize = 0;
while ((readBufferSize = fread(buffer, 2, bufferSize / 2, pcmFile)) > 0){
for(int i = 0; i < readBufferSize; i++){
if(i % 2 == 0){
leftBuffer[i/2] = buffer[i];
}else{
rightBuffer[i/2] = buffer[i];
}
}
size_t wroteSize = lame_encode_buffer(
lameClient,
leftBuffer,
rightBuffer,
(int)readBufferSize/2,
mp3_buffer,
bufferSize);
fwrite(mp3_buffer, 1, wroteSize, mp3File);
}
delete[] buffer;
delete [] leftBuffer;
delete [] rightBuffer;
delete [] mp3_buffer;
}
void Mp3Encoder::Destroy() {
if(pcmFile) {
fclose(pcmFile);
}
if(mp3File) {
fclose(mp3File);
lame_close(lameClient);
}
}
/*
* Class: com_tal_mp3encoder_Mp3Encoder
* Method: init
* Signature: (Ljava/lang/String;IIILjava/lang/String;)I
*/
extern "C" JNIEXPORT jint JNICALL
Java_com_tal_mp3encode_Mp3Encoder_init
(JNIEnv* env, jobject, jstring pcmPathParam,
jint audioChannels, jint bitRate, jint sampleRate,
jstring mp3PathParam){
LOGI("mp3Encoder开始执行初始化");
const char* pcmPath = env->GetStringUTFChars(pcmPathParam, NULL);
const char* mp3Path = env->GetStringUTFChars(mp3PathParam, NULL);
return mp3Encoder.Init(pcmPath, mp3Path, sampleRate, audioChannels, bitRate);
}
/*
* Class: com_tal_mp3encoder_Mp3Encoder
* Method: destroy
* Signature: ()V
*/
extern "C" JNIEXPORT void JNICALL
Java_com_tal_mp3encode_Mp3Encoder_destroy
(JNIEnv *, jobject){
mp3Encoder.Destroy();
}
编写CMakeLists
CMakeLists.txt
# CMake最小版本
cmake_minimum_required(VERSION 3.18.1)
# 工程名称
project("mp3encode")
# 定义目标库
add_library(
mp3encode # 库名称
SHARED # 动态库
Mp3Encoder.cpp # 源文件
)
# 包含头文件
target_include_directories(
mp3encode # 库名称
PUBLIC # 头文件作用域
include/Mp3Encoder.h include/lame.h # 具体哪些头文件
)
# 查找预编译库
find_library(
log-lib
log # 日志库
${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}/liblame.so # lame库
)
# 目标库的链接
target_link_libraries(
mp3encode # 库名称
${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}/liblame.so # lame库
${log-lib} # 日志库
)
# 查看引入的lame库的路径
message("LibPath -> " ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}/liblame.so)
配置Gradle
build.gradle(app)
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.xxx.mp3encode"
minSdkVersion 22
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// 设置cmake编译脚本
externalNativeBuild {
cmake {
cppFlags '-std=c++11'
}
}
ndk {
abiFilters "armeabi-v7a"
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
cmake {
path file('src/main/cpp/CMakeLists.txt')
version '3.18.1'
}
}
buildFeatures {
viewBinding true
}
ndkVersion '20.0.5594570'
sourceSets {
main {
jniLibs.srcDirs = ['src/main/cpp/lib']
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
编写上层代码
MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// 申请权限请求码
private static final int REQUEST_EXTERNAL_STORAGE = 1001;
static {
System.loadLibrary("lame");
System.loadLibrary("mp3encode");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
verifyStoragePermissions(this);
}
public void startEncodeMp3(View view) {
File extDir = Environment.getExternalStorageDirectory();
File pcmCaseFile = new File(extDir, "testcase.pcm");
if(!pcmCaseFile.exists()) {
Log.i(TAG, "PCM文件不存在");
return;
}
File mp3File = new File(extDir, "testcase.mp3");
Mp3Encoder mp3Encoder = new Mp3Encoder();
mp3Encoder.init(
pcmCaseFile.getAbsolutePath(),
2,
5000,
44100,
mp3File.getAbsolutePath()
);
mp3Encoder.encode();
mp3Encoder.destroy();
Toast.makeText(this, "编码完成", Toast.LENGTH_SHORT).show();
TextView sampleTextView = (TextView) findViewById(R.id.sample_text);
sampleTextView.setText("Mp3 Path = " + mp3File.getAbsolutePath());
}
public static void verifyStoragePermissions(Activity activity) {
int writePermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
int readPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE);
if (writePermission != PackageManager.PERMISSION_GRANTED
|| readPermission != PackageManager.PERMISSION_GRANTED) {
// 如果没有权限需要动态地去申请权限
ActivityCompat.requestPermissions(
activity,
// 权限数组
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
// 权限请求码
REQUEST_EXTERNAL_STORAGE
);
}
}
}
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.xxx.mp3encode">
<!-- 在SDCard中创建与删除文件的权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Mp3Encode">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
OK,点击按钮触发编码Mp3,Success!