解码 Opus 音频数据

Posted

技术标签:

【中文标题】解码 Opus 音频数据【英文标题】:Decoding Opus audio data 【发布时间】:2013-05-05 23:11:17 【问题描述】:

我正在尝试将 Opus 文件解码回原始 48 kHz。 但是我找不到任何示例代码来做到这一点。

我当前的代码是这样的:

void COpusCodec::Decode(unsigned char* encoded, short* decoded, unsigned int len)

     int max_size=960*6;//not sure about this one

     int error;
     dec = opus_decoder_create(48000, 1, &error);//decode to 48kHz mono

     int frame_size=opus_decode(dec, encoded, len, decoded, max_size, 0);

“编码”参数可能是大量数据,所以我认为我必须将其拆分为帧。 我不确定我该怎么做。

作为 Opus 的初学者,我真的很害怕把事情搞砸。

有人可以帮忙吗?

【问题讨论】:

“编码”代表什么?它是原始作品文件流吗?是单帧吗?是多帧吗?如果有,格式是什么? @not-sehe 我使用 opus_demo.exe 对长 RAW 文件进行编码。我没有做任何花哨的事情,我只是使用默认设置。然后我读取了一定数量的字节并将它们提供给 void COpus::Decode。 你不能把它分成任何“一定数量的字节”块。您需要读取下一个块的长度/final_enc_range 该块的数据(len 字节),您可以通过调用opus_decode 将其提供给解码器。如果您的界面不能保证这一点,则必须在两者之间添加自己的缓冲机制。我敢打赌std::stringstream::stringstream(std::ios::binary|std::ios::in|std::ios::out) 在那里可能不仅仅是工具,但你可以轻松地自己动手。 来自rfc6716: 3. Internal Framing:Opus 编码器产生“数据包”[...] 作为单个单元传输。 [...] 单个数据包可能包含多个音频帧,只要 [...]。此框架不是自定界。相反,它假设较低层(例如 UDP 或 RTP [RFC3550]、Ogg [RFC3533] 或 Matroska)将传达数据包的长度(以字节为单位) . ^ 您应该检查一下,但是如果您从 Opus 音频数据包的“典型”来源接收数据包,您几乎可以假设接收到的单元已经在 Opus 数据包边界上划定了界限,因为它显然是Opus 编解码器的自然使用模式。这是有道理的,因为除了通用音频压缩之外,Opus 还适用于实时(语音)压缩和流式传输。 【参考方案1】:

libopus 提供了一个 API,用于将 opus 数据包转换为 PCM 数据块,反之亦然。

但是要将 opus 数据包存储在文件中,您需要某种容器格式来存储数据包边界。 opus_demo 是一个演示应用程序:它有自己的用于测试目的的最小容器格式,未记录在案,因此不应分发 opus_demo 生成的文件。作品文件的标准容器格式是 Ogg,它还支持元数据和样本精确解码以及对可变比特率流的高效搜索。 Ogg Opus 文件的扩展名为“.opus”。

Ogg Opus 规范位于 https://wiki.xiph.org/OggOpus。

(由于 Opus 也是一种 VoIP 编解码器,因此 Opus 的一些用途不需要容器,例如直接通过 UDP 传输 Opus 数据包。)

所以首先你应该使用来自 opus-tools 的opusenc 编码你的文件,而不是opus_demo。其他软件也可以生成 Ogg Opus 文件(例如,我相信 gstreamer 和 ffmpeg 可以),但 opus-tools 不会出错,因为它是参考实现。

然后,假设您的文件是标准 Ogg Opus 文件(可以通过 Firefox 读取),您需要做的是: (a) 从 Ogg 容器中提取 opus 数据包; (b) 将数据包传递给 libopus 并返回原始 PCM。

方便的是,有一个名为 libopusfile 的库正是这样做的。 libopusfile 支持 Ogg Opus 流的所有功能,包括元数据和搜索(包括通过 HTTP 连接搜索)。

libopusfile 在https://git.xiph.org/?p=opusfile.git 和https://github.com/xiph/opusfile 上可用。 API 记录在 here 中,opusfile_example.c (xiph.org | github) 提供了解码为 WAV 的示例代码。由于您使用的是 Windows,我应该在 downloads 页面上添加预构建的 DLL。

【讨论】:

+1 信息丰富。我对文件格式提出了相同的想法,但我并不完全清楚 OP 是否有兴趣解码实时数据包,也许是通过 UDP 接收它们(“单声道”似乎表示没有 hifi 意图,原始标签也包括speex,我将其与语音聊天应用程序相关联) 很好地提到了opusfile_example.c,我希望他们能在 Opus 网站上列出。 是否可以仅使用 libopusfile (a) extract opus packets from the Ogg container; 或这只是端到端解决方案?【参考方案2】:

我认为source tarball 中的opus_demo.c 程序有你想要的。

这很复杂,因为所有不相关的代码都与

编码,从命令行参数解析编码器参数 人工丢包注入 随机帧大小选择/即时更改 带内 FEC(意味着解码成两个缓冲区,在两者之间切换) 调试和验证 比特率统计报告

