Android 多媒体之 Silk 格式音频解码

2,772 阅读5分钟

1 编译 Silk 源码

1.1 下载源码

由于官方的网站已经无法访问,可以到这里下载github.com/zly394/Silk…

下载后解压,目录结构如下:

根据不同的 CPU 分了不同文件夹,我这里使用的是 SILK_SDK_SRC_ARM_v1.0.9。

1.2 编写编译脚本

省略 ndk 环境配置过程

进入 SILK_SDK_SRC_ARM_v1.0.9 目录

在该目录下创建配置脚本:

build.sh

# ndk 目录根据你的安装目录
ANDROID_NDK=/Users/zhuleiyue/Library/Android/sdk/ndk-bundle
# 指定 CPU 架构
CPU=armeabi-v7a

# 最低支持的 Android 版本
ANDROID_API=android-18
# CPU 架构
ARCH=arch-arm
# 工具链版本
TOOLCHAIN_VERSION=4.9
# 指定工具链 CPU 架构
TOOLCHAIN_CPU=arm-linux-androideabi
# 指定编译工具 CPU 架构
CROSS_CPU=arm-linux-androideabi
# 优化参数
ADDED_CFLAGS="-fpic -pipe "

case $CPU in
armeabi-v7a)
    ARCH=arch-arm
    TOOLCHAIN_CPU=arm-linux-androideabi
    CROSS_CPU=arm-linux-androideabi
    TARGET_ARCH=armv7-a
    ADDED_CFLAGS+="-DNO_ASM"
    ;;
arm64-v8a)
    ARCH=arch-arm64
    ANDROID_API=android-21
    TOOLCHAIN_CPU=aarch64-linux-android
    CROSS_CPU=aarch64-linux-android
    TARGET_ARCH=armv8-a
    ADDED_CFLAGS+="-D__ARMEL__"
    ;;
*)
    echo "不支持的架构 $CPU";
    exit 1
    ;;
esac

# 设置编译针对的平台
# 最低支持的 android 版本,CPU 架构
SYSROOT=$ANDROID_NDK/platforms/$ANDROID_API/$ARCH
# 设置编译工具前缀
export TOOLCHAIN_PREFIX=$ANDROID_NDK/toolchains/$TOOLCHAIN_CPU-$TOOLCHAIN_VERSION/prebuilt/darwin-x86_64/bin/$CROSS_CPU-
# 设置编译工具后缀
export TOOLCHAIN_SUFFIX=" --sysroot=$SYSROOT"
# 设置 CPU 架构
export TARGET_ARCH
# 设置优化参数
export ADDED_CFLAGS

make clean all

对于 armeabi-v7a 的 CPU 架构需要设置 NO_ASM 来禁用 asm,对于 arm64-v8a 架构,需要设置 ARMEL 支持 big endian。

1.3 编译

给 build.sh 赋予可执行权限:

chmod +x build.sh

然后运行编译脚本进行编译:

./build.sh

编译完成后会在当前目录生成静态库 libSKP_SILK_SDK.a。

2 引入到 Android 项目

2.1 添加静态库和头文件

创建支持 C/C++ 的项目

在 app 的 build.gradle 文件中 defaultConfig 标签下添加如下配置:

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
        // 指定 ABI
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
    ...
}

在 app/src/main 目录下新建 jniLibs 文件夹,在 jniLibs 根据支持的 CPU 架构新建 armeabi-v7a 和 arm64-v8a 文件夹。将编译好的不同 CPU 架构的 libSKP_SILK_SDK.a 静态库文件分别添加进去。如下所示:

将 SILK_SDK_SRC_ARM_v1.0.9 目录下的 interface 文件夹添加到 app/src/cpp 目录下:

2.2 配置 CMakelists.txt

在 CMakelist.txt 文件中添加如下配置:

...

# 添加库到项目中
# STATIC 表示为静态库文件
# 因为库已经预先构建,您需要使用 IMPORTED 标志告知 CMake 只希望将库导入到项目中

add_library( silk
             STATIC
             IMPORTED )

# 使用 set_target_properties() 命令指定库的路径
# 要向 CMake 构建脚本中添加库的多个 ABI 版本,而不必为库的每个版本编写多个命令,可以使用 ANDROID_ABI 路径变量。

