0.前言
SILK 编码最早在 Skype 中使用,它在编码效率和质量之间取得了很好的平衡,因此被广泛应用在互联网的音频相关产品中。SILK 的最新版本是 2012 年发布的 SDK 1.0.9,即SILK V3。
腾讯系产品,包括QQ、微信、小程序,在语音相关的实现中,也大量使用到 SILK 编码,不过他在标准的 SILK 文件头加了一个字节的 0x02 ,所以在解码的时候要多判断一次。虽然腾讯的音频文件格式后缀也可能叫 AMR,不过并不是真的 AMR格式,还是 SILK 格式的。
下图是一个微信 AMR 文件的文件头,以 0x02 开头,然后是九个固定字符: #!SILK_V3,接下来就是语音数据。
还要注意的是 SILK 编码的采样频率和码率是自适应的,参见 翻译的文档。
WAV 格式就相对方便点,可以直接存放 PCM 数据,将 SLIK 解码为 PCM 后加一个 WAV 头就成了一个 wav 音频文件( WAV 头的格式参考 WAV格式分析 )。
参考示例:SILK_SDK_SRC_v1.0.9\SILK_SDK_SRC_ARM_v1.0.9\test\Decoder.c
参考博客(转载):https://blog.csdn.net/weixin_34292924/article/details/87987436
参考博客:https://www.cnblogs.com/protosec/p/11673358.html
参考博客:https://blog.csdn.net/wanggp_2007/article/details/5536818
参考博客(翻译):https://blog.csdn.net/zhaosipei/article/details/7467810
别人fork的SDK源码:https://gitee.com/alvis/SILK_SDK_SRC
别人做的decoder:https://github.com/kn007/silk-v3-decoder
1.实现
下载好 SILK SDK 后,可以直接用 VS 打开工程文件进行编译为静态库,然后将 interface 文件夹和库文件导入到我们的应用工程中。也可以自己建一个 DLL 工程把那些原文件 copy 过去,然后导出 SKP_Silk_SDK_API.h 文件中的函数接口,生成动态或者静态库。
借助 SLIK 库,解码主要有两步,初始化解码器+循环解码为 PCM 数据。
转为 WAV 也两步,生成文件头+写入 PCM 数据。
代码链接(github):https://github.com/gongjianbo/MyTestCode/tree/master/Qt/SilkToWav
工程链接(CSDN):https://download.csdn.net/download/gongjianbo1992/13206416
主要代码:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
/**
* @brief wav文件头结构体
* @author 龚建波
* @date 2020-11-12
* @details
* wav头是不定长格式,不过这里用的比较简单的格式
* (数值以小端存储,不过pc一般是小端存储,暂不特殊处理)
* 参照:https://www.cnblogs.com/ranson7zop/p/7657874.html
* 参照:https://www.cnblogs.com/Ph-one/p/6839892.html
*/
struct EasyWavHead
{
char riffFlag[4]; //文档标识,大写"RIFF"
//从下一个字段首地址开始到文件末尾的总字节数。
//该字段的数值加 8 为当前文件的实际长度。
unsigned int riffSize; //数据长度
char waveFlag[4]; //文件格式标识,大写"WAVE"
char fmtFlag[4]; //格式块标识,小写"fmt "
unsigned int fmtSize; //格式块长度,可以是 16、 18 、20、40 等
unsigned short compressionCode; //编码格式代码,1为pcm
unsigned short numChannels; //通道个数
unsigned int sampleRate; //采样频率
//该数值为:声道数×采样频率×每样本的数据位数/8。
//播放软件利用此值可以估计缓冲区的大小。
unsigned int bytesPerSecond; //码率(数据传输速率)
//采样帧大小。该数值为:声道数×位数/8。
//播放软件需要一次处理多个该值大小的字节数据,用该数值调整缓冲区。
unsigned short blockAlign; //数据块对其单位
//存储每个采样值所用的二进制数位数。常见的位数有 4、8、12、16、24、32
unsigned short bitsPerSample; //采样位数(采样深度)
char dataFlag[4]; //表示数据开头,小写"data"
unsigned int dataSize; //数据部分的长度
};
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
void paintEvent(QPaintEvent *event) override;
//silk解码为pcm
void decodeSilk(const QString &filepath);
//生成wav(pcm)文件头信息
//sampleRate: 采样频率
//dataSize: pcm数据字节长度
//return EasyWavHead: wav头
static EasyWavHead createWavHead(int sampleRate,unsigned int dataSize);
private:
Ui::MainWindow *ui;
QByteArray pcmData;
};
#endif // MAINWINDOW_H
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include <QFile>
#include <QTime>
#include <QPainter>
#include <QDebug>
#include "SKP_Silk_SDK_API.h"
#define MAX_BYTES_PER_FRAME 1024
#define MAX_INPUT_FRAMES 5
#define MAX_FRAME_LENGTH 480
#define FRAME_LENGTH_MS 20
#define MAX_API_FS_KHZ 48
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
ui->lineEdit->setText(qApp->applicationDirPath()+"/weixin.amr");
//选择文件
connect(ui->btnFile,&QPushButton::clicked,[=]{
const QString filepath=QFileDialog::getOpenFileName(this);
if(!filepath.isEmpty())
ui->lineEdit->setText(filepath);
});
//解码
connect(ui->btnDecode,&QPushButton::clicked,[=]{
const QString filepath=ui->lineEdit->text();
if(filepath.isEmpty())
return;
qDebug()<<"---------------------------------";
qDebug()<<"sdk version"<<SKP_Silk_SDK_get_version();
qDebug()<<"begin decode"<<QTime::currentTime();
decodeSilk(filepath);
qDebug()<<"end decode"<<QTime::currentTime();
qDebug()<<"data size"<<pcmData.size();
update();
});
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
if(pcmData.count()<4)
return;
painter.translate(0,height()/2);
const int length=pcmData.count()/2;
const short *datas=(const short *)pcmData.constData();
//点的x间隔
double xspace=width()/(double)length;
//绘制采样点步进,测试用的固定值,文件比较大懒得算
const int step=1;
//qDebug()<<"step"<<step;
for(int i=0;i<length-step;i+=step)
{
painter.drawLine(xspace*i,-datas[i]/150,xspace*(i+step),-datas[i+step]/150);
}
}
void MainWindow::decodeSilk(const QString &filepath)
{
//SDK地址:https://gitee.com/alvis/SILK_SDK_SRC
//参照示例:SILK_SDK_SRC_v1.0.9\SILK_SDK_SRC_ARM_v1.0.9\test\Decoder.c
//参照博客:https://blog.csdn.net/weixin_34292924/article/details/87987436
QFile file(filepath);
qDebug()<<"Filepath"<<filepath<<"exists"<<file.exists();
if(file.size()<10||!file.open(QIODevice::ReadOnly))
return;
//先判断silk头
//#!SILK_V3
QByteArray read_temp=file.read(1);
if(read_temp=="#"){
read_temp+=file.read(8);
}else{
//微信的silk前面加个一个字节的0x02
read_temp=file.read(9);
}
if(read_temp!="#!SILK_V3"){
file.close();
return;
}
SKP_SILK_SDK_DecControlStruct dec_ctrl;
//采样率,可作为参数传入
//Valid values are 8000,12000, 16000, 24000, 32000, 44100, and 48000.
const int sample_rate=8000;
dec_ctrl.API_sampleRate = sample_rate;
dec_ctrl.framesPerPacket = 1;
//创建解码器
SKP_int32 dec_size;
int ret = SKP_Silk_SDK_Get_Decoder_Size(&dec_size);
qDebug()<<"SKP_Silk_SDK_Get_Decoder_Size return"<<ret<<"size"<<dec_size;
SKP_uint8 *dec_state=new SKP_uint8[dec_size];
//初始化解码器
ret=SKP_Silk_SDK_InitDecoder(dec_state);
qDebug()<<"SKP_Silk_SDK_InitDecoder return"<<ret;
//默认解出来貌似是16bit的精度
SKP_uint8 payload[MAX_BYTES_PER_FRAME * MAX_INPUT_FRAMES], *payload_ptr = NULL;
SKP_int16 out[((FRAME_LENGTH_MS * MAX_API_FS_KHZ) << 1) * MAX_INPUT_FRAMES], *out_ptr = NULL;
SKP_int16 n_bytes=0;
SKP_int32 read_counter=0;
SKP_int16 len=0;
SKP_int16 total_len=0;
SKP_int32 frames=0;
pcmData.clear();
//循环读取并解码
while (true) {
//读取有效数据大小
read_counter=file.read((char*)&n_bytes,sizeof(SKP_int16));
if(n_bytes<1||read_counter<1)
break;
//读取有效数据
read_counter=file.read((char*)payload,n_bytes);
//qDebug()<<"read_counter"<<read_counter<<n_bytes<<QByteArray((char*)dec_state,sizeof(SKP_uint8)).toHex();
if( (SKP_int16)read_counter < n_bytes ) {
break;
}
payload_ptr=payload;
out_ptr=out;
total_len = 0;
frames = 0;
do {
//解码
ret = SKP_Silk_SDK_Decode(dec_state, &dec_ctrl, 0, payload_ptr, n_bytes, out_ptr, &len);
if (ret) {
qDebug()<<"SKP_Silk_SDK_Decode returned"<<ret;
}
frames++;
out_ptr += len;
total_len += len;
if (frames > MAX_INPUT_FRAMES) {
qDebug()<<"frames > MAX_INPUT_FRAMES"<<frames<<total_len;
out_ptr = out;
total_len = 0;
frames = 0;
}
} while (dec_ctrl.moreInternalDecoderFrames);
//将解码后的数据pcm保存
pcmData.append((const char *)out,total_len*2);
}
//清理
file.close();
delete[]dec_state;
//以wav(pcm s16)格式写入
file.setFileName(filepath+".pcm.wav");
if(file.open(QIODevice::WriteOnly)){
//自定义的wav头结构体
EasyWavHead head=createWavHead(sample_rate,pcmData.count());
file.write((const char*)&head,sizeof(head));
file.write(pcmData);
file.close();
}
}
EasyWavHead MainWindow::createWavHead(int sampleRate, unsigned int dataSize)
{
//默认貌似是16位精度,单通道
const int bits=16;
const int channels=1;
const int head_size = sizeof(EasyWavHead);
EasyWavHead wav_head;
memset(&wav_head, 0, head_size);
memcpy(wav_head.riffFlag, "RIFF", 4);
memcpy(wav_head.waveFlag, "WAVE", 4);
memcpy(wav_head.fmtFlag, "fmt ", 4);
memcpy(wav_head.dataFlag, "data", 4);
//出去头部前8个字节的长度,用的44字节的格式头,所以+44-8=36
wav_head.riffSize = dataSize + 36;
//不知道干嘛的
wav_head.fmtSize = 16;
//1为pcm
wav_head.compressionCode = 0x01;
wav_head.numChannels = channels;
wav_head.sampleRate = sampleRate;
wav_head.bytesPerSecond = (bits / 8) * channels * sampleRate;
wav_head.blockAlign = (bits / 8) * channels;
wav_head.bitsPerSample = bits;
//除去头的数据长度
wav_head.dataSize = dataSize;
return wav_head;
}