encoder/opus: add optional stream chaining support

support for chaining ogg opus streams to enable changing stream' metadata on the fly.
currently support is opt-in (enabled by additional option) because lots of clients can't handle this properly yet.
This commit is contained in:
cathugger 2018-01-26 15:34:44 +00:00
parent 1628d801f9
commit 47d1d3c855
3 changed files with 105 additions and 32 deletions

2
NEWS
View File

@ -26,6 +26,8 @@ ver 0.21 (not yet released)
- sndio: remove support for the broken RoarAudio sndio emulation - sndio: remove support for the broken RoarAudio sndio emulation
* mixer * mixer
- sndio: new mixer plugin - sndio: new mixer plugin
* encoder
- opus: support for sending metadata using ogg stream chaining
* require GCC 5.0 * require GCC 5.0
ver 0.20.18 (2018/02/24) ver 0.20.18 (2018/02/24)

View File

@ -3251,6 +3251,23 @@ run</programlisting>
(the default), "voice" and "music". (the default), "voice" and "music".
</entry> </entry>
</row> </row>
<row>
<entry>
<varname>opustags</varname>
<parameter>yes|no</parameter>
</entry>
<entry>
Configures how metadata is interleaved into the stream.
If set to <parameter>yes</parameter>, then metadata
is inserted using ogg stream chaining, as specified
in <ulink url="https://tools.ietf.org/html/rfc7845.html#section-7.2">RFC
7845</ulink>. If set to <parameter>no</parameter>
(the default), then ogg stream chaining is avoided
and other output-dependent method is used, if
available.
</entry>
</row>
</tbody> </tbody>
</tgroup> </tgroup>
</informaltable> </informaltable>

View File

