libopus 实现pcm 编码到opus

  • Post author:
  • Post category:其他


opus 是一种音频格式,常用于语音通话、视频会议中。最近做了个pcm 到opus 的编码,踩了不少坑,特此记录一下。


目录


1、基础知识


2、使用流程


2.1 创建编码器


2.2 编码器配置


2.3 进行编码


2.4 完整代码


3、结果验证


4、参考资料


1、基础知识

opus 支持2.5、5、10、20、40、60ms 等帧长,对于一个48000khz 的 16bit,双通道,20 ms 的pcm 音频来说,每ms 样本数为 48000/1000 = 48,采用位深为16bit/8 = 2byte,所以需要的pcm 字节数为

   pcm size = 48 样本/ms  X 20ms X 2byte X 2 channel = 3840 byte

对于采样为16 bit 的2声道的PCM 数据来说,其内存布局如下图所示

LLLL LLLL LLLL LLLL RRRR RRRR RRRR RRRR

opus 编码函数是 opus_encode,其输入数组是 opus_int16 数组,2字节,要进行unsigned char 数组到 opus_int16 数组的转换后才能送入编码器。

2、使用流程

2.1 创建编码器

OPUS_EXPORT OPUS_WARN_UNUSED_RESULT OpusEncoder *opus_encoder_create(
    opus_int32 Fs,
    int channels,
    int application,
    int *error
);

fs:采样率,8000,12000,16000,24000,48000 之一

channels:通道数

application:编码模式,有三种:

OPUS_APPLICATION_VOIP:对语音信号进行处理,适用于voip 业务场景

OPUS_APPLICATION_AUDIO:这个模式适用于音乐类型等非语音内容

OPUS_APPLICATION_RESTRICTED_LOWDELAY:低延迟模式

error :编码返回值

2.2 编码器配置

opus_encoder_ctl(OpusEncoder *st, int request, ...)

st:opus_encoder_create 创建的结构体

request:宏定义的配置参数

典型配置

    opus_encoder_ctl(encoder, OPUS_SET_VBR(0));//0:CBR, 1:VBR
    opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(true));
    opus_encoder_ctl(encoder, OPUS_SET_BITRATE(96000));
    opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(8));//8    0~10
    opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
    opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(16));//每个采样16个bit,2个byte
    opus_encoder_ctl(encoder, OPUS_SET_DTX(0));
    opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(0));

2.3 进行编码

OPUS_EXPORT OPUS_WARN_UNUSED_RESULT opus_int32 opus_encode(
    OpusEncoder *st,
    const opus_int16 *pcm,
    int frame_size,
    unsigned char *data,
    opus_int32 max_data_bytes
) 

st:opus编码器实例

pcm:输入的pcm 数据,双通道的话数据交叉存储,大小为 frame_size x channels x sizeof(ipus_int16)。

frme_size:

每个通道中输入音频信号的样本数,这里不是传pcm 数组大小,比如采用的是 48000 hz 编码,20ms 帧长,那么frame_size 应该是48*2 = 960,pcm 分配大小= frame_size x channels x sizeof(ipus_int16)。

data:输出缓冲区,接收编码后的数据

max_data_bytes:输出缓冲区大小

返回值:实际编码后输出数据大小

2.4 完整代码

base_type.h


#ifndef __BASE_TYPE_H__
#define __BASE_TYPE_H__
typedef struct StreamInfo
{
    unsigned char *data;
    int len;
    int dts;
}StreamInfo;

#endif

OpusEncoderImpl.h


#ifndef __OPUSENCODERIMPL_H
#define __OPUSENCODERIMPL_H 
#include "include/opus/opus.h"
#include <vector>
#include <mutex>
#include "base_type.h"
#include <queue>
#include <thread>


class OpusEncoderImpl
{
private:
   OpusEncoder *encoder;
   const int channel_num;
   int sample_rate;
   std::queue<StreamInfo> info_queue;
   std::queue <unsigned char> pcm_queue;
   std::mutex mutex;
   bool isRuning = true;
   std::mutex access_mutex;
   std::unique_ptr<std::thread> m_thread;
public:
    OpusEncoderImpl(int sampleRate, int channel);
    void Feed(unsigned char*data, int len);
    bool PopFrame(StreamInfo &info);
    void EncodeRun();
    void Stop();
    ~OpusEncoderImpl();
};

