DeaDBeeF-Player / deadbeef

DeaDBeeF Player
https://deadbeef.sourceforge.io/
Other
1.65k stars 178 forks source link

converter: opus plugin writes bogus replaygain tag into files after conversion #2232

Closed spvkgn closed 4 years ago

spvkgn commented 5 years ago

Steps to reproduce the problem

Encode flac to opus using deadbeef converter and opusenc from command line.

What's going on? Describe the problem in as much detail as possible.

When encoding any flac to opus using deadbeef converter, it encodes with -5 dB gain:

$ opusinfo 01.\ The\ Ides\ Of\ March.opus | grep gain
  Playback gain: -5 dB

$ bs1770gain --replaygain 01.\ The\ Ides\ Of\ March.opus 
scanning 1
analyzing ...
[1/1] 01. The Ides Of March.opus
  integrated (momentary mean): -19.23 dBFS / 1.23 dB

When encoding it using opusenc directly, this not occurs:

$ opusenc --quiet --bitrate 128 01.\ The\ Ides\ Of\ March.flac 01.\ The\ Ides\ Of\ March.1.opus

$ opusinfo 01.\ The\ Ides\ Of\ March.1.opus | grep gain
  Playback gain: 0 dB

$ bs1770gain --replaygain 01.\ The\ Ides\ Of\ March.1.opus 
scanning 1
analyzing ...
[1/1] 01. The Ides Of March.1.opus
  integrated (momentary mean): -14.23 dBFS / -3.77 dB

I guess, encoding using converter should have same result as using opusenc from command line.

Information about the software:

Deadbeef version: devel OS: Ubuntu 18.04 opus-tools 0.1.10 (using libopus 1.3.1)

Oleksiy-Yakovenko commented 5 years ago

deadbeef is literally using opusenc from command line. my only guess is that maybe replaygain got applied during conversion, which is not supposed to happen, but wrong code might have sneaked in by accident.

Oleksiy-Yakovenko commented 5 years ago

I just checked this theory, and no - replaygain not applied.

spvkgn commented 5 years ago

In my case this is not a theory - I have this with any file, even with generated sine wave:

$ ffmpeg -loglevel error -f lavfi -i "sine=frequency=1000:duration=30" -filter_complex "[0:a][0:a]amerge=inputs=2[aout]" -map "[aout]" output.wav

$ bs1770gain --replaygain output.wav 
scanning 1
analyzing ...
[1/1] output.wav
  integrated (momentary mean): -18.06 dBFS / 0.06 dB

$ deadbeef --queue output.wav 
starting deadbeef devel
...
Encoding using libopus 1.3.1 (audio)
-----------------------------------------------------
   Input: 44.1kHz 2 channels
  Output: 2 channels (2 coupled)
          20ms packets, 128kbit/sec VBR
 Preskip: 312

Encoding complete                            
-----------------------------------------------------
       Encoded: 30.02 seconds
       Runtime: 1e-06 seconds
                (3.002e+07x realtime)
         Wrote: 736734 bytes, 1501 packets, 33 pages
       Bitrate: 194.889kbit/s (without overhead)
 Instant rates: 168.4kbit/s to 253.6kbit/s
                (421 to 634 bytes per packet)
      Overhead: 0.735% (container+metadata)
…
hej-hej!

$ opusinfo output.opus | grep gain
    Playback gain: -5 dB

$ bs1770gain --replaygain output.opus 
scanning 1
analyzing ...
[1/1] output.opus
  integrated (momentary mean): -23.04 dBFS / 5.04 dB

Well, if it does not depend on deadbeef , I close this.

Oleksiy-Yakovenko commented 5 years ago

It's weird that you're getting different results between opusenc as used by deadbeef, and opusenc used outside of deadbeef. Command lines are identical, and Playback gain is not something deadbeef is putting in there -- it's an opusenc feature.

spvkgn commented 5 years ago

I've just found the cause and it seems that it depends on vorbis comments written with tagger. If encode without tag writing, then it writes OPUS_HEADER_GAIN with 5 dB - gain is 0 dB as it should be. Otherwise it writes REPLAYGAIN_ALBUMGAIN and OPUS_HEADER_GAIN with 0 dB both (even if source not contains REPLAYGAIN tags), in which case gain is -5 dB: Снимок экрана в 2019-10-10 15-22-30

$ opusinfo output.0dB.opus | grep gain
    Playback gain: 0 dB
$ bs1770gain --replaygain output.0dB.opus
scanning 1
analyzing ...
[1/1] output.0dB.opus
  integrated (momentary mean): -18.04 dBFS / 0.04 dB

Снимок экрана в 2019-10-10 15-22-54

$ opusinfo output.-5dB.opus | grep gain
    Playback gain: -5 dB
$ bs1770gain --replaygain output.-5dB.opus 
scanning 1
analyzing ...
[1/1] output.-5dB.opus
  integrated (momentary mean): -23.04 dBFS / 5.04 dB

