编辑
2021-07-23
客户端技术
00
请注意,本文编写于 801 天前,最后修改于 113 天前,其中某些信息可能已经过时。

目录

LAME
MP3参数
编码Mp3基本流程
初始化编码参数
设置编码参数
初始化编码器器
编码PCM数据
销毁编码器
编码器参数
编译Mp3实践
准备PCM文件
编写JNI层代码
编写CMakeLists
配置Gradle
编写上层代码

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)

MPEG1MPEG2MPEG2.5
441002205011025
480002400012000
32000160008000

比特率支持(bit/s)

MPEG1MPEG2MPEG2.5
3288
401616
482424
563232
644040
804848
965656
1126464
12880
16096
192112
224128
256144
320160

编码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_bufferlame_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库会优先服从输出采样率的设置,根据采样率选择协议版本,然后在这个版本所能支持的比特率中选一个和设置比特率最接近的。

c
/* * 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

java
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

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,然后稍作修改:

cpp
#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

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最小版本 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)

groovy
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

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
<?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!

本文作者:Tim

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!