set_target_properties( silk
                       PROPERTIES IMPORTED_LOCATION
                       ${CMAKE_SOURCE_DIR}/src/main/jniLibs/${ANDROID_ABI}/libSKP_SILK_SDK.a )

# 指定头文件路径
include_directories( src/main/cpp/interface )

...

# 将预构建库关联到自己的原生库

target_link_libraries( # Specifies the target library.
                       native-lib
                       silk

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

在项目中添加预构建库需要以下 4 步:

  1. 使用 add_library( name SHARED IMPORTED ) 命令将库添加进来。第一个参数为添加进来的库指定名称;SHARED 表示添加的是动态库,如果是静态库则是 STATIC ;因为是预先构建的库,使用 IMPORTED 标志表示只将库导入到项目中。

  2. 使用 set_target_properties() 命令指定库的路径。库的名称,要和 add_library 中的一致;使用 ANDROID_ABI 路径变量添加库的多个 ABI 版本。

  3. 使用 include_directories() 命令指定头文件的路径。

  4. 使用target_link_libraries() 将预构建库关联到自己的原生库

配置好 CMakeLists.txt 后同步代码。

这样就把 libSKP_SILK_SDK.a 引入到项目中了。

2.3 测试

在 Activity 中添加测试代码,如下所示:

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("native-lib");
    }

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

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(getSilkVersion());
    }

    /**
     * 获取 Silk_SDK 的版本号
     */
    public native String getSilkVersion();
}

在 native-lib.cpp 中实现 native 方法:

#include <jni.h>
#include <string>

