直接使用 JNI 调用实现微信语音编解码

微信语音编解码实现

配套源码

WeChatVoiceCodec

1.文前

       截至目前,我们已经可以实现微信语音在 android 平台的格式转换,如下:

wechat-voice <--silk--> pcm <--lame--> mp3

       不足之处在于,当前只能通过 shell 命令进行调用,而且如果要实现微信语音和 mp3 格式互转,必须执行两次不同的命令。

       基于此,本篇文章将在之前的基础上,直接将 silk 和 lame 库整合成一个 so 库,实现直接使用 JNI 调用完成微信语音的编解码。在实现过程,力争实现以下目标:

2.整合 so 库

2.1 新建 libwcvcodec (LibWeChatVoiceCodec) 库模块

android {
    ……
    defaultConfig {
        ……
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++11 -fexceptions -pthread"
                // 要支持 'armeabi-v7a' 需开启 NO_ASM 宏
                cFlags "-DSTDC_HEADERS -DHAVE_LIMITS_H -DHAVE_MPGLIB -DNO_ASM"
            }
            ndk {
                abiFilters 'armeabi-v7a', 'arm64-v8a'
            }
        }
    }
}
android {
    ……
    externalNativeBuild {
        cmake {
            path "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.6)
project(wcvcodec)

# silk
include_directories(src/main/cpp/silk/interface)
include_directories(src/main/cpp/silk/src)

aux_source_directory(src/main/cpp/silk/src SILK_SRC)


# lame
include_directories(src/main/cpp/lame/frontend)
include_directories(src/main/cpp/lame/include)
include_directories(src/main/cpp/lame/libmp3lame)
include_directories(src/main/cpp/lame/mpglib)

aux_source_directory(src/main/cpp/lame/frontend LAME_FRONTEND_SRC)
aux_source_directory(src/main/cpp/lame/libmp3lame LAME_SRC)
aux_source_directory(src/main/cpp/lame/mpglib LAME_MPGLIB_SRC)


# wcv codec (WeChat Voice Codec)
include_directories(src/main/cpp)
aux_source_directory(src/main/cpp/silk SILK_CODEC_SRC)
aux_source_directory(src/main/cpp/lame LAME_CODEC_SRC)
add_library(wcvcodec SHARED src/main/cpp/WcvCodec.c ${SILK_SRC} ${SILK_CODEC_SRC}
        ${LAME_FRONTEND_SRC} ${LAME_SRC} ${LAME_MPGLIB_SRC} ${LAME_CODEC_SRC})
find_library(android-log log)
target_link_libraries(wcvcodec ${android-log})

2.2 扩展 silk 编解码接口

/**
 * Silk decoder and encoder
 *
 * @author Reinhard(李剑波)
 * @date 2019/6/15
 */

#ifndef SILK_CODEC_H
#define SILK_CODEC_H

int silk_decoder_main(int argc, char *argv[]);

int silk_encoder_main(int argc, char *argv[]);

#endif //SILK_CODEC_H
/**
 * Android log macro definition
 *
 * @author Reinhard(李剑波)
 * @date 2019/6/15
 */

#ifndef ANDROID_LOG_H
#define ANDROID_LOG_H

#include <android/log.h>

#define LOG_TAG "WcvCodec"
#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 LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#endif //ANDROID_LOG_H

2.3 扩展 lame 编解码接口

/**
 * Lame decoder and encoder
 *
 * @author Reinhard(李剑波)
 * @date 2019/6/22
 */

#ifndef LAME_CODEC_H
#define LAME_CODEC_H

int lame_codec_main(int argc, char *argv[]);

#endif //LAME_CODEC_H

int lame_codec_main(int argc, char *argv[]) {
    return c_main(argc, argv);
}

2.4 创建 JNI 接口

package com.reinhard.wcvcodec;

/**
 * WeChat voice decoder and encoder
 *
 * @author Reinhard(李剑波)
 * @date 2019-06-22
 */
public class WcvCodec {
    static {
        System.loadLibrary("wcvcodec");
    }

    /**
     * decode amr to mp3 (1. amr -> pcm  2. pcm -> mp3)
     *
     * @param amrPath amr file path
     * @param pcmPath pcm file path
     * @param mp3Path mp3 file path
     * @return 0 if success, otherwise -1
     */
    public static native int decode(String amrPath, String pcmPath, String mp3Path);

    /**
     * encode pcm to amr
     *
     * @param pcmPath pcm file path
     * @param amrPath amr file path
     * @return 0 if success, otherwise -1
     */
    public static native int encode(String pcmPath, String amrPath);

    /**
     * encode mp3 to amr (1. mp3 -> pcm  2. pcm -> amr)
     *
     * @param mp3Path mp3 file path
     * @param pcmPath pcm file path
     * @param amrPath amr file path
     * @return 0 if success, otherwise -1
     */
    public static native int encode2(String mp3Path, String pcmPath, String amrPath);
}
javah com.reinhard.wcvcodec.WcvCodec
/**
 * WeChat voice decoder and encoder
 *
 * @author Reinhard(李剑波)
 * @date 2019/6/22
 */

#include <silk/SilkCodec.h>
#include <lame/LameCodec.h>
#include "WcvCodec.h"
#include "android_log.h"

/*
 * Class:     com_reinhard_wcvcodec_WcvCodec
 * Method:    decode
 * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_reinhard_wcvcodec_WcvCodec_decode
        (JNIEnv *env, jclass clazz, jstring amrPath, jstring pcmPath, jstring mp3Path) {
    const char *amr = (*env)->GetStringUTFChars(env, amrPath, JNI_FALSE);
    const char *pcm = (*env)->GetStringUTFChars(env, pcmPath, JNI_FALSE);
    const char *mp3 = (*env)->GetStringUTFChars(env, mp3Path, JNI_FALSE);
    int argc = 5;
    const char *argv[] = {"./Decoder", amr, pcm, "-stx_header", "-quiet"};
    if (silk_decoder_main(argc, (char **) argv) == JNI_OK) {
        int argc2 = 14;
        const char *argv2[] = {"./lame", "-q", "5", "-b", "128", "-m", "m", "-r",
                               "-s", "24000", "--resample", "24000", pcm, mp3};
        return lame_codec_main(argc2, (char **) argv2);
    } else {
        LOGE("silk_decoder_main failed!");
        return JNI_ERR;
    }
}

/*
 * Class:     com_reinhard_wcvcodec_WcvCodec
 * Method:    encode
 * Signature: (Ljava/lang/String;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_reinhard_wcvcodec_WcvCodec_encode
        (JNIEnv *env, jclass clazz, jstring pcmPath, jstring amrPath) {
    const char *pcm = (*env)->GetStringUTFChars(env, pcmPath, JNI_FALSE);
    const char *amr = (*env)->GetStringUTFChars(env, amrPath, JNI_FALSE);
    int argc = 7;
    const char *argv[] = {"./Encoder", pcm, amr, "-rate", "24000", "-stx_header", "-quiet"};
    return silk_encoder_main(argc, (char **) argv);
}

/*
 * Class:     com_reinhard_wcvcodec_WcvCodec
 * Method:    encode2
 * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I
 */
JNIEXPORT jint JNICALL Java_com_reinhard_wcvcodec_WcvCodec_encode2
        (JNIEnv *env, jclass clazz, jstring mp3Path, jstring pcmPath, jstring amrPath) {
    const char *mp3 = (*env)->GetStringUTFChars(env, mp3Path, JNI_FALSE);
    const char *pcm = (*env)->GetStringUTFChars(env, pcmPath, JNI_FALSE);
    const char *amr = (*env)->GetStringUTFChars(env, amrPath, JNI_FALSE);
    int argc = 5;
    const char *argv[] = {"./lame", "--decode", "-t", mp3, pcm};
    if (lame_codec_main(argc, (char **) argv) == JNI_OK) {
        int argc2 = 7;
        const char *argv2[] = {"./Encoder", pcm, amr, "-rate", "24000", "-stx_header", "-quiet"};
        return silk_encoder_main(argc2, (char **) argv2);
    } else {
        LOGE("lame_codec_main failed!");
        return JNI_ERR;
    }
}

3.测试

3.1 新建一个 app 模块

       AndroidStudio->File->New->New Module…->Phone And Tablet Module->创建带一个空界面的应用

3.2 添加 libwcvcodec 库模块依赖

       修改 app 模块的 build.gradle 文件,在 dependencies 项下添加如下内容:

dependencies {
    ……
    implementation project(path: ':libwcvcodec')
}

3.3 添加 sd 卡访问权限

       修改 app 模块的清单文件 AndroidManifest.xml,添加如下内容:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.reinhard.wechat.voicecodec">

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application ...>
        ...
    </application>

</manifest>

3.4 添加测试按钮

       修改 app 模块的布局文件 activity_main.xml,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_amr_to_mp3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="amr_to_mp3"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btn_pcm_to_amr"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:onClick="onClick"
        android:text="pcm_to_amr"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_amr_to_mp3" />

    <Button
        android:id="@+id/btn_mp3_to_amr"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:onClick="onClick"
        android:text="mp3_to_amr"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.5"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_pcm_to_amr" />

</android.support.constraint.ConstraintLayout>

3.5 编写测试代码

       修改 app 模块的 MainActivity,内容如下:

package com.reinhard.wechat.voicecodec;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import com.reinhard.wcvcodec.WcvCodec;

public class MainActivity extends Activity {
    private static final String TAG = "WcvCodec";
    private static final String TEST_DIR = "/sdcard/reinhard/";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void onClick(View view) {
        int id = view.getId();
        switch (id) {
            case R.id.btn_amr_to_mp3:
                testAmrToMp3();
                break;
            case R.id.btn_pcm_to_amr:
                testPcmToAmr();
                break;
            case R.id.btn_mp3_to_amr:
                testMp3ToAmr();
                break;
            default:
                break;
        }
    }

    private void testAmrToMp3() {
        Log.d(TAG, "testAmrToMp3");
        String amrPath = TEST_DIR + "in.amr";
        String pcmPath = TEST_DIR + "out.pcm";
        String mp3Path = TEST_DIR + "out.mp3";
        if (WcvCodec.decode(amrPath, pcmPath, mp3Path) == 0) {
            Toast.makeText(this, "testAmrToMp3 success", Toast.LENGTH_SHORT)
                    .show();
        }
    }

    private void testPcmToAmr() {
        Log.d(TAG, "testPcmToAmr");
        String pcmPath = TEST_DIR + "in.pcm";
        String amrPath = TEST_DIR + "out.amr";
        if (WcvCodec.encode(pcmPath, amrPath) == 0) {
            Toast.makeText(this, "testPcmToAmr success", Toast.LENGTH_SHORT)
                    .show();
        }
    }

    private void testMp3ToAmr() {
        Log.d(TAG, "testMp3ToAmr");
        String mp3Path = TEST_DIR + "in.mp3";
        String pcmPath = TEST_DIR + "out.pcm";
        String amrPath = TEST_DIR + "out.amr";
        if (WcvCodec.encode2(mp3Path, pcmPath, amrPath) == 0) {
            Toast.makeText(this, "testMp3ToAmr success", Toast.LENGTH_SHORT)
                    .show();
        }
    }
}

       注意:demo 只做简单测试,所以直接将操作放在 UI 线程,实际使用时,应放到其他线程。

3.6 将测试用的音频文件推送到手机

cd xxx/WeChatVoiceCodec/libwcvcodec/test_vectors/
adb shell mkdir -p /sdcard/reinhard/
adb push wechat_voice.amr /sdcard/reinhard/in.amr
adb push wechat_voice.mp3 /sdcard/reinhard/in.mp3
adb push wechat_voice.pcm /sdcard/reinhard/in.pcm

3.7 将编译生成的 apk 安装到手机,并给予 sd 卡访问权限

       说明:因为只是 demo,没有引入运行时权限申请,必要时可能需要手动到设置中配置。

3.8 测试编解码

       打开应用后,点击按钮进行测试,成功时会弹出吐司提示,并在 /sdcard/reinhard/ 文件夹生成对应的文件。按钮与测试功能对应关系如下:

       可以将生成的文件拉取到电脑进行验证:

4. 总结

5. 回顾与后续

       到此,本系列的文章基本完成了。

       在这些文章中,我尝试着去解答了为什么移植和怎么移植 silk 和 lame 库的问题,但还是存在一些疑惑和可以改进的点:

       对于疑惑点,可能需要进一步学习语音编解码相关的知识才能解答;至于待改进的点,就留待以后有时间了再实现吧。

6. 参考