音视频开发进阶指南(第二章)
使用libmp3lame开源库,编码PCM数据为MP3,结尾有源码,分支为chapter_2.x
一、新建NDK工程
网上一大堆。
二、下载LAME源码
lame源码地址lame官网
下载后解压,本demo中中使用的是3.100版本
三、拷贝libmp3lame源码到工程
在项目cpp目录下新建libmp3lame目录,用来存放下载的源码。
找到解压 的libmp3lame文件夹,将里面的.c和.h文件全部复制到项目的cpp/libmp3lame目录中。 libmp3lame文件夹内还包含其他文件夹,例如vector和i386是不需要的,可以忽略,然后,再找到解压的include文件夹,将lame.h文件拷贝到cpp/libmp3lame目录中,一共拷贝43个文件。
移植过来的代码要做一些修改才能编译通过:
1)删除fft.c文件的47行的”include “vector/lame_intrin.h”“ 2)修改set_get.h文件的24行的#include“lame.h” 3)将util.h文件的574行的”extern ieee754_float32_tfast_log2(ieee754_float32_t x);” 替换为 “extern float fast_log2(float x);”
四、编写编码工具类
mp3_encoder.h
#include <stdio.h> #include "jni.h" #include "../libmp3lame/lame.h" 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 Destory(); };
mp3_encoder.cpp
// // Created by bian on 2019/10/10. // #include "stdio.h" #include <android/log.h> #include <mp3_encoder.h> #define LOG_TAG "Mp3Encorder" #define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,FORMAT,##__VA_ARGS__); #define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,FORMAT,##__VA_ARGS__); int Mp3Encoder::Init(const char *pcmFilePath, const char *mp3FilePath, int sampleRate, int channels, int bitRate) { int ret = -1; pcmFile = fopen(pcmFilePath, "rb");//rb-以二进制读取模式打开文件 if (pcmFile) { mp3File = fopen(mp3FilePath, "wb");//wb-以二进制写入模式打开文件 if (mp3File) { lameClient = lame_init(); lame_set_in_samplerate(lameClient, sampleRate);//输入采样率 lame_set_out_samplerate(lameClient, sampleRate);//输出采样率 lame_set_num_channels(lameClient, channels);//声道个数 lame_set_brate(lameClient, bitRate / 1000);//比特率 lame_init_params(lameClient); ret = 0; LOGI("set lameClient params done!") } else { LOGE("open mp3File failed! path=%s", mp3FilePath); } } else { LOGE("open pcmFile failed! path=%s", pcmFilePath); } return ret; } void Mp3Encoder::Encode() { int channels = lame_get_num_channels(lameClient); LOGI("通道个数为:%d", channels); int bufferSize = 1024 * 256;//缓冲区大小 short *buffer = new short[bufferSize / channels];//读取pcmFile的缓冲区 //双声道情况时,为左右声道分配缓冲区 short *leftBuffer = new short[bufferSize / 4]; //左声道缓冲区 short *rightBuffer = new short[bufferSize / 4];//右声道缓冲区 unsigned char *mp3_buffer = new unsigned char[bufferSize]; size_t readBufferSize = 0; int frameSize = sizeof(short int) * channels;//一个帧的字节数 LOGI("frameSize=%d", frameSize); while ((readBufferSize = fread(buffer, frameSize, bufferSize / channels, pcmFile)) > 0) { if (channels == 1) { LOGI("readBufferSize=%d", readBufferSize); //对pcm数据进行编码,单声道情况,右声道为空 size_t wroteSize = lame_encode_buffer(lameClient, (short int *) buffer,//左声道 NULL,//右声道 (int) (readBufferSize), mp3_buffer,//编译后的数据 bufferSize); LOGI("wroteSize=%d", wroteSize); fwrite(mp3_buffer, 1, wroteSize, mp3File);//写入编码后的数据到mp3File } else if (channels == 2) { for (int i = 0; i < readBufferSize; i++) { if (i % 2 == 0) { leftBuffer[i / 2] = buffer[i]; } else { rightBuffer[i / 2] = buffer[i]; } } //对pcm数据进行编码 size_t wroteSize = lame_encode_buffer(lameClient, (short int *) leftBuffer,//左声道 (short int *) rightBuffer,//右声道 (int) (readBufferSize / 2), mp3_buffer,//编译后的数据 bufferSize); fwrite(mp3_buffer, 1, wroteSize, mp3File);//写入编码后的数据到mp3File } } delete[] buffer; delete[] leftBuffer; delete[] rightBuffer; delete[] mp3_buffer; } void Mp3Encoder::Destory() { if (pcmFile) { fclose(pcmFile); } if (mp3File) { fclose(mp3File); lame_close(lameClient); } } Mp3Encoder::Mp3Encoder() { } Mp3Encoder::~Mp3Encoder() { }
五、在java中调用
新建类Mp3Encoder.java
public class Mp3Encoder { public native int init(String pcmPath, int audioChannels, int bitRate, int sampleRate, String mp3Path); public native void encode(); public native void destroy(); }
生成头文件
在Mp3Encoder右键,如下图
如果你没有,请点击File->Settings->Tools->External Tools,点击+号,按照下图照抄即可
最后是JNI开发
Mp3Encoder.cpp
Mp3Encoder *encoder; JNIEXPORT jint JNICALL Java_com_flyscale_mp3encoder_Mp3Encoder_init (JNIEnv *env, jobject jclazz, jstring pcmFileParam, jint channels, jint bitRate, jint sampleRate, jstring mp3FileParam) { int ret = -1; const char *pcmPath = env->GetStringUTFChars(pcmFileParam, NULL); const char *mp3Path = env->GetStringUTFChars(mp3FileParam, NULL); encoder = new Mp3Encoder(); ret = encoder->Init(pcmPath, mp3Path, sampleRate, channels, bitRate); env->ReleaseStringUTFChars(mp3FileParam, mp3Path); env->ReleaseStringUTFChars(pcmFileParam, pcmPath); return ret; } JNIEXPORT void JNICALL Java_com_flyscale_mp3encoder_Mp3Encoder_encode(JNIEnv *env, jobject jclazz) { LOGI("encoder encode"); encoder->Encode(); } JNIEXPORT void JNICALL Java_com_flyscale_mp3encoder_Mp3Encoder_destroy (JNIEnv *env, jobject jclazz) { encoder->Destory(); }
六、编写CMakeLists.txt
依赖lame库的方式有几种,我直接把lame的源码与我自己的源码放在一起进行编译。
#指定使用的cmake最低版本号 cmake_minimum_required(VERSION 3.4.1) #指定工程名,非必须 project(mp3encoder) #指定头文件路径 include_directories(src/main/cpp/include/) set(SRC_FILES src/main/cpp/Mp3Encoder.cpp src/main/cpp/mp3_encoder.cpp src/main/cpp/libmp3lame/bitstream.c src/main/cpp/libmp3lame/encoder.c src/main/cpp/libmp3lame/fft.c src/main/cpp/libmp3lame/gain_analysis.c src/main/cpp/libmp3lame/id3tag.c src/main/cpp/libmp3lame/lame.c src/main/cpp/libmp3lame/mpglib_interface.c src/main/cpp/libmp3lame/newmdct.c src/main/cpp/libmp3lame/presets.c src/main/cpp/libmp3lame/psymodel.c src/main/cpp/libmp3lame/quantize_pvt.c src/main/cpp/libmp3lame/quantize.c src/main/cpp/libmp3lame/reservoir.c src/main/cpp/libmp3lame/set_get.c src/main/cpp/libmp3lame/tables.c src/main/cpp/libmp3lame/takehiro.c src/main/cpp/libmp3lame/util.c src/main/cpp/libmp3lame/vbrquantize.c src/main/cpp/libmp3lame/VbrTag.c src/main/cpp/libmp3lame/version.c ) #编译为共享库,名称audioencoder add_library( audioencoder # Sets the library as a shared library. SHARED #源文件路径 ${SRC_FILES}) #在默认路径下查找log库,并保存在变量log-lib中 find_library(log-lib log) # 链接库 target_link_libraries( # Specifies the target library. audioencoder # 将Log-lib变量代表的库,链接到audioencoder库 ${log-lib})
七、PCM源与参数问题
pcm音频源文件分单通道和双通道,采样率,比特率,所以要转码时要注意根据PCM源文件的情况进行区分,是进行单通道编码还是双通道编码。例子中也有体现。否则可以会出现声音变慢,变快,有杂音,有空白等情况。