OpusEncoderImpl.cpp

#include "OpusEncoderImpl.h"
#include "OpusDecoderImpl.h"
#include <unistd.h>
#include <stdlib.h>
#define MAX_PACKET_SIZE 3*1276

/*
* sampleRate:采样率
* channel:通道数
*/

OpusEncoderImpl::OpusEncoderImpl(int sampleRate, int channel):channel_num(channel),sample_rate(sampleRate)
 {
    int err;
    int applications[3] = {OPUS_APPLICATION_AUDIO, OPUS_APPLICATION_VOIP, OPUS_APPLICATION_RESTRICTED_LOWDELAY};
   
    encoder = opus_encoder_create(sampleRate, channel_num, applications[1], &err);

    if(err != OPUS_OK || encoder == NULL) {
        printf("打开opus 编码器失败\n");
    }

    opus_encoder_ctl(encoder, OPUS_SET_VBR(0));//0:CBR, 1:VBR
    opus_encoder_ctl(encoder, OPUS_SET_VBR_CONSTRAINT(true));
    opus_encoder_ctl(encoder, OPUS_SET_BITRATE(96000));
    opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(8));//8    0~10
    opus_encoder_ctl(encoder, OPUS_SET_SIGNAL(OPUS_SIGNAL_VOICE));
    opus_encoder_ctl(encoder, OPUS_SET_LSB_DEPTH(16));//每个采样16个bit,2个byte
    opus_encoder_ctl(encoder, OPUS_SET_DTX(0));
    opus_encoder_ctl(encoder, OPUS_SET_INBAND_FEC(0));

    EncodeRun();
}
//每一帧pcm 是23ms
void OpusEncoderImpl::Feed(unsigned char *data, int len) {
    mutex.lock();
    for(auto i = 0;i < len;i++) {
        pcm_queue.emplace(data[i]);
    }
    mutex.unlock();
}

bool OpusEncoderImpl::PopFrame(StreamInfo &info) {
    if(info_queue.size() > 0) {
        access_mutex.lock();
        info = info_queue.front();
        info_queue.pop();
        access_mutex.unlock();
        return true;
    }

    return false;
}


//48000 采样率,48个样本/ms * 20ms * 2 channel = 1920
void OpusEncoderImpl::EncodeRun() {
    m_thread = std::make_unique<std::thread>([this](){
        const int frame_size = 48*20;//960
        const int input_len = sizeof(opus_int16) * frame_size * 2;

        FILE *opus_file = fopen("/data/bin/out.customopus", "wb+");
        FILE *pcm_file = fopen("/data/bin/out.pcm", "wb+");
        OpusDecoderImpl decoder(48000, channel_num);

        opus_int16 input_data[frame_size * 2] = {0};//frame_size*channels*sizeof(opus_int16)
        unsigned char input_buffer[input_len] = {0};//每一帧的数据量
        unsigned char out_data[MAX_PACKET_SIZE] = {0};
        
        while (isRuning) {   
            if(pcm_queue.size() >= input_len) {
                mutex.lock();              
                for(int i = 0;i < input_len;i++) 
                {
                    input_buffer[i] = pcm_queue.front();
                    pcm_queue.pop();
                }

                // for (size_t i = 0; i < frame_size * channel_num; i++)
                // {
                //     input_data[i] = input_buffer[2*i + 1] << 8 | input_buffer[2*i];
                // }

                mutex.unlock();
                memcpy(input_data, input_buffer, input_len);
                // fwrite(input_buffer, 1, input_len, pcm_file);
                // fflush(pcm_file);          
                auto ret = opus_encode(encoder, input_data, frame_size, out_data, MAX_PACKET_SIZE);
                if(ret < 0) {
                    printf("opus编码失败, %d\n", ret);
                    break;
                }

                //写入文件
                // uint32_t len = static_cast<int>(ret);
                // fwrite(&len, 1, sizeof(uint32_t), opus_file);
                //fwrite(out_data, 1, ret, opus_file);              
                //fflush(opus_file);             
                unsigned char* opus_buffer = (unsigned char*)malloc(ret);
                memcpy(opus_buffer, out_data, ret);
                //decoder.Decode(opus_buffer, ret);

                StreamInfo info;
                info.data = opus_buffer;
                info.len = ret;
                info.dts = 20;
                access_mutex.lock();             
                info_queue.push(info);
                access_mutex.unlock();                      
                
            }else {
                usleep(1000);
            }  
        } 
    });

}