If I understand correctly opus header should not contain REPLAYGAIN_* comments to avoid confusion - they replaced with EBU-R128 standard as described here https://tools.ietf.org/html/draft-ietf-codec-oggopus-06#section-5.2.1

Oleksiy-Yakovenko commented 5 years ago

Otherwise it writes REPLAYGAIN_ALBUMGAIN and OPUS_HEADER_GAIN with 0 dB both (even if source not contains REPLAYGAIN tags), in which case gain is -5 dB:

This looks like a bug, and I think this is easy enough to fix. Meanwhile you can use a workaround, which is to delete the bogus replaygain tag from the files.

Oleksiy-Yakovenko commented 5 years ago

If I understand correctly opus header should not contain REPLAYGAIN_* comments

I don't think there's anybody on earth who fully understands opus header gain, and how to make this stuff work universally well across various implementations.

Lithopsian commented 5 years ago

I'll go out on a limb and claim I understand opus header gain. As to making it work universally well across all implementations, just not possible. Opus reinvented the wheel with their own approach to replaygain - you either use it or you get into a big mess. If you use it, you can still get in a mess because so many implementations don't use it exactly as specified.

Amongst other things, the Opus spec says not to use the REPLAYGAIN tags that are almost universally recognised by music players, and instead to use R128GAIN tags specific to Opus. In addition it defines the infamous "output gain" field in its header which is not a tag as such, but without really saying when it should be used or what it should be used for. The one thing it does say, extremely strongly, is that every decoder must apply the header gain to any decoded audio, which is impossible given that most music players include an option to ignore any replay gain values. Checkmate. Hence, many music players still add the REPLAYGAIN* tags to Opus files, and many still read them. Whether this works as intended is a bit random depending on whether the encoder and decoder make the same assumptions.

Oleksiy-Yakovenko commented 5 years ago

@Lithopsian well this is kind of how I understand it as well. Thanks for the detailed write up :)

Lithopsian commented 5 years ago

Now, the bug. Deadbeef runs opus enc and then it may update the resulting file with tags. If it doesn't update the file then any gain tags created by opusenc will be left. opusenc parses any flac replaygain values, converts to the R128 equivalent, and writes them to the encoded file. I believe that it still writes the album gain into the header gain field and writes the difference between that and the track gain into an R128_TRACK_GAIN tag. As you can imagine, this is not very helpful in the context of Deadbeef.

If Deadbeef is configured to write tags after encoding, then the internal Deadbeef Opus plugin writes an R128_TRACK_GAIN tag containing the track gain value, and R128_ALBUM_GAIN tag containing the album gain value, and a header gain field containing the track gain less 5. That last piece just looks flat wrong to me. I don't know if it is intended to be a conversion from the internal replaygain values (normalised to 89dB, ironically converted from an actual R128 value derived in the replaygain scanner plugin) to the R128 values, or if it is a holdover from something that used to put the album gain into the header. Either way, not right. Probably it should just write zero into the header and write what it really means into the R128 tags.

ToadKing commented 5 years ago

According to spec, R128_TRACK_GAIN and R128_ALBUM_GAIN are gain values added on top of the header gain. From how I interpret it, it seems like if you omit R128_ALBUM_GAIN, then it assumes the value in the header alone is enough for it, while R128_TRACK_GAIN is applied on top of that for track gain. This doesn't allow for "no gain" (unless you ignore the header gain but I think that's out-of-spec) but because for Opus gain can be applied during decoding, the concept of "no gain" doesn't really have any advantages for playback IMO.

Lithopsian commented 5 years ago

The (Opus encapsulation) specification states that replaygain applied by decoders MUST (their caps, not mine) be calculated by adding the R128_TRACK_GAIN or R128_ALBUM_GAIN to the output gain from the header. That is all you can know for sure. You can't make any assumptions about how much gain of what type is in the header or in either of the two tags, only that the sum of either tag present and the header gain field is the corresponding replaygain value. Earlier versions of the spec did not include the R128_ALBUM_GAIN tag and stated the assumption that when needed it would simply be placed in the header gain, leaving no possibility of knowing whether some or all of the header gain was in fact an album gain, or a track gain, or something else entirely. The R128_ALBUM_GAIN tag was defined specifically to allow the assumption (although not the guarantee, it can still be done the "old" way) that the value it contains is an album replaygain and that anything in the header constitutes some other form of gain. Even getting that to happen was like pulling teeth: Opus was envisaged as "always just working" without having to interpret or misinterpret mysterious metadata, with the header gain allowing for always-applied gain control. The idea that different decoders might want to decode at different levels was not part of the plan.

Deadbeef, in common with many "high-end" music players, has options to play back audio with either track or album replaygain applied, or not. You can argue all day about whether you should want to do not apply replaygain or even whether it has any meaning, the preferences exist so we should try to support them in some form. However Opus makes it near-impossible to determine what the playback level should be "without replaygain", because there is no way to know whether some or all of any replaygain is in the header. Assumptions must be made.