extern "C" {
#include <SKP_Silk_SDK_API.h>
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_zly_silkdecoder_MainActivity_getSilkVersion(JNIEnv *env, jobject instance) {
    const char *version = SKP_Silk_SDK_get_version();
    return env->NewStringUTF(version);
}

查看运行结果

3 解码并保存为 PCM 文件

解码 silk 格式的音频的步骤如下:

  1. 打开输入文件

  2. 验证文件 header

  3. 读取有效数据大小

  4. 读取有效数据,调用 SKP_Silk_SDK_Decode() 方法解码

  5. 处理解码出来的 PCM 数据,保存为 PCM 文件

3.1 编写 JNI 方法

#define LOG_I(TAG, ...)    __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOG_E(TAG, ...)    __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)

#define TAG "SILK"
#define ERROR_BAD_VALUE -2

#define MAX_BYTES_PER_FRAME     1024
#define MAX_INPUT_FRAMES        5
#define FRAME_LENGTH_MS         20
#define MAX_API_FS_KHZ          48

unsigned long GetHighResolutionTime() /* O: time in usec*/
{
    struct timeval tv;
    gettimeofday(&tv, 0);
    return (unsigned long) ((tv.tv_sec * 1000000) + (tv.tv_usec));
}

JNIEXPORT jstring JNICALL
Java_com_zly_silkdecoder_SilkDecoder_nativeTranscode2PCM(JNIEnv *env, jclass type,
                                                         jstring inputPath_, jint sampleRate,
                                                         jstring outputPath_) {
    const char *inputPath = (*env)->GetStringUTFChars(env, inputPath_, 0);
    const char *outputPath = (*env)->GetStringUTFChars(env, outputPath_, 0);

    unsigned long totTime, startTime;
    double fileLength;
    size_t counter;
    SKP_int32 ret, tot_len, totPackets;
    SKP_int32 decSizeBytes, frames, packetSize_ms = 0;
    SKP_int16 nBytes, len;
    SKP_uint8 payload[MAX_BYTES_PER_FRAME * MAX_INPUT_FRAMES], *payloadToDec = NULL;
    SKP_int16 out[((FRAME_LENGTH_MS * MAX_API_FS_KHZ) << 1) * MAX_INPUT_FRAMES], *outPtr;
    void *psDec;
    FILE *inFile, *outFile;
    SKP_SILK_SDK_DecControlStruct DecControl;

    LOG_I(TAG, "********** Silk Decoder (Fixed Point) v %s ********************",
          SKP_Silk_SDK_get_version());
    LOG_I(TAG, "********** Compiled for %d bit cpu *******************************",
          (int) sizeof(void *) * 8);
    LOG_I(TAG, "Input:                       %s", inputPath);
    LOG_I(TAG, "Output:                      %s", outputPath);

    // 打开输入文件
    inFile = fopen(inputPath, "rb");
    if (inFile == NULL) {
        LOG_E(TAG, "Error: could not open input file %s", inputPath);
        return NULL;
    }

    // 验证文件头
    {
        char header_buf[50];
        fread(header_buf, sizeof(char), strlen("#!SILK_V3"), inFile);
        header_buf[strlen("#!SILK_V3")] = '\0';
        if (strcmp(header_buf, "#!SILK_V3") != 0) {
            LOG_E(TAG, "Error: Wrong Header %s", header_buf);
            return NULL;
        }
        LOG_I(TAG, "Header is \"%s\"", header_buf);
    }

    // 打开输出文件
    outFile = fopen(outputPath, "wb");
    if (outFile == NULL) {
        LOG_E(TAG, "Error: could not open output file %s", outputPath);
        return NULL;
    }

    // 设置采样率
    if (sampleRate == 0) {
        DecControl.API_sampleRate = 24000;
    } else {
        DecControl.API_sampleRate = sampleRate;
    }

    // 获取 Silk 解码器状态的字节大小
    ret = SKP_Silk_SDK_Get_Decoder_Size(&decSizeBytes);
    if (ret) {
        LOG_E(TAG, "SKP_Silk_SDK_Get_Decoder_Size returned %d", ret);
    }

    psDec = malloc((size_t) decSizeBytes);

    // 初始化或充值解码器
    ret = SKP_Silk_SDK_InitDecoder(psDec);
    if (ret) {
        LOG_E(TAG, "SKP_Silk_SDK_InitDecoder returned %d", ret);
    }

    totPackets = 0;
    totTime = 0;

    while (1) {
        // 读取有效数据大小
        counter = fread(&nBytes, sizeof(SKP_int16), 1, inFile);
        if (nBytes < 0 || counter < 1) {
            break;
        }
        // 读取有效数据
        counter = fread(payload, sizeof(SKP_uint8), (size_t) nBytes, inFile);
        if ((SKP_int16) counter < nBytes) {
            break;
        }

        payloadToDec = payload;

        outPtr = out;
        tot_len = 0;
        startTime = GetHighResolutionTime();

        frames = 0;
        do {
            // 解码
            ret = SKP_Silk_SDK_Decode(psDec, &DecControl, 0, payloadToDec, nBytes, outPtr, &len);
            if (ret) {
                LOG_E(TAG, "SKP_Silk_SDK_Decode returned %d", ret);
            }

            frames++;
            outPtr += len;
            tot_len += len;
            if (frames > MAX_INPUT_FRAMES) {
                outPtr = out;
                tot_len = 0;
                frames = 0;
            }
        } while (DecControl.moreInternalDecoderFrames);

        packetSize_ms = tot_len / (DecControl.API_sampleRate / 1000);
        totTime += GetHighResolutionTime() - startTime;
        totPackets++;
        // 将解码后的数据保存到文件
        fwrite(out, sizeof(SKP_int16), (size_t) tot_len, outFile);
    }

    LOG_I(TAG, "Packets decoded:             %d", totPackets);
    LOG_I(TAG, "Decoding Finished");

    free(psDec);

    fclose(outFile);
    fclose(inFile);

    fileLength = totPackets * 1e-3 * packetSize_ms;

    LOG_I(TAG, "File length:                 %.3f s", fileLength);
    LOG_I(TAG, "Time for decoding:           %.3f s (%.3f%% of realTime)", 1e-6 * totTime,
          1e-4 * totTime / fileLength);

    (*env)->ReleaseStringUTFChars(env, inputPath_, inputPath);
    (*env)->ReleaseStringUTFChars(env, outputPath_, outputPath);

    return (*env)->NewStringUTF(env, outputPath);
}

在解码前需要验证文件头是否为 "#!SILK_V3",但是如果是微信里的语音的话,需要把文件的第一个字节去掉,然后才是 "#!SILK_V3" 的文件头。

项目地址:github.com/zly394/Silk…