From 23d5a2b8620cea69958d087fc7e13fe1e5adb83d Mon Sep 17 00:00:00 2001
From: Desuwa <tszymanski@google.com>
Date: Fri, 18 Sep 2020 03:46:37 -0700
Subject: [PATCH] Support opus header gain tags and match opus playback volume
 to other tracks when ReplayGain is enabled.

---
 NEWS                                      |  1 +
 src/decoder/plugins/OpusDecoderPlugin.cxx | 24 +++++++++++++++++++----
 src/decoder/plugins/OpusHead.cxx          |  5 ++++-
 src/decoder/plugins/OpusHead.hxx          |  2 +-
 src/decoder/plugins/OpusTags.cxx          |  4 ++--
 5 files changed, 28 insertions(+), 8 deletions(-)

diff --git a/NEWS b/NEWS
index aeb7eb5bf..9ece34870 100644
--- a/NEWS
+++ b/NEWS
@@ -13,6 +13,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 5284ac9a1..b1ed9c4e9 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,
 
 static 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 30d959f04..7a1c766a9 100644
--- a/src/decoder/plugins/OpusHead.cxx
+++ b/src/decoder/plugins/OpusHead.cxx
@@ -33,12 +33,15 @@ 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 OpusHead *h = (const OpusHead *)data;
 	if (size < 19 || (h->version & 0xf0) != 0)
 		return false;
 
+	uint16_t gain_bits = FromLE16(h->output_gain);
+	output_gain_r = (gain_bits & 0x8000) ? gain_bits - 0x10000 : gain_bits;
+
 	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 39b5f832a..4fada6459 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 70e452860..3133dcc4d 100644
--- a/src/decoder/plugins/OpusTags.cxx
+++ b/src/decoder/plugins/OpusTags.cxx
@@ -53,7 +53,7 @@ ScanOneOpusTag(const char *name, const char *value,
 		char *endptr;
 		long l = strtol(value, &endptr, 10);
 		if (endptr > value && *endptr == 0)
-			rgi->track.gain = float(l) / 256.0f;
+			rgi->track.gain += float(l) / 256.0f;
 	} else if (rgi != nullptr &&
 		   StringEqualsCaseASCII(name, "R128_ALBUM_GAIN")) {
 		/* R128_ALBUM_GAIN is a Q7.8 fixed point number in
@@ -62,7 +62,7 @@ ScanOneOpusTag(const char *name, const char *value,
 		char *endptr;
 		long l = strtol(value, &endptr, 10);
 		if (endptr > value && *endptr == 0)
-			rgi->album.gain = float(l) / 256.0f;
+			rgi->album.gain += float(l) / 256.0f;
 	}
 
 	handler.OnPair(name, value);