From 6cd3b7df67880e9444a9bee8414b35a3380ae733 Mon Sep 17 00:00:00 2001 From: badaix Date: Sun, 3 Nov 2019 17:37:04 +0100 Subject: [PATCH] opus add pseudo header and configurable options --- client/decoder/opus_decoder.cpp | 77 +++++++++++++++++++------- client/decoder/opus_decoder.hpp | 1 + server/encoder/opus_encoder.cpp | 96 ++++++++++++++++++++++++++++++++- server/encoder/opus_encoder.hpp | 2 + 4 files changed, 155 insertions(+), 21 deletions(-) diff --git a/client/decoder/opus_decoder.cpp b/client/decoder/opus_decoder.cpp index c35d43ba..bffb3dcc 100644 --- a/client/decoder/opus_decoder.cpp +++ b/client/decoder/opus_decoder.cpp @@ -18,16 +18,21 @@ #include "opus_decoder.hpp" #include "common/aixlog.hpp" +#include "common/snap_exception.hpp" +#include "common/str_compat.hpp" + +#define ID_OPUS 0x4F505553 + +/// int: Number of samples per channel in the input signal. +/// This must be an Opus frame size for the encoder's sampling rate. For example, at 48 kHz the +/// permitted values are 120, 240, 480, 960, 1920, and 2880. +/// Passing in a duration of less than 10 ms (480 samples at 48 kHz) will prevent the encoder from using the LPC or hybrid modes. +static constexpr int const_max_frame_size = 2880; -OpusDecoder::OpusDecoder() : Decoder() +OpusDecoder::OpusDecoder() : Decoder(), dec_(nullptr) { - int error; - dec_ = opus_decoder_create(48000, 2, &error); // fixme - if (error != 0) - { - LOG(ERROR) << "Failed to initialize opus decoder: " << error << '\n' << " Rate: " << 48000 << '\n' << " Channels: " << 2 << '\n'; - } + pcm_.resize(120); } @@ -40,32 +45,66 @@ OpusDecoder::~OpusDecoder() bool OpusDecoder::decode(msg::PcmChunk* chunk) { - size_t samples = 480; - // reserve space for decoded audio - if (pcm_.size() < samples * 2) - pcm_.resize(samples * 2); + int frame_size = 0; + + while ((frame_size = opus_decode(dec_, (unsigned char*)chunk->payload, chunk->payloadSize, pcm_.data(), pcm_.size() / sample_format_.channels, 0)) == + OPUS_BUFFER_TOO_SMALL) + { + if (pcm_.size() < const_max_frame_size * sample_format_.channels) + { + pcm_.resize(pcm_.size() * 2); + LOG(INFO) << "OPUS encoding buffer too small, resizing to " << pcm_.size() / sample_format_.channels << " samples per channel\n"; + } + else + break; + } - // decode - int frame_size = opus_decode(dec_, (unsigned char*)chunk->payload, chunk->payloadSize, pcm_.data(), samples, 0); if (frame_size < 0) { - LOG(ERROR) << "Failed to decode chunk: " << frame_size << '\n' << " IN size: " << chunk->payloadSize << '\n' << " OUT size: " << samples * 4 << '\n'; + LOG(ERROR) << "Failed to decode chunk: " << frame_size << '\n' << " IN size: " << chunk->payloadSize << '\n' << " OUT size: " << pcm_.size() << '\n'; return false; } else { - LOG(DEBUG) << "Decoded chunk: size " << chunk->payloadSize << " bytes, decoded " << frame_size << " bytes" << '\n'; + LOG(DEBUG) << "Decoded chunk: size " << chunk->payloadSize << " bytes, decoded " << frame_size << " samples" << '\n'; // copy encoded data to chunk - chunk->payloadSize = samples * 4; + chunk->payloadSize = frame_size * sample_format_.channels * sizeof(opus_int16); chunk->payload = (char*)realloc(chunk->payload, chunk->payloadSize); - memcpy(chunk->payload, (char*)pcm_.data(), samples * 4); + memcpy(chunk->payload, (char*)pcm_.data(), chunk->payloadSize); return true; } } -SampleFormat OpusDecoder::setHeader(msg::CodecHeader* /*chunk*/) +SampleFormat OpusDecoder::setHeader(msg::CodecHeader* chunk) { - return {48000, 16, 2}; + // decode the opus pseudo header + if (chunk->payloadSize < 12) + throw SnapException("OPUS header too small"); + + // decode the "opus id" magic number, this is our constant part that must match + uint32_t id_opus; + memcpy(&id_opus, chunk->payload, sizeof(id_opus)); + if (SWAP_32(id_opus) != ID_OPUS) + throw SnapException("Not an Opus pseudo header"); + + // decode the sampleformat + uint32_t rate; + memcpy(&rate, chunk->payload + 4, sizeof(id_opus)); + uint16_t bits; + memcpy(&bits, chunk->payload + 8, sizeof(bits)); + uint16_t channels; + memcpy(&channels, chunk->payload + 10, sizeof(channels)); + + sample_format_.setFormat(SWAP_32(rate), SWAP_16(bits), SWAP_16(channels)); + LOG(INFO) << "Opus sampleformat: " << sample_format_.getFormat() << "\n"; + + // create the decoder + int error; + dec_ = opus_decoder_create(sample_format_.rate, sample_format_.channels, &error); + if (error != 0) + throw SnapException("Failed to initialize Opus decoder: " + cpt::to_string(error)); + + return sample_format_; } diff --git a/client/decoder/opus_decoder.hpp b/client/decoder/opus_decoder.hpp index 1334858e..b066e38b 100644 --- a/client/decoder/opus_decoder.hpp +++ b/client/decoder/opus_decoder.hpp @@ -33,4 +33,5 @@ public: private: OpusDecoder* dec_; std::vector pcm_; + SampleFormat sample_format_; }; diff --git a/server/encoder/opus_encoder.cpp b/server/encoder/opus_encoder.cpp index a5a6d09f..5154ec1d 100644 --- a/server/encoder/opus_encoder.cpp +++ b/server/encoder/opus_encoder.cpp @@ -20,11 +20,27 @@ #include "common/aixlog.hpp" #include "common/snap_exception.hpp" #include "common/str_compat.hpp" +#include "common/utils/string_utils.hpp" +#define ID_OPUS 0x4F505553 + +namespace +{ +template +void assign(void* pointer, T val) +{ + T* p = (T*)pointer; + *p = val; +} +} // namespace + +// TODO: +// - handle variable chunk durations (now it's fixed to 10ms) // const msg::SampleFormat& format); OpusEncoder::OpusEncoder(const std::string& codecOptions) : Encoder(codecOptions), enc_(nullptr) { + headerChunk_.reset(new msg::CodecHeader("opus")); } @@ -35,6 +51,17 @@ OpusEncoder::~OpusEncoder() } +std::string OpusEncoder::getAvailableOptions() const +{ + return "BR:[6 - 512|MAX|AUTO],COMPLEXITY:[1-10]"; +} + + +std::string OpusEncoder::getDefaultOptions() const +{ + return "BR:192,COMPLEXITY:10"; +} + std::string OpusEncoder::name() const { return "opus"; @@ -43,13 +70,78 @@ std::string OpusEncoder::name() const void OpusEncoder::initEncoder() { + opus_int32 bitrate = 192000; + opus_int32 complexity = 10; + auto options = utils::string::split(codecOptions_, ','); + for (const auto& option : options) + { + auto kv = utils::string::split(option, ':'); + if (kv.size() == 2) + { + if (kv.front() == "BR") + { + if (kv.back() == "MAX") + bitrate = OPUS_BITRATE_MAX; + else if (kv.back() == "AUTO") + bitrate = OPUS_AUTO; + else + { + try + { + bitrate = cpt::stoi(kv.back()); + if ((bitrate < 6) || (bitrate > 512)) + throw SnapException("Opus bitrate must be between 6 and 512"); + bitrate *= 1000; + } + catch (const std::invalid_argument&) + { + throw SnapException("Opus error parsing bitrate (must be between 6 and 512): " + kv.back()); + } + } + } + else if (kv.front() == "COMPLEXITY") + { + try + { + complexity = cpt::stoi(kv.back()); + if ((complexity < 1) || (complexity > 10)) + throw SnapException("Opus complexity must be between 1 and 10"); + } + catch (const std::invalid_argument&) + { + throw SnapException("Opus error parsing complexity (must be between 1 and 10): " + kv.back()); + } + } + else + throw SnapException("Opus unknown option: " + kv.front()); + } + else + throw SnapException("Opus error parsing options: " + codecOptions_); + } + + LOG(INFO) << "Opus bitrate: " << bitrate << " bps, complexity: " << complexity << "\n"; + int error; enc_ = opus_encoder_create(sampleFormat_.rate, sampleFormat_.channels, OPUS_APPLICATION_RESTRICTED_LOWDELAY, &error); if (error != 0) { - throw SnapException("Failed to initialize opus encoder: " + cpt::to_string(error)); + throw SnapException("Failed to initialize Opus encoder: " + cpt::to_string(error)); } - headerChunk_.reset(new msg::CodecHeader("opus")); + + opus_encoder_ctl(enc_, OPUS_SET_BITRATE(bitrate)); // max 512000, OPUS_BITRATE_MAX, OPUS_AUTO + opus_encoder_ctl(enc_, OPUS_SET_COMPLEXITY(complexity)); // max 10 + + if ((sampleFormat_.rate != 48000) || (sampleFormat_.bits != 16) || (sampleFormat_.channels != 2)) + throw SnapException("Opus sampleformat must be 48000:16:2"); + + // create some opus pseudo header to let the decoder know about the sample format + headerChunk_->payloadSize = 12; + headerChunk_->payload = (char*)malloc(headerChunk_->payloadSize); + char* payload = headerChunk_->payload; + assign(payload, SWAP_32(ID_OPUS)); + assign(payload + 4, SWAP_32(sampleFormat_.rate)); + assign(payload + 8, SWAP_16(sampleFormat_.bits)); + assign(payload + 10, SWAP_16(sampleFormat_.channels)); } diff --git a/server/encoder/opus_encoder.hpp b/server/encoder/opus_encoder.hpp index e4706e98..70b11cd7 100644 --- a/server/encoder/opus_encoder.hpp +++ b/server/encoder/opus_encoder.hpp @@ -29,6 +29,8 @@ public: ~OpusEncoder() override; void encode(const msg::PcmChunk* chunk) override; + std::string getAvailableOptions() const override; + std::string getDefaultOptions() const override; std::string name() const override; protected: