diff --git a/NEWS b/NEWS
index 5b578056d..f2991a53f 100644
--- a/NEWS
+++ b/NEWS
@@ -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)
diff --git a/doc/user.xml b/doc/user.xml
index 41b0299a8..e547f1279 100644
--- a/doc/user.xml
+++ b/doc/user.xml
@@ -3251,6 +3251,23 @@ run
(the default), "voice" and "music".
+
+
+
+ opustags
+ yes|no
+
+
+ Configures how metadata is interleaved into the stream.
+ If set to yes, then metadata
+ is inserted using ogg stream chaining, as specified
+ in RFC
+ 7845. If set to no
+ (the default), then ogg stream chaining is avoided
+ and other output-dependent method is used, if
+ available.
+
+
diff --git a/src/encoder/plugins/OpusEncoderPlugin.cxx b/src/encoder/plugins/OpusEncoderPlugin.cxx
index b4419882c..b7424171f 100644
--- a/src/encoder/plugins/OpusEncoderPlugin.cxx
+++ b/src/encoder/plugins/OpusEncoderPlugin.cxx
@@ -24,6 +24,7 @@
#include "config/ConfigError.hxx"
#include "util/Alloc.hxx"
#include "system/ByteOrder.hxx"
+#include "util/StringUtil.hxx"
#include
#include
@@ -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,24 +189,24 @@ 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
? opus_encode(enc,
- (const opus_int16 *)buffer,
- buffer_frames,
- buffer2,
- sizeof(buffer2))
+ (const opus_int16 *)buffer,
+ buffer_frames,
+ buffer2,
+ sizeof(buffer2))
: opus_encode_float(enc,
- (const float *)buffer,
- buffer_frames,
- buffer2,
- sizeof(buffer2));
+ (const float *)buffer,
+ buffer_frames,
+ buffer2,
+ sizeof(buffer2));
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);
}
}