@ -24,6 +24,7 @@
#include "config/ConfigError.hxx" #include "config/ConfigError.hxx"
#include "util/Alloc.hxx" #include "util/Alloc.hxx"
#include "system/ByteOrder.hxx" #include "system/ByteOrder.hxx"
#include "util/StringUtil.hxx"
#include <opus.h> #include <opus.h>
#include <ogg/ogg.h> #include <ogg/ogg.h>
@ -55,27 +56,30 @@ class OpusEncoder final : public OggEncoder {
ogg_int64_t granulepos = 0; ogg_int64_t granulepos = 0;
public: public:
OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc); OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc, bool _chaining);
~OpusEncoder() override; ~OpusEncoder() override;
/* virtual methods from class Encoder */ /* virtual methods from class Encoder */
void End() override; void End() override;
void Write(const void *data, size_t length) override; void Write(const void *data, size_t length) override;
size_t Read(void *dest, size_t length) override; void PreTag() override;
void SendTag(const Tag &tag) override;
private: private:
void DoEncode(bool eos); void DoEncode(bool eos);
void WriteSilence(unsigned fill_frames); void WriteSilence(unsigned fill_frames);
void GenerateHeaders(const Tag *tag);
void GenerateHead(); void GenerateHead();
void GenerateTags(); void GenerateTags(const Tag *tag);
}; };
class PreparedOpusEncoder final : public PreparedEncoder { class PreparedOpusEncoder final : public PreparedEncoder {
opus_int32 bitrate; opus_int32 bitrate;
int complexity; int complexity;
int signal; int signal;
const bool chaining;
public: public:
PreparedOpusEncoder(const ConfigBlock &block); PreparedOpusEncoder(const ConfigBlock &block);
@ -89,6 +93,7 @@ public:
}; };
PreparedOpusEncoder::PreparedOpusEncoder(const ConfigBlock &block) PreparedOpusEncoder::PreparedOpusEncoder(const ConfigBlock &block)
:chaining(block.GetBlockValue("opustags", false))
{ {
const char *value = block.GetBlockValue("bitrate", "auto"); const char *value = block.GetBlockValue("bitrate", "auto");
if (strcmp(value, "auto") == 0) if (strcmp(value, "auto") == 0)
@ -124,8 +129,8 @@ opus_encoder_init(const ConfigBlock &block)
return new PreparedOpusEncoder(block); return new PreparedOpusEncoder(block);
} }
OpusEncoder::OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc) OpusEncoder::OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc, bool _chaining)
:OggEncoder(false), :OggEncoder(_chaining),
audio_format(_audio_format), audio_format(_audio_format),
frame_size(_audio_format.GetFrameSize()), frame_size(_audio_format.GetFrameSize()),
buffer_frames(_audio_format.sample_rate / 50), buffer_frames(_audio_format.sample_rate / 50),
@ -134,6 +139,7 @@ OpusEncoder::OpusEncoder(AudioFormat &_audio_format, ::OpusEncoder *_enc)
enc(_enc) enc(_enc)
{ {
opus_encoder_ctl(enc, OPUS_GET_LOOKAHEAD(&lookahead)); opus_encoder_ctl(enc, OPUS_GET_LOOKAHEAD(&lookahead));
GenerateHeaders(nullptr);
} }
Encoder * Encoder *
@ -171,7 +177,7 @@ PreparedOpusEncoder::Open(AudioFormat &audio_format)
opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity)); opus_encoder_ctl(enc, OPUS_SET_COMPLEXITY(complexity));
opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal)); opus_encoder_ctl(enc, OPUS_SET_SIGNAL(signal));
return new OpusEncoder(audio_format, enc); return new OpusEncoder(audio_format, enc, chaining);
} }
OpusEncoder::~OpusEncoder() OpusEncoder::~OpusEncoder()
@ -183,24 +189,24 @@ OpusEncoder::~OpusEncoder()
void void
OpusEncoder::DoEncode(bool eos) OpusEncoder::DoEncode(bool eos)
{ {
assert(buffer_position == buffer_size); assert(buffer_position == buffer_size || eos);
opus_int32 result = opus_int32 result =
audio_format.format == SampleFormat::S16 audio_format.format == SampleFormat::S16
? opus_encode(enc, ? opus_encode(enc,
(const opus_int16 *)buffer, (const opus_int16 *)buffer,
buffer_frames, buffer_frames,
buffer2, buffer2,
sizeof(buffer2)) sizeof(buffer2))
: opus_encode_float(enc, : opus_encode_float(enc,
(const float *)buffer, (const float *)buffer,
buffer_frames, buffer_frames,
buffer2, buffer2,
sizeof(buffer2)); sizeof(buffer2));
if (result < 0) if (result < 0)
throw std::runtime_error("Opus encoder error"); throw std::runtime_error("Opus encoder error");
granulepos += buffer_frames; granulepos += buffer_position / frame_size;
ogg_packet packet; ogg_packet packet;
packet.packet = buffer2; packet.packet = buffer2;
@ -217,13 +223,10 @@ OpusEncoder::DoEncode(bool eos)
void void
OpusEncoder::End() OpusEncoder::End()
{ {
Flush();
memset(buffer + buffer_position, 0, memset(buffer + buffer_position, 0,
buffer_size - buffer_position); buffer_size - buffer_position);
buffer_position = buffer_size;
DoEncode(true); DoEncode(true);
Flush();
} }
void void
@ -275,6 +278,13 @@ OpusEncoder::Write(const void *_data, size_t length)
} }
} }
void
OpusEncoder::GenerateHeaders(const Tag *tag)
{
GenerateHead();
GenerateTags(tag);
}
void void
OpusEncoder::GenerateHead() OpusEncoder::GenerateHead()
{ {
@ -290,27 +300,65 @@ OpusEncoder::GenerateHead()
ogg_packet packet; ogg_packet packet;
packet.packet = header; packet.packet = header;
packet.bytes = 19; packet.bytes = sizeof(header);
packet.b_o_s = true; packet.b_o_s = true;
packet.e_o_s = false; packet.e_o_s = false;
packet.granulepos = 0; packet.granulepos = 0;
packet.packetno = packetno++; packet.packetno = packetno++;
stream.PacketIn(packet); stream.PacketIn(packet);
Flush(); // flush not needed because libogg autoflushes on b_o_s flag
} }
void void
OpusEncoder::GenerateTags() OpusEncoder::GenerateTags(const Tag *tag)
{ {
const char *version = opus_get_version_string(); const char *version = opus_get_version_string();
size_t version_length = strlen(version); size_t version_length = strlen(version);
// len("OpusTags") + 4 byte version length + len(version) + 4 byte tag count
size_t comments_size = 8 + 4 + version_length + 4; size_t comments_size = 8 + 4 + version_length + 4;
uint32_t tag_count = 0;
if (tag) {
for (const auto &item: *tag) {
++tag_count;
// 4 byte length + len(tagname) + len('=') + len(value)
comments_size += 4 + strlen(tag_item_names[item.type]) + 1 + strlen(item.value);
}
}
unsigned char *comments = (unsigned char *)xalloc(comments_size); unsigned char *comments = (unsigned char *)xalloc(comments_size);
unsigned char *p = comments;
memcpy(comments, "OpusTags", 8); memcpy(comments, "OpusTags", 8);
*(uint32_t *)(comments + 8) = ToLE32(version_length); *(uint32_t *)(comments + 8) = ToLE32(version_length);
memcpy(comments + 12, version, version_length); p += 12;
*(uint32_t *)(comments + 12 + version_length) = ToLE32(0);
memcpy(p, version, version_length);
p += version_length;
tag_count = ToLE32(tag_count);
memcpy(p, &tag_count, 4);
p += 4;
if (tag) {
for (const auto &item: *tag) {
size_t tag_name_len = strlen(tag_item_names[item.type]);
size_t tag_val_len = strlen(item.value);
uint32_t tag_len_le = ToLE32(tag_name_len + 1 + tag_val_len);
memcpy(p, &tag_len_le, 4);
p += 4;
ToUpperASCII((char *)p, tag_item_names[item.type], tag_name_len + 1);
p += tag_name_len;
*p++ = '=';
memcpy(p, item.value, tag_val_len);
p += tag_val_len;
}
}
assert(comments + comments_size == p);
ogg_packet packet; ogg_packet packet;
packet.packet = comments; packet.packet = comments;
@ -325,15 +373,21 @@ OpusEncoder::GenerateTags()
free(comments); free(comments);
} }
size_t void
OpusEncoder::Read(void *dest, size_t length) OpusEncoder::PreTag()
{ {
if (packetno == 0) End();
GenerateHead(); packetno = 0;
else if (packetno == 1) granulepos = 0; // not really required, but useful to prevent wraparound
GenerateTags(); opus_encoder_ctl(enc, OPUS_RESET_STATE);
}
return OggEncoder::Read(dest, length); void
OpusEncoder::SendTag(const Tag &tag)
{
stream.Reinitialize(GenerateOggSerial());
opus_encoder_ctl(enc, OPUS_GET_LOOKAHEAD(&lookahead));
GenerateHeaders(&tag);
} }
} }