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