void OpusEncoderImpl::Stop() {
    isRuning = false;
    m_thread->join();

    while (pcm_queue.size() > 0)
    {
        pcm_queue.pop();
    }

    opus_encoder_destroy(encoder);
    
}

OpusEncoderImpl::~OpusEncoderImpl() {

}

3、结果验证

验证方法一是将编码后的文件打包成ogg,或者将编码后的数据再解码成pcm,用audacity 查看,这里是采用的是后者。

OpusDecoderImpl.h


#ifndef __OPUSDECODERIMPL_H
#define __OPUSDECODERIMPL_H
#include <stdio.h>
#include "include/opus/opus.h"
#include <vector>
#include <mutex>
#include "base_type.h"
#include <queue>
#include <thread>

class OpusDecoderImpl
{
private:
    /* data */
    OpusDecoder *decoder;
    int sample_rate;
    int channel_num;
    FILE *pcm_file;
public:
    bool Decode(unsigned char* in_data, int len);
    OpusDecoderImpl(int sampleRate, int channel);
    ~OpusDecoderImpl();
};

#endif

OpusDecoderImpl.cpp

#include "OpusDecoderImpl.h"
#define MAX_FRAME_SIZE 6*960
#define CHANNELS 2

OpusDecoderImpl::OpusDecoderImpl(int sampleRate, int channel) 
{
    int err;
    decoder = opus_decoder_create(sampleRate, channel, &err);
    opus_decoder_ctl(decoder, OPUS_SET_LSB_DEPTH(16));
    sample_rate = sample_rate;
    channel_num = channel;
    if(err < 0 || decoder == NULL)
    {
        printf("创建解码器失败\n");
        return;
    }

    pcm_file = fopen("/data/bin/decode.pcm", "wb+");
}

bool OpusDecoderImpl::Decode(unsigned char* in_data, int len)
{
    unsigned char pcm_bytes[MAX_FRAME_SIZE * CHANNELS * 2];
    opus_int16 out[MAX_FRAME_SIZE * CHANNELS];
    auto frame_size = opus_decode(decoder, in_data, len, out, MAX_FRAME_SIZE, 0);

    if (frame_size < 0)
    {
       printf("解码失败\n");
       return false;
    }

    for (auto i = 0; i < channel_num * frame_size; i++)
    {
        pcm_bytes[2 * i] = out[i] & 0xFF;
        pcm_bytes[2 * i + 1] = (out[i] >> 8) & 0xFF;
    }

    fwrite(pcm_bytes, sizeof(short), frame_size * channel_num, pcm_file);
    fflush(pcm_file);
    return true;
}

OpusDecoderImpl::~OpusDecoderImpl()
{

}

补上函数调用(ps:代码可能会有语法错误,请自行解决)

int main(int argc, char** argc)
{
    OpusEncoderImpl opusEncoder = new OpusEncoderImpl(48000, 2);

    for (size_t i = 0; i < 100; i++)
    {
        opusEncoder.Feed(pcm_data, pcm_len);//送pcm 数据编码
    }

    //读取编码后的opus,一般放在单独线程,这里只是为了方便
    StreamInfo info;
    while (opusEncoder.PopFrame(info))
    {
        .....
    }
    
    opusEncoder.Stop();
    
}


4、参考资料


音频和OPUS开源库简介_WuYuJun’s blog的博客-CSDN博客_opus库


音视频编解码–Opus编解码系列1_Fenngtun的博客-CSDN博客


Qt 之 opus编码_老菜鸟的每一天的博客-CSDN博客_opus编码



版权声明:本文为sinat_27720649原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。