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;
}

 

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