The specification states that the output gain in the header SHOULD be applied by all decoders. Although decoders could ignore this, it is a bad idea since it could contain large gain values unrelated to replaygain. If the assumptions made by the decoder do not match the assumptions made by the encoder then strangeness may happen. Note that the spec also states that the output gain SHOULD be set to zero when audio is encoded, so one possible assumption is that replaygain is only contained in the R128_TRACK_GAIN or R128_ALBUM_GAIN tags, and that anything in the header is something else although that starts to look dubious when the R128_ALBUM_GAIN tag is missing. Helpful practice by encoders is to write both R128_TRACK_GAIN and R128_ALBUM_GAIN tags even if they are zero, to give players a clue that replaygain has been calculated and found to be zero (again no guarantees, there might still be replaygain in the header, however meaningless that distinction might be).

Lithopsian commented 5 years ago

As purely an implementation detail, Deadbeef runs into difficulties if we allow the Opus decoder library to automatically calculate and apply replaygain, or even if we just give it simple instructions like "use album gain". When I first wrote the third-party Opus plugin, the streamer could not easily be persuaded not to apply its own replaygain in line with user preferences although I think this is now better supported. Also, peak capping and preamp settings cannot easily be controlled if replaygain has already been applied to the decoded audio before the streamer gets it (Opus does not officially support any peak tags, but Deadbeef assumes full scale or peak=1 in this case), and there would be several seconds delay for any new playback preferences to be applied to the audio. So just at the code level we really need to decode "without replaygain" and then work it all out ourselves.

Oleksiy-Yakovenko commented 4 years ago

It seems like a chicken-egg problem.

There are the following things I came up with, which can help with the problem:

Oleksiy-Yakovenko commented 4 years ago

I think that the main confusion of this issue comes from the fact that opusinfo itself doesn't support the replaygain format used in deadbeef. I'm fairly sure that both deadbeef and foobar2k will be able to handle each other's RG tags successfully. I have tested this when developing RG support for opus plugin, but will try to re-test ASAP.

Oleksiy-Yakovenko commented 4 years ago

I just verified with foobar, and both tag writing and reading works identical to deadbeef. Closing since this is technically not a bug, but intended behavior. If anybody is interested in getting a "opus spec conformant replaygain support" -- please post another issue.

Lithopsian commented 4 years ago

I'm building a brand new clean copy of Deadbeef and I'll look into this some more. I thought I had a good version, but I'm getting some errors trying to write the replaygain data, so the results are confusing.

spvkgn commented 4 years ago

@Alexey-Yakovenko

I just verified with foobar, and both tag writing and reading works identical to deadbeef.

I don't see this behavior with foobar2k (just tried under wine) - foobar doesn't write bogus REPLAYGAIN_ALBUMGAIN as deadbeef does.

I'd better attach zip file with samples: test sine wav file and both resulting opus files - from foobar and from deadbeef. Everyone can make sure that these opus files are not identical in gain. output.zip

Снимок экрана в 2019-11-22 18-13-37

Oleksiy-Yakovenko commented 4 years ago

When writing the last comment and closing, I confused myself, and thought that we're talking about RG-scanner. The issue is about how "default opus header" is written though. It shouldn't contain any RG info, therefore the issue is still valid.

Lithopsian commented 4 years ago

In the Deadbeef track properties dialog, the REPLAYGAIN_TRACKGAIN value shown is the sum of the header gain value any R128_TRACK_GAIN tag, adjusted by 5dB (approximately from -23 LUFS to 89dB loudness), but it is only shown if there is a replaygain tag. The OPUS_HEADER_GAIN is always shown, whether there is an R128_TRACK_GAIN tag or not, adjusted by 5dB. If the header gain value is zero, then "5.00 dB" is shown for OPUS_HEADER_GAIN in the dialog, although this is not used for anything. REPLAYGAIN_ALBUMGAIN is shown when the sum of any R128_ALBUM_GAIN tag and the header gain is other than zero (before being adjusted by 5dB), even if no R128_ALBUM_GAIN exists. This produces some odd results when there is supposedly no replaygain data, for example after removing replaygain information using the Deadbeef command REPLAYGAIN_ALBUMGAIN still appears as 0.00 dB.

If the track gain metadata key exists internally then an R128_TRACK_GAIN tag is written with the value zero, otherwise no tag is written. The gain value written to the header is the track gain minus 5. This is all intentional so far (I think), although I'd suggest not the best solution (putting replaygain in tags seems like a better idea, so players can see what is replaygain and what isn't). However, if the track gain key does not exist (ie. no replaygain data internally) then the header gain is still set to -5dB, which I think is the issue being described? In such cases, no gain tags will be written, the track info dialog will show OPUS_HEADER_GAIN of 0.00 dB (-5dB adjusted by 5dB), no REPLAYGAIN_TRACKGAIN value, but a REPLAYGAIN_ALBUMGAIN value of 0.00 dB. Playback "with gain" will be correct (I think?), but playback without gain ("none" option) will not be at the original volume because the header gain is now -5dB.

Confused yet? I am.