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
* mixer
- sndio: new mixer plugin
* encoder
- opus: support for sending metadata using ogg stream chaining
* require GCC 5.0
ver 0.20.18 (2018/02/24)

View File

@ -3251,6 +3251,23 @@ run</programlisting>
(the default), "voice" and "music".
</entry>
</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>
</tgroup>
</informaltable>

View File

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