Android使用lame将PCM文件转码为MP3文件
1. 软件版本
Android Studio
版本:4.2
lame
版本:3.100
编译参考:centos7使用ndk编译lame
2. 项目配置
2.1 新建Native C++项目
2.2 引入lame库文件和头文件
头文件以及静态库位置:
2.2 编写build.gradle
文件
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.icebear.testappwithffmpeg"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
// 1. 添加参数
cppFlags "-frtti -fexceptions"
// 2. 设置支持的架构 "arm64-v8a", "armeabi-v7a", "x86_64", "x86"
abiFilters "armeabi-v7a"
}
}
// 3. 设置支持的架构 "arm64-v8a", "armeabi-v7a", "x86_64", "x86"
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.10.2'
}
}
buildFeatures {
viewBinding true
}
// 4. 设置ndk版本,可以不添加
// ndkVersion "21.0.6113669"
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
2.3 编写CmakeLists.txt
文件
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)
# Declares and names the project.
project("testappwithlame")
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
# 设置文件位置的变量
file(GLOB SOURCE
${CMAKE_SOURCE_DIR}/*.cpp
${CMAKE_SOURCE_DIR}/*.h
)
# 导入编译时的头文件
include_directories(${CMAKE_SOURCE_DIR}/include)
# 设置链接库目录
set(my_lib_path ${CMAKE_SOURCE_DIR}/libs/${CMAKE_ANDROID_ARCH_ABI})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${my_lib_path}")
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
${SOURCE} )
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
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 )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib
mp3lame
# Links the target library to the log library
# included in the NDK.
${log-lib} )
3. 文件编写
3.1 新建Mp3Encoder.h
和Mp3Encoder.cpp
Mp3Encoder.h
#ifndef TESTAPPWITHLAME_MP3ENCODER_H
#define TESTAPPWITHLAME_MP3ENCODER_H
#include "include/lame/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 Destroy();
};
#endif //TESTAPPWITHLAME_MP3ENCODER_H
Mp3Encoder.cpp
#include "Mp3Encoder.h"
Mp3Encoder::Mp3Encoder() {
}
Mp3Encoder::~Mp3Encoder() {
}
int Mp3Encoder::Init(const char *pcmFilePath, const char *mp3FilePath, int sampleRate, int channels,
int bitRate) {
int ret = -1;
pcmFile = fopen(pcmFilePath, "rb");
if (pcmFile) {
mp3File = fopen(mp3FilePath, "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;
}
}
return ret;
}
void Mp3Encoder::Encode() {
int bufferSize = 1024 * 256;
short *buffer = new short[bufferSize / 2];
short *leftBuffer = new short[bufferSize / 4];
short *rightBuffer = new short[bufferSize / 4];
unsigned char *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 writeSize = lame_encode_buffer(lameClient, (short int *) leftBuffer,
(short int *) rightBuffer, (int) (readBufferSize / 2),
mp3_buffer, bufferSize);
fwrite(mp3_buffer, 1, writeSize, mp3File);
}
delete[] buffer;
delete[] leftBuffer;
delete[] rightBuffer;
delete[] mp3_buffer;
}
void Mp3Encoder::Destroy() {
if (pcmFile) {
fclose(pcmFile);
}
if (mp3File) {
fclose(mp3File);
lame_close(lameClient);
}
}
3.2 新建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();
}
3.3 修改native-lib.cpp
#include <jni.h>
#include <string>
#include "Mp3Encoder.h"
extern "C" JNIEXPORT jstring JNICALL
Java_com_icebear_testappwithlame_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
// 定义全局Mp3Encoder
Mp3Encoder *encoder = NULL;
extern "C"
JNIEXPORT jint JNICALL
Java_com_icebear_testappwithlame_lame_Mp3Encoder_init(JNIEnv *env, jobject thiz, jstring pcm_path,
jint audio_channels, jint bit_rate,
jint sample_rate, jstring mp3_path) {
const char* pcmPath = env->GetStringUTFChars(pcm_path, NULL);
const char* mp3Path = env->GetStringUTFChars(mp3_path, NULL);
encoder = new Mp3Encoder();
encoder->Init(pcmPath, mp3Path, sample_rate, audio_channels, bit_rate);
env->ReleaseStringUTFChars(pcm_path, pcmPath);
env->ReleaseStringUTFChars(mp3_path, mp3Path);
return 0;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_icebear_testappwithlame_lame_Mp3Encoder_encode(JNIEnv *env, jobject thiz) {
// TODO: implement encode()
encoder->Encode();
}
extern "C"
JNIEXPORT void JNICALL
Java_com_icebear_testappwithlame_lame_Mp3Encoder_destroy(JNIEnv *env, jobject thiz) {
// TODO: implement destroy()
encoder->Destroy();
}
3.4 修改activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<TextView
android:id="@+id/sample_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/edit_pcm_file"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<EditText
android:id="@+id/edit_mp3_file"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<Button
android:id="@+id/btn_start"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开始"
/>
</LinearLayout>
3.5 修改MainActivity.java
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
private ActivityMainBinding binding;
// 权限设置
private int requestPermissionCode = 10000;
private String[] requestPermission = new String[]{
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
};
// 全局
private Context mContext;
private Mp3Encoder mp3Encoder;
// 控件
private TextView infoText;
private EditText pcmFileEdit;
private EditText mp3FileEdit;
private Button startBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
requestPermission();
initData();
initView();
}
/**
* 获取写文件权限
*/
public void requestPermission() {
if (Build.VERSION.SDK_INT >= 30) {
if (!Environment.isExternalStorageManager()) {
Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
startActivity(intent);
return;
}
} else {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
if (PermissionChecker.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PermissionChecker.PERMISSION_GRANTED) {
requestPermissions(requestPermission, requestPermissionCode);
}
}
}
}
public void initData() {
mContext = getBaseContext();
mp3Encoder = new Mp3Encoder();
infoText = findViewById(R.id.sample_text);
pcmFileEdit = findViewById(R.id.edit_pcm_file);
mp3FileEdit = findViewById(R.id.edit_mp3_file);
startBtn = findViewById(R.id.btn_start);
}
public void initView() {
pcmFileEdit.setText("/sdcard/Download/gg.pcm");
mp3FileEdit.setText("/sdcard/Download/gg.mp3");
startBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String pcmFileName = pcmFileEdit.getText().toString();
String mp3FileName = mp3FileEdit.getText().toString();
// 参数使用ffprobe查看
int ret = mp3Encoder.init(pcmFileName, 2, 1411000, 44100, mp3FileName);
if (ret == 0) {
mp3Encoder.encode();
mp3Encoder.destroy();
infoText.setText("ok");
} else {
infoText.setText("error");
}
}
});
}
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
4. 结果
5. 问题
5.1 点击按钮后闪退
原因:C文件中的函数没有写返回值
参考:jni开发 android报错F libc : Fatal signal 4 (SIGILL), code 1 (ILL_ILLOPC), fault addr 0xc0ea1b10 in
5.2 调试时无法命中native方法所在断点
参考:AndroidStudio 调试 Native 代码,无法命中断点
原因::
由于使用的Android
模拟器CPU
是x86
的,但编译的架构只有armeabi-v7a
,导致手机上运行的静态库架构和手机的 CPU
架构不符,虽然代码可以运行,但是不能进行断点调试。
解决方法:
添加x86的静态库,并修改build.gradle文件
plugins {
id 'com.android.application'
}
android {
compileSdkVersion 30
buildToolsVersion "30.0.3"
defaultConfig {
applicationId "com.icebear.testappwithlame"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
// 1. 添加参数
cppFlags "-frtti -fexceptions"
// 2. 设置支持的架构 "arm64-v8a", "armeabi-v7a", "x86_64", "x86"
abiFilters "armeabi-v7a", "x86"
}
}
// 3. 设置支持的架构 "arm64-v8a", "armeabi-v7a", "x86_64", "x86"
ndk {
abiFilters "armeabi-v7a", "x86"
}
}
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.10.2'
}
}
buildFeatures {
viewBinding true
}
// 4. 设置ndk版本,可以不添加
// ndkVersion "21.0.6113669"
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
重新编译后,可以对native方法进行调试。
5.3 点击开始按钮后程序退出
可能原因:
文件路径错误,没有找到对应的文件
解决方法:
修改路径,或者添加文件
6. 参考
6.1 使用FFmpeg
制作pcm
文件参考
6.2 针对报错的解决
- jni开发 android报错F libc : Fatal signal 4 (SIGILL), code 1 (ILL_ILLOPC), fault addr 0xc0ea1b10 in
- AndroidStudio 调试 Native 代码,无法命中断点