feat(player): ReplayGain/R128 volume normalization for the built-in player

Reads REPLAYGAIN_TRACK_GAIN (album gain fallback) from FLAC/MP3/M4A tags
and converts Opus R128_TRACK_GAIN/R128_ALBUM_GAIN (Q7.8, -23 LUFS) to the
same ReplayGain dB representation on the Go side, then attenuates the
internal player's volume per track. Off by default; toggle lives under
Library > Playback. Opus header output gain is left to the decoder, which
already applies it.

Closes #465
This commit is contained in:
zarzet
2026-07-14 15:18:42 +07:00
parent e37f13275f
commit 9ac10dd6bb
25 changed files with 270 additions and 0 deletions
+6
View File
@@ -20,6 +20,8 @@ class AppSettings {
artistTagMode; // 'joined' or 'split_vorbis' for Vorbis-based formats
final bool embedLyrics;
final bool embedReplayGain;
// Apply ReplayGain/R128 tags as volume normalization in the built-in player.
final bool playbackNormalization;
final bool maxQualityCover;
final bool isFirstLaunch;
final bool checkForUpdates;
@@ -112,6 +114,7 @@ class AppSettings {
this.artistTagMode = artistTagModeJoined,
this.embedLyrics = true,
this.embedReplayGain = false,
this.playbackNormalization = false,
this.maxQualityCover = true,
this.isFirstLaunch = true,
this.checkForUpdates = true,
@@ -179,6 +182,7 @@ class AppSettings {
String? artistTagMode,
bool? embedLyrics,
bool? embedReplayGain,
bool? playbackNormalization,
bool? maxQualityCover,
bool? isFirstLaunch,
bool? checkForUpdates,
@@ -249,6 +253,8 @@ class AppSettings {
artistTagMode: artistTagMode ?? this.artistTagMode,
embedLyrics: embedLyrics ?? this.embedLyrics,
embedReplayGain: embedReplayGain ?? this.embedReplayGain,
playbackNormalization:
playbackNormalization ?? this.playbackNormalization,
maxQualityCover: maxQualityCover ?? this.maxQualityCover,
isFirstLaunch: isFirstLaunch ?? this.isFirstLaunch,
checkForUpdates: checkForUpdates ?? this.checkForUpdates,
+2
View File
@@ -19,6 +19,7 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
artistTagMode: json['artistTagMode'] as String? ?? artistTagModeJoined,
embedLyrics: json['embedLyrics'] as bool? ?? true,
embedReplayGain: json['embedReplayGain'] as bool? ?? false,
playbackNormalization: json['playbackNormalization'] as bool? ?? false,
maxQualityCover: json['maxQualityCover'] as bool? ?? true,
isFirstLaunch: json['isFirstLaunch'] as bool? ?? true,
checkForUpdates: json['checkForUpdates'] as bool? ?? true,
@@ -105,6 +106,7 @@ Map<String, dynamic> _$AppSettingsToJson(
'artistTagMode': instance.artistTagMode,
'embedLyrics': instance.embedLyrics,
'embedReplayGain': instance.embedReplayGain,
'playbackNormalization': instance.playbackNormalization,
'maxQualityCover': instance.maxQualityCover,
'isFirstLaunch': instance.isFirstLaunch,
'checkForUpdates': instance.checkForUpdates,