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库会优先服从输出采样率的设置,根据采样率选择协议版本,然后在这个版本所能支持的比特率中选一个和设置比特率最接近的。
1/*
2 * 0: MPEG-2 LSF
3 * 1: MPEG-1
4 * 2: MPEG-2.5 LSF FhG extention (1995-07-11 shn)
5 */
6typedef enum {
7 MPEG_2 = 0,
8 MPEG_1 = 1,
9 MPEG_25 = 2
10} MPEG_t;
11const int bitrate_table[3][16] = {
12 {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, -1}, /* MPEG 2 */
13 {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1}, /* MPEG 1 */
14 {0, 8, 16, 24, 32, 40, 48, 56, 64, -1, -1, -1, -1, -1, -1, -1}, /* MPEG 2.5 */
15};
16const int samplerate_table[3][4] = {
17 {22050, 24000, 16000, -1}, /* MPEG 2 */
18 {44100, 48000, 32000, -1}, /* MPEG 1 */
19 {11025, 12000, 8000, -1}, /* MPEG 2.5 */
20};
编译Mp3实践
准备PCM文件
需要先准备PCM文件放在手机的公共存储区,lame的源码包里有一个testcase.wav,其实这个wav去掉前面的提出44位的wav前缀,就是pcm文件,通过代码去除前44位的wav前缀,修改后缀名即可得到testcase.pcm
1public class WAV2PCM {
2 public static void main(String[] args) {
3 wavToPcmFilePath("/Users/zchanglin/Downloads/lame-3.100/testcase.wav");
4 }
5
6 public static String wavToPcmFilePath(String wavFile){
7 try {
8 byte[] buffer= new byte[1024];
9 // wav和PCM的区别就是wav在pcm的前面多了44字节
10 byte[] preBuffer= new byte[44];
11 int readByte = 0;
12 FileInputStream fis = new FileInputStream(wavFile);
13 String new_audio = wavFile.substring(0,wavFile.lastIndexOf(".")+1)+"pcm";
14 FileOutputStream fos = new FileOutputStream(new_audio);
15 // 提出44位的wav前缀
16 if (fis.read(preBuffer)==-1) {
17 return null;
18 }
19 // 复制PCM内容
20 while((readByte = fis.read(buffer)) != -1) {
21 fos.write(buffer,0,readByte);
22 }
23 fos.flush();
24 fos.close();
25 fis.close();
26 return new_audio;
27 } catch (IOException e) {
28 e.printStackTrace();
29 }
30 return null;
31 }
32}
现在得到了PCM文件,也就是原始音频文件。
编写JNI层代码
新建一个C++的Native工程,删除native-lib.cpp,开始编写我们自己的调用库,app目录树如下:
1├── assert
2├── build
3├── build.gradle
4└── src
5 ├── androidTest
6 ├── main
7 │ ├── AndroidManifest.xml
8 │ ├── cpp
9 │ │ ├── CMakeLists.txt
10 │ │ ├── Mp3Encoder.cpp
11 │ │ ├── include
12 │ │ │ ├── Mp3Encoder.h
13 │ │ │ └── lame.h
14 │ │ └── lib
15 │ │ ├── arm64-v8a
16 │ │ │ └── liblame.so
17 │ │ └── armeabi-v7a
18 │ │ └── liblame.so
19 │ ├── java
20 │ │ └── com
21 │ │ └── xxx
22 │ │ └── mp3encode
23 │ │ ├── MainActivity.java
24 │ │ └── Mp3Encoder.java
25 │ └── res
26 │ └── layout
27 │ └── activity_main.xml
28 │
29 └── test
lame的so库得拷贝过来,头文件同样必不可少。
Mp3Encoder.java
1package com.xxx.mp3encode;
2
3/**
4 * Mp3编码器
5 */
6public class Mp3Encoder {
7 /**
8 * 编码(PCM -> Mp3文件)
9 */
10 public native void encode();
11
12 /**
13 * 初始化编码器
14 * @param pcmPath
15 * @param audioChannels
16 * @param bitRate
17 * @param sampleRate
18 * @param mp3Path
19 * @return
20 */
21 public native int init(String pcmPath, int audioChannels,
22 int bitRate,
23 int sampleRate, String mp3Path);
24 /**
25 * 销毁编码器
26 */
27 public native void destroy();
28}
通过javah命令生成头文件Mp3Encoder.h,然后稍作修改:
1#include <jni.h>
2#include <cstdio>
3// 引入Lame头文件
4#include "lame.h"
5
6extern "C"
7JNIEXPORT void JNICALL
8Java_com_xxx_mp3encode_Mp3Encoder_encode
9(JNIEnv *env, jobject);
10
11extern "C" JNIEXPORT jint JNICALL
12Java_com_xxx_mp3encode_Mp3Encoder_init
13 (JNIEnv *, jobject, jstring, jint, jint, jint, jstring);
14
15extern "C" JNIEXPORT void JNICALL
16Java_com_xxx_mp3encode_Mp3Encoder_destroy
17 (JNIEnv *, jobject);
18
19class Mp3Encoder {
20private:
21 FILE* pcmFile;
22 FILE* mp3File;
23 lame_t lameClient;
24public:
25 Mp3Encoder();
26 ~Mp3Encoder();
27 // 初始化准备工作
28 int Init(const char* pcmFilePath, const char *mp3FilePath,
29 int sampleRate, int channels, int bitRate);
30 // 编码
31 void Encode();
32 // 销毁
33 void Destroy();
34};
35
36Mp3Encoder mp3Encoder;
Mp3Encoder.cpp
1#include "include/Mp3Encoder.h"
2#include <android/log.h>
3#include <string>
4
5//定义输出的TAG
6#define LOG_TAG "MP3Encoder"
7#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)
8#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
9#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
10#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
11#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)
12
13extern "C"
14JNIEXPORT void JNICALL
15Java_com_tal_mp3encode_Mp3Encoder_encode
16 (JNIEnv *env, jobject){
17 mp3Encoder.Encode();
18}
19
20Mp3Encoder::Mp3Encoder(){
21
22};
23
24Mp3Encoder::~Mp3Encoder() {
25 mp3Encoder.Destroy();
26}
27
28int Mp3Encoder::Init(const char* pcmFilePath, const char* mp3FilePath,
29 int sampleRate, int channels,
30 int bitRate) {
31 int ret = -1;
32 // 打开PCM文件流
33 LOGI("打开PCM文件流, pcmFilePath = %s", pcmFilePath);
34 pcmFile = fopen("/storage/emulated/0/testcase.pcm", "rb");
35 if(pcmFile == nullptr){
36 LOGE("PCM文件指针为NULL");
37 return -1;
38 }
39
40 // 打开Mp3文件流
41 mp3File = fopen(mp3FilePath, "wb");
42 if(mp3File){
43 // 初始化LAME
44 lameClient = lame_init();
45 // 设置被输入编码器的原始数据的采样率,默认44100hz
46 lame_set_in_samplerate(lameClient, sampleRate);
47 // 设置最终mp3编码输出的声音的采样率,如果不设置则和输入采样率一样
48 lame_set_out_samplerate(lameClient, sampleRate);
49 // 设置被输入编码器的原始数据的声道数,默认是2
50 lame_set_num_channels(lameClient, channels);
51 // 设置压缩比,默认11
52 lame_set_brate(lameClient, bitRate / 1000);
53 // 根据上面设置好的参数建立编码器
54 lame_init_params(lameClient);
55 ret = 0;
56 }
57 return ret;
58}
59
60void Mp3Encoder::Encode() {
61 int bufferSize = 1024 * 256;
62 auto buffer = new short[bufferSize / 2];
63 auto leftBuffer = new short[bufferSize / 4];
64 auto rightBuffer = new short[bufferSize / 4];
65
66 auto mp3_buffer = new unsigned char [bufferSize];
67 size_t readBufferSize = 0;
68 while ((readBufferSize = fread(buffer, 2, bufferSize / 2, pcmFile)) > 0){
69 for(int i = 0; i < readBufferSize; i++){
70 if(i % 2 == 0){
71 leftBuffer[i/2] = buffer[i];
72 }else{
73 rightBuffer[i/2] = buffer[i];
74 }
75 }
76 size_t wroteSize = lame_encode_buffer(
77 lameClient,
78 leftBuffer,
79 rightBuffer,
80 (int)readBufferSize/2,
81 mp3_buffer,
82 bufferSize);
83 fwrite(mp3_buffer, 1, wroteSize, mp3File);
84 }
85 delete[] buffer;
86 delete [] leftBuffer;
87 delete [] rightBuffer;
88 delete [] mp3_buffer;
89}
90
91void Mp3Encoder::Destroy() {
92 if(pcmFile) {
93 fclose(pcmFile);
94 }
95
96 if(mp3File) {
97 fclose(mp3File);
98 lame_close(lameClient);
99 }
100}
101
102
103
104/*
105 * Class: com_tal_mp3encoder_Mp3Encoder
106 * Method: init
107 * Signature: (Ljava/lang/String;IIILjava/lang/String;)I
108 */
109extern "C" JNIEXPORT jint JNICALL
110Java_com_tal_mp3encode_Mp3Encoder_init
111 (JNIEnv* env, jobject, jstring pcmPathParam,
112 jint audioChannels, jint bitRate, jint sampleRate,
113 jstring mp3PathParam){
114 LOGI("mp3Encoder开始执行初始化");
115 const char* pcmPath = env->GetStringUTFChars(pcmPathParam, NULL);
116 const char* mp3Path = env->GetStringUTFChars(mp3PathParam, NULL);
117 return mp3Encoder.Init(pcmPath, mp3Path, sampleRate, audioChannels, bitRate);
118}
119
120/*
121 * Class: com_tal_mp3encoder_Mp3Encoder
122 * Method: destroy
123 * Signature: ()V
124 */
125extern "C" JNIEXPORT void JNICALL
126Java_com_tal_mp3encode_Mp3Encoder_destroy
127 (JNIEnv *, jobject){
128 mp3Encoder.Destroy();
129}
编写CMakeLists
CMakeLists.txt
1# CMake最小版本
2cmake_minimum_required(VERSION 3.18.1)
3
4# 工程名称
5project("mp3encode")
6
7# 定义目标库
8add_library(
9 mp3encode # 库名称
10 SHARED # 动态库
11 Mp3Encoder.cpp # 源文件
12)
13
14# 包含头文件
15target_include_directories(
16 mp3encode # 库名称
17 PUBLIC # 头文件作用域
18 include/Mp3Encoder.h include/lame.h # 具体哪些头文件
19)
20
21# 查找预编译库
22find_library(
23 log-lib
24 log # 日志库
25 ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}/liblame.so # lame库
26)
27
28# 目标库的链接
29target_link_libraries(
30 mp3encode # 库名称
31 ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}/liblame.so # lame库
32 ${log-lib} # 日志库
33)
34
35# 查看引入的lame库的路径
36message("LibPath -> " ${CMAKE_SOURCE_DIR}/lib/${ANDROID_ABI}/liblame.so)
配置Gradle
build.gradle(app)
1plugins {
2 id 'com.android.application'
3}
4
5android {
6 compileSdkVersion 30
7 buildToolsVersion "30.0.3"
8
9 defaultConfig {
10 applicationId "com.xxx.mp3encode"
11 minSdkVersion 22
12 targetSdkVersion 30
13 versionCode 1
14 versionName "1.0"
15
16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17
18 // 设置cmake编译脚本
19 externalNativeBuild {
20 cmake {
21 cppFlags '-std=c++11'
22 }
23 }
24
25 ndk {
26 abiFilters "armeabi-v7a"
27 }
28 }
29
30 buildTypes {
31 release {
32 minifyEnabled false
33 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
34 }
35 }
36 compileOptions {
37 sourceCompatibility JavaVersion.VERSION_1_8
38 targetCompatibility JavaVersion.VERSION_1_8
39 }
40 externalNativeBuild {
41 cmake {
42 path file('src/main/cpp/CMakeLists.txt')
43 version '3.18.1'
44 }
45 }
46 buildFeatures {
47 viewBinding true
48 }
49 ndkVersion '20.0.5594570'
50 sourceSets {
51 main {
52 jniLibs.srcDirs = ['src/main/cpp/lib']
53 }
54 }
55}
56
57dependencies {
58 implementation 'androidx.appcompat:appcompat:1.3.0'
59 implementation 'com.google.android.material:material:1.4.0'
60 implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
61 testImplementation 'junit:junit:4.13.2'
62 androidTestImplementation 'androidx.test.ext:junit:1.1.3'
63 androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
64}
编写上层代码
MainActivity.java
1public class MainActivity extends AppCompatActivity {
2
3 private static final String TAG = "MainActivity";
4 // 申请权限请求码
5 private static final int REQUEST_EXTERNAL_STORAGE = 1001;
6
7 static {
8 System.loadLibrary("lame");
9 System.loadLibrary("mp3encode");
10 }
11
12 @Override
13 protected void onCreate(Bundle savedInstanceState) {
14 super.onCreate(savedInstanceState);
15 setContentView(R.layout.activity_main);
16 verifyStoragePermissions(this);
17 }
18
19 public void startEncodeMp3(View view) {
20 File extDir = Environment.getExternalStorageDirectory();
21 File pcmCaseFile = new File(extDir, "testcase.pcm");
22 if(!pcmCaseFile.exists()) {
23 Log.i(TAG, "PCM文件不存在");
24 return;
25 }
26 File mp3File = new File(extDir, "testcase.mp3");
27 Mp3Encoder mp3Encoder = new Mp3Encoder();
28 mp3Encoder.init(
29 pcmCaseFile.getAbsolutePath(),
30 2,
31 5000,
32 44100,
33 mp3File.getAbsolutePath()
34 );
35
36 mp3Encoder.encode();
37 mp3Encoder.destroy();
38 Toast.makeText(this, "编码完成", Toast.LENGTH_SHORT).show();
39 TextView sampleTextView = (TextView) findViewById(R.id.sample_text);
40 sampleTextView.setText("Mp3 Path = " + mp3File.getAbsolutePath());
41 }
42
43 public static void verifyStoragePermissions(Activity activity) {
44 int writePermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);
45 int readPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE);
46
47 if (writePermission != PackageManager.PERMISSION_GRANTED
48 || readPermission != PackageManager.PERMISSION_GRANTED) {
49 // 如果没有权限需要动态地去申请权限
50 ActivityCompat.requestPermissions(
51 activity,
52 // 权限数组
53 new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
54 // 权限请求码
55 REQUEST_EXTERNAL_STORAGE
56 );
57 }
58 }
59}
AndroidManifest.xml
1<?xml version="1.0" encoding="utf-8"?>
2<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3 package="com.xxx.mp3encode">
4
5 <!-- 在SDCard中创建与删除文件的权限 -->
6 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
7 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
8
9 <application
10 android:requestLegacyExternalStorage="true"
11 android:allowBackup="true"
12 android:icon="@mipmap/ic_launcher"
13 android:label="@string/app_name"
14 android:roundIcon="@mipmap/ic_launcher_round"
15 android:supportsRtl="true"
16 android:theme="@style/Theme.Mp3Encode">
17 <activity android:name=".MainActivity">
18 <intent-filter>
19 <action android:name="android.intent.action.MAIN" />
20 <category android:name="android.intent.category.LAUNCHER" />
21 </intent-filter>
22 </activity>
23 </application>
24
25</manifest>
OK,点击按钮触发编码Mp3,Success!