事实证明,删除所有这些位是一项非常乏味的工作。但是一旦你这样做了,你就会得到非常干净、易于理解的代码,见下文。

注意我

保留“丢包”协议代码(即使从文件中读取不会发生丢包)以供参考 保留解码每一帧后验证最终范围的代码

主要是因为它似乎不会使代码复杂化,而且您可能会对它感兴趣。

我用两种方式测试了这个程序:

听觉(通过验证先前使用opus_demo 编码的单声道wav 是否已使用此剥离解码器正确解码)。测试 wav 约为 23Mb,压缩后为 2.9Mb。 当使用./opus_demo -d 48000 1 <opus-file> <pcm-file> 调用时,回归测试与香草 opus_demo 一起进行。生成的文件与此处使用剥离解码器解码的文件具有相同的 md5sum 校验和。

重大更新我对代码进行了 C++ 化。这应该可以让您使用 iostreams。

现在注意fin.readsome 上的循环;这个循环可以被设为“异步”(即它可以返回,并在新数据到达时继续读取(在下一次调用你的Decode 函数时?[1] 我已经从头文件中删除了对 opus.h 的依赖 为了异常安全和稳健性,我已将“全部”手动内存管理替换为标准库(vectorunique_ptr)。 我实现了一个派生自std::exceptionOpusErrorException 类,用于传播来自libopus 的错误

在此处查看所有代码 + Makefile:https://github.com/sehe/opus/tree/master/contrib

[1] 对于真正的异步 IO(例如网络或串行通信)考虑使用 Boost Asio,参见例如http://www.boost.org/doc/libs/1_53_0/doc/html/boost_asio/overview/networking/iostreams.html

头文件

// (c) Seth Heeren 2013
//
// Based on src/opus_demo.c in opus-1.0.2
// License see http://www.opus-codec.org/license/
#include <stdexcept>
#include <memory>
#include <iosfwd>

struct OpusErrorException : public virtual std::exception

    OpusErrorException(int code) : code(code) 
    const char* what() const noexcept;
private:
    const int code;
;

struct COpusCodec

    COpusCodec(int32_t sampling_rate, int channels);
    ~COpusCodec();

    bool decode_frame(std::istream& fin, std::ostream& fout);
private:
    struct Impl;
    std::unique_ptr<Impl> _pimpl;
;

实施文件

// (c) Seth Heeren 2013
//
// Based on src/opus_demo.c in opus-1.0.2
// License see http://www.opus-codec.org/license/
#include "COpusCodec.hpp"
#include <vector>
#include <iomanip>
#include <memory>
#include <sstream>

#include "opus.h"

#define MAX_PACKET 1500

const char* OpusErrorException::what() const noexcept

    return opus_strerror(code);


// I'd suggest reading with boost::spirit::big_dword or similar
static uint32_t char_to_int(char ch[4])

    return static_cast<uint32_t>(static_cast<unsigned char>(ch[0])<<24) |
        static_cast<uint32_t>(static_cast<unsigned char>(ch[1])<<16) |
        static_cast<uint32_t>(static_cast<unsigned char>(ch[2])<< 8) |
        static_cast<uint32_t>(static_cast<unsigned char>(ch[3])<< 0);


struct COpusCodec::Impl

    Impl(int32_t sampling_rate = 48000, int channels = 1)
    : 
        _channels(channels),
        _decoder(nullptr, &opus_decoder_destroy),
        _state(_max_frame_size, MAX_PACKET, channels)
    
        int err = OPUS_OK;
        auto raw = opus_decoder_create(sampling_rate, _channels, &err);
        _decoder.reset(err == OPUS_OK? raw : throw OpusErrorException(err) );
    

    bool decode_frame(std::istream& fin, std::ostream& fout)
    
        char ch[4] = 0;

        if (!fin.read(ch, 4) && fin.eof())
            return false;

        uint32_t len = char_to_int(ch);

        if(len>_state.data.size())
            throw std::runtime_error("Invalid payload length");

        fin.read(ch, 4);
        const uint32_t enc_final_range = char_to_int(ch);
        const auto data = reinterpret_cast<char*>(&_state.data.front());

        size_t read = 0ul;
        for (auto append_position = data; fin && read<len; append_position += read)
        
            read += fin.readsome(append_position, len-read);
        

        if(read<len)
        
            std::ostringstream oss;
            oss << "Ran out of input, expecting " << len << " bytes got " << read << " at " << fin.tellg();
            throw std::runtime_error(oss.str());
        

        int output_samples;
        const bool lost = (len==0);
        if(lost)
        
            opus_decoder_ctl(_decoder.get(), OPUS_GET_LAST_PACKET_DURATION(&output_samples));
        
        else
        
            output_samples = _max_frame_size;
        

        output_samples = opus_decode(
                _decoder.get(), 
                lost ? NULL : _state.data.data(),
                len,
                _state.out.data(),
                output_samples,
                0);

        if(output_samples>0)
        
            for(int i=0; i<(output_samples)*_channels; i++)
            
                short s;
                s=_state.out[i];
                _state.fbytes[2*i]   = s&0xFF;
                _state.fbytes[2*i+1] = (s>>8)&0xFF;
            
            if(!fout.write(reinterpret_cast<char*>(_state.fbytes.data()), sizeof(short)* _channels * output_samples))
                throw std::runtime_error("Error writing");
        
        else
        
            throw OpusErrorException(output_samples); // negative return is error code
        

        uint32_t dec_final_range;
        opus_decoder_ctl(_decoder.get(), OPUS_GET_FINAL_RANGE(&dec_final_range));

        /* compare final range encoder rng values of encoder and decoder */
        if(enc_final_range!=0
                && !lost && !_state.lost_prev
                && dec_final_range != enc_final_range)
        
            std::ostringstream oss;
            oss << "Error: Range coder state mismatch between encoder and decoder in frame " << _state.frameno << ": " <<
                    "0x" << std::setw(8) << std::setfill('0') << std::hex << (unsigned long)enc_final_range <<
                    "0x" << std::setw(8) << std::setfill('0') << std::hex << (unsigned long)dec_final_range;

            throw std::runtime_error(oss.str());
        

        _state.lost_prev = lost;
        _state.frameno++;

        return true;
    
private:
    const int _channels;
    const int _max_frame_size = 960*6;
    std::unique_ptr<OpusDecoder, void(*)(OpusDecoder*)> _decoder;

    struct State
    
        State(int max_frame_size, int max_payload_bytes, int channels) :
            out   (max_frame_size*channels),
            fbytes(max_frame_size*channels*sizeof(decltype(out)::value_type)),
            data  (max_payload_bytes)
         

        std::vector<short>         out;
        std::vector<unsigned char> fbytes, data;
        int32_t frameno   = 0;
        bool    lost_prev = true;
    ;
    State _state;
;

COpusCodec::COpusCodec(int32_t sampling_rate, int channels)
    : _pimpl(std::unique_ptr<Impl>(new Impl(sampling_rate, channels)))

    //


COpusCodec::~COpusCodec()

    // this instantiates the pimpl deletor code on the, now-complete, pimpl class


bool COpusCodec::decode_frame(
        std::istream& fin,
        std::ostream& fout)

    return _pimpl->decode_frame(fin, fout);

test.cpp

// (c) Seth Heeren 2013
//
// Based on src/opus_demo.c in opus-1.0.2
// License see http://www.opus-codec.org/license/
#include <fstream>
#include <iostream>

#include "COpusCodec.hpp"

int main(int argc, char *argv[])

    if(argc != 3)
    
        std::cerr << "Usage: " << argv[0] << " <input> <output>\n";
        return 255;
    

    std::basic_ifstream<char> fin (argv[1], std::ios::binary);
    std::basic_ofstream<char> fout(argv[2], std::ios::binary);

    if(!fin)  throw std::runtime_error("Could not open input file");
    if(!fout) throw std::runtime_error("Could not open output file");

    try
    
        COpusCodec codec(48000, 1);

        size_t frames = 0;
        while(codec.decode_frame(fin, fout))
        
            frames++;
        

        std::cout << "Successfully decoded " << frames << " frames\n";
    
    catch(OpusErrorException const& e)
    
        std::cerr << "OpusErrorException: " << e.what() << "\n";
        return 255;
    

【讨论】:

感谢您的努力和解释。你能更准确地与我的代码相关吗?我也使用过 Opus 网站上的演示代码,毫无疑问,它可以工作。但我不明白。我需要一种更简单的形式。我已经阅读了编码的字节,我想把它变成解码的短裤。我不再打开文件以供阅读,因此不需要 fread(ch, 1, 4, fin),但我也不知道没有它我该怎么做,因为我不了解示例代码。我很高兴你这样做!你似乎是世界上为数不多的人之一。 见comment at the original post。我认为您应该能够完全使用上述内容,可能在添加缓冲区以与COpusCodec::Decode 的外部用户分离之后,以防您无法“控制”它们并且他们可能会使用 indiscriminate 数据包大小。 @tmighty 我添加了一个带有 C++ 版本代码的重大更新。回归测试可以很好地检查我的测试文件。测试程序现在更干净了:) github.com/sehe/opus/blob/master/contrib/test.cpp。另请参阅答案中的注释。 您是否还有 C 代码? C++ 流让我做噩梦。 @Chase 刚刚意识到我以前有一个清理过的 C 代码版本(为什么?!?! :)),所以保留旧版本:***.com/revisions/16551709/2

以上是关于解码 Opus 音频数据的主要内容,如果未能解决你的问题,请参考以下文章

实时音频编解码之十一Opus编码

实时音频编解码之十六 Opus解码

实时音频编解码之十七 Opus解码 SILK解码

实时音频编解码之十一Opus编码

实时音频编解码之十八 Opus解码 CELT解码

实时音频编解码之十八 Opus解码 CELT解码