Android 多媒體之 Silk 格式音訊解碼

zly394發表於2019-03-04

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")] = ` `;
        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…

相關文章