因为官方的网站已经没法访问,能够到这里下载github.com/zly394/Silk…java
下载后解压,目录结构以下:linux
根据不一样的 CPU 分了不一样文件夹,我这里使用的是 SILK_SDK_SRC_ARM_v1.0.9。android
省略 ndk 环境配置过程git
进入 SILK_SDK_SRC_ARM_v1.0.9 目录github
在该目录下建立配置脚本:bash
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。架构
给 build.sh 赋予可执行权限:app
chmod +x build.sh复制代码
而后运行编译脚本进行编译:ide
./build.sh复制代码
编译完成后会在当前目录生成静态库 libSKP_SILK_SDK.a。
建立支持 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 目录下:
在 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 引入到项目中了。
在 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);
}复制代码
查看运行结果
解码 silk 格式的音频的步骤以下:
打开输入文件
验证文件 header
读取有效数据大小
读取有效数据,调用 SKP_Silk_SDK_Decode() 方法解码
处理解码出来的 PCM 数据,保存为 PCM 文件
#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" 的文件头。