diff --git a/NEWS b/NEWS index 19df7e53e..da37166ec 100644 --- a/NEWS +++ b/NEWS @@ -54,6 +54,7 @@ ver 0.21.26 (not yet released) * decoder - ffmpeg: remove "rtsp://" from the list of supported protocols - ffmpeg: add "hls+http://" to the list of supported protocols + - opus: support the gain value from the Opus header - sndfile: fix lost samples at end of file * fix "single" mode bug after resuming playback * the default log_level is "default", not "info" diff --git a/src/decoder/plugins/OpusDecoderPlugin.cxx b/src/decoder/plugins/OpusDecoderPlugin.cxx index ba5a2bff4..a0cf02ba7 100644 --- a/src/decoder/plugins/OpusDecoderPlugin.cxx +++ b/src/decoder/plugins/OpusDecoderPlugin.cxx @@ -75,6 +75,12 @@ class MPDOpusDecoder final : public OggDecoder { OpusDecoder *opus_decoder = nullptr; opus_int16 *output_buffer = nullptr; + /** + * The output gain from the Opus header. Initialized by + * OnOggBeginning(). + */ + signed output_gain; + /** * The pre-skip value from the Opus header. Initialized by * OnOggBeginning(). @@ -164,7 +170,7 @@ MPDOpusDecoder::OnOggBeginning(const ogg_packet &packet) throw std::runtime_error("BOS packet must be OpusHead"); unsigned channels; - if (!ScanOpusHeader(packet.packet, packet.bytes, channels, pre_skip) || + if (!ScanOpusHeader(packet.packet, packet.bytes, channels, output_gain, pre_skip) || !audio_valid_channel_count(channels)) throw std::runtime_error("Malformed BOS packet"); @@ -239,6 +245,15 @@ MPDOpusDecoder::HandleTags(const ogg_packet &packet) ReplayGainInfo rgi; rgi.Clear(); + /** + * Output gain is a Q7.8 fixed point number in dB that should be, + * applied unconditionally, but is often used specifically for + * ReplayGain. Add 5dB to compensate for the different + * reference levels between ReplayGain (89dB) and EBU R128 (-23 LUFS). + */ + rgi.track.gain = float(output_gain) / 256.0f + 5; + rgi.album.gain = float(output_gain) / 256.0f + 5; + TagBuilder tag_builder; AddTagHandler h(tag_builder); @@ -384,14 +399,14 @@ mpd_opus_stream_decode(DecoderClient &client, bool ReadAndParseOpusHead(OggSyncState &sync, OggStreamState &stream, - unsigned &channels, unsigned &pre_skip) + unsigned &channels, signed &output_gain, unsigned &pre_skip) { ogg_packet packet; return OggReadPacket(sync, stream, packet) && packet.b_o_s && IsOpusHead(packet) && ScanOpusHeader(packet.packet, packet.bytes, channels, - pre_skip) && + output_gain, pre_skip) && audio_valid_channel_count(channels); } @@ -436,7 +451,8 @@ mpd_opus_scan_stream(InputStream &is, TagHandler &handler) OggStreamState os(first_page); unsigned channels, pre_skip; - if (!ReadAndParseOpusHead(oy, os, channels, pre_skip) || + signed output_gain; + if (!ReadAndParseOpusHead(oy, os, channels, output_gain, pre_skip) || !ReadAndVisitOpusTags(oy, os, handler)) return false; diff --git a/src/decoder/plugins/OpusHead.cxx b/src/decoder/plugins/OpusHead.cxx index 596ef186a..eb7edf121 100644 --- a/src/decoder/plugins/OpusHead.cxx +++ b/src/decoder/plugins/OpusHead.cxx @@ -33,12 +33,14 @@ struct OpusHead { bool ScanOpusHeader(const void *data, size_t size, unsigned &channels_r, - unsigned &pre_skip_r) + signed &output_gain_r, unsigned &pre_skip_r) { const auto *h = (const OpusHead *)data; if (size < 19 || (h->version & 0xf0) != 0) return false; + output_gain_r = FromLE16S(h->output_gain); + channels_r = h->channels; pre_skip_r = FromLE16(h->pre_skip); return true; diff --git a/src/decoder/plugins/OpusHead.hxx b/src/decoder/plugins/OpusHead.hxx index d140cee21..cbacf0c48 100644 --- a/src/decoder/plugins/OpusHead.hxx +++ b/src/decoder/plugins/OpusHead.hxx @@ -24,6 +24,6 @@ bool ScanOpusHeader(const void *data, size_t size, unsigned &channels_r, - unsigned &pre_skip_r); + signed &output_gain_r, unsigned &pre_skip_r); #endif diff --git a/src/decoder/plugins/OpusTags.cxx b/src/decoder/plugins/OpusTags.cxx index ee1d0cb22..c78586065 100644 --- a/src/decoder/plugins/OpusTags.cxx +++ b/src/decoder/plugins/OpusTags.cxx @@ -61,7 +61,7 @@ ScanOneOpusTag(StringView name, StringView value, const char *endptr; const auto l = ParseInt64(value, &endptr, 10); if (endptr > value.begin() && endptr == value.end()) - rgi->track.gain = float(l) / 256.0f; + rgi->track.gain += float(l) / 256.0f; } else if (rgi != nullptr && name.EqualsIgnoreCase("R128_ALBUM_GAIN")) { /* R128_ALBUM_GAIN is a Q7.8 fixed point number in @@ -70,7 +70,7 @@ ScanOneOpusTag(StringView name, StringView value, const char *endptr; const auto l = ParseInt64(value, &endptr, 10); if (endptr > value.begin() && endptr == value.end()) - rgi->album.gain = float(l) / 256.0f; + rgi->album.gain += float(l) / 256.0f; } handler.OnPair(name, value); diff --git a/src/pcm/Mix.cxx b/src/pcm/Mix.cxx index ed2b0f8db..9126f60f6 100644 --- a/src/pcm/Mix.cxx +++ b/src/pcm/Mix.cxx @@ -27,6 +27,7 @@ #include "Dither.cxx" // including the .cxx file to get inlined templates #include +#include template> static typename Traits::value_type diff --git a/src/util/ByteOrder.hxx b/src/util/ByteOrder.hxx index 2c145a262..48222854e 100644 --- a/src/util/ByteOrder.hxx +++ b/src/util/ByteOrder.hxx @@ -243,4 +243,15 @@ ToLE64(uint64_t value) noexcept return IsLittleEndian() ? value : ByteSwap64(value); } +/** + * Converts a 16 bit integer from little endian to the host byte order + * and returns it as a signed integer. + */ +constexpr int16_t +FromLE16S(uint16_t value) noexcept +{ + /* assuming two's complement representation */ + return static_cast(FromLE16(value)); +} + #endif