feat(replaygain): add Opus R128 gain tags

Write R128_TRACK_GAIN/R128_ALBUM_GAIN (Q7.8, ref -23 LUFS) alongside the existing REPLAYGAIN_* tags for .opus files so spec-compliant Opus players (RFC 7845) apply gain. Covers per-track and album embed during download (incl. SAF) and standalone re-scan.
This commit is contained in:
zarzet
2026-06-20 02:24:50 +07:00
parent 1489378ffd
commit ef52332b8b
2 changed files with 52 additions and 6 deletions
@@ -4860,6 +4860,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
scannedReplayGain = rgResult;
metadata['REPLAYGAIN_TRACK_GAIN'] = rgResult.trackGain;
metadata['REPLAYGAIN_TRACK_PEAK'] = rgResult.trackPeak;
if (format == 'opus') {
final r128 = FFmpegService.replayGainDbToR128(rgResult.trackGain);
if (r128 != null) metadata['R128_TRACK_GAIN'] = r128;
}
_log.d(
'ReplayGain for $format: gain=${rgResult.trackGain}, peak=${rgResult.trackPeak}',
);
+48 -6
View File
@@ -1240,8 +1240,22 @@ class FFmpegService {
);
}
/// Write album ReplayGain tags to a non-FLAC file (MP3/Opus) using FFmpeg.
/// Preserves all existing metadata and adds/overwrites album gain fields.
/// Convert a ReplayGain gain value (dB, referenced to -18 LUFS) into an Opus
/// R128 gain tag value (Q7.8 fixed point integer, referenced to -23 LUFS).
///
/// Opus players read `R128_TRACK_GAIN` / `R128_ALBUM_GAIN` per RFC 7845, not
/// the `REPLAYGAIN_*` dB strings. The reference levels differ by exactly 5 dB
/// (-18 vs -23 LUFS), so the R128 gain equals the ReplayGain value minus 5 dB,
/// stored as `round(dB * 256)`.
static String? replayGainDbToR128(String replayGainDb) {
final match = RegExp(r'-?\d+\.?\d*').firstMatch(replayGainDb);
if (match == null) return null;
final rgDb = double.tryParse(match.group(0) ?? '');
if (rgDb == null) return null;
final r128Db = rgDb - 5.0;
return (r128Db * 256).round().toString();
}
/// Write album ReplayGain tags to a file via FFmpeg.
///
/// For local files, replaces the file in-place and returns `true`.
@@ -1277,10 +1291,21 @@ class FFmpegService {
'REPLAYGAIN_ALBUM_GAIN=$albumGain',
'-metadata',
'REPLAYGAIN_ALBUM_PEAK=$albumPeak',
tempOutput,
'-y',
];
if (ext.toLowerCase() == '.opus') {
final r128 = replayGainDbToR128(albumGain);
if (r128 != null) {
arguments
..add('-metadata')
..add('R128_ALBUM_GAIN=$r128');
}
}
arguments
..add(tempOutput)
..add('-y');
_log.d('Writing album ReplayGain tags via FFmpeg');
final result = await _executeWithArguments(arguments);
@@ -1347,10 +1372,21 @@ class FFmpegService {
'REPLAYGAIN_TRACK_GAIN=$trackGain',
'-metadata',
'REPLAYGAIN_TRACK_PEAK=$trackPeak',
tempOutput,
'-y',
];
if (ext.toLowerCase() == '.opus') {
final r128 = replayGainDbToR128(trackGain);
if (r128 != null) {
arguments
..add('-metadata')
..add('R128_TRACK_GAIN=$r128');
}
}
arguments
..add(tempOutput)
..add('-y');
_log.d('Writing track ReplayGain tags via FFmpeg');
final result = await _executeWithArguments(arguments);
@@ -2725,6 +2761,12 @@ class FFmpegService {
case 'REPLAYGAINALBUMPEAK':
vorbis['REPLAYGAIN_ALBUM_PEAK'] = value;
break;
case 'R128TRACKGAIN':
vorbis['R128_TRACK_GAIN'] = value;
break;
case 'R128ALBUMGAIN':
vorbis['R128_ALBUM_GAIN'] = value;
break;
}
}