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 步:
-
使用 add_library( name SHARED IMPORTED ) 命令將庫新增進來。第一個引數為新增進來的庫指定名稱;SHARED 表示新增的是動態庫,如果是靜態庫則是 STATIC ;因為是預先構建的庫,使用 IMPORTED 標誌表示只將庫匯入到專案中。
-
使用 set_target_properties() 命令指定庫的路徑。庫的名稱,要和 add_library 中的一致;使用 ANDROID_ABI 路徑變數新增庫的多個 ABI 版本。
-
使用 include_directories() 命令指定標頭檔案的路徑。
-
使用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 格式的音訊的步驟如下:
-
開啟輸入檔案
-
驗證檔案 header
-
讀取有效資料大小
-
讀取有效資料,呼叫 SKP_Silk_SDK_Decode() 方法解碼
-
處理解碼出來的 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” 的檔案頭。