From 47d1d3c855b35d0771d811135e0d8392bcbfc1f2 Mon Sep 17 00:00:00 2001
From: cathugger <cathugger@cock.li>
Date: Fri, 26 Jan 2018 15:34:44 +0000
Subject: [PATCH] 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.
---
 NEWS                                      |   2 +
 doc/user.xml                              |  17 ++++
 src/encoder/plugins/OpusEncoderPlugin.cxx | 118 ++++++++++++++++------
 3 files changed, 105 insertions(+), 32 deletions(-)

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</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>
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 <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,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);
 }
 
 }