mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-28 23:08:59 +02:00
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:
@@ -1079,6 +1079,21 @@ func parseVorbisComments(data []byte, metadata *AudioMetadata) {
|
||||
metadata.ReplayGainAlbumGain = value
|
||||
case "REPLAYGAIN_ALBUM_PEAK":
|
||||
metadata.ReplayGainAlbumPeak = value
|
||||
// Opus gain tags (RFC 7845): Q7.8 fixed point on the R128 -23 LUFS
|
||||
// reference. Exposed as ReplayGain 2 dB (-18 LUFS reference) so
|
||||
// consumers see one representation; explicit REPLAYGAIN_* wins.
|
||||
case "R128_TRACK_GAIN":
|
||||
if metadata.ReplayGainTrackGain == "" {
|
||||
if db, ok := r128ToReplayGainDb(value); ok {
|
||||
metadata.ReplayGainTrackGain = db
|
||||
}
|
||||
}
|
||||
case "R128_ALBUM_GAIN":
|
||||
if metadata.ReplayGainAlbumGain == "" {
|
||||
if db, ok := r128ToReplayGainDb(value); ok {
|
||||
metadata.ReplayGainAlbumGain = db
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1090,6 +1105,17 @@ func parseVorbisComments(data []byte, metadata *AudioMetadata) {
|
||||
}
|
||||
}
|
||||
|
||||
// r128ToReplayGainDb converts an R128_*_GAIN value (integer, 1/256 dB steps,
|
||||
// -23 LUFS reference) to a ReplayGain 2 dB string (-18 LUFS reference):
|
||||
// rg = q/256 + 5. Inverse of the writer's replayGainDbToR128.
|
||||
func r128ToReplayGainDb(raw string) (string, bool) {
|
||||
q, err := strconv.Atoi(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return fmt.Sprintf("%.2f dB", float64(q)/256.0+5.0), true
|
||||
}
|
||||
|
||||
func GetOggQuality(filePath string) (*OggQuality, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -509,3 +509,16 @@ func buildTestFLACPictureBlock(image []byte, mime string) []byte {
|
||||
picture.Write(image)
|
||||
return picture.Bytes()
|
||||
}
|
||||
|
||||
func TestR128ToReplayGainDb(t *testing.T) {
|
||||
// -1280/256 = -5 dB on the R128 (-23 LUFS) scale -> 0 dB ReplayGain (-18).
|
||||
if db, ok := r128ToReplayGainDb("-1280"); !ok || db != "0.00 dB" {
|
||||
t.Fatalf("got %q ok=%v", db, ok)
|
||||
}
|
||||
if db, ok := r128ToReplayGainDb(" -2944 "); !ok || db != "-6.50 dB" {
|
||||
t.Fatalf("got %q ok=%v", db, ok)
|
||||
}
|
||||
if _, ok := r128ToReplayGainDb("abc"); ok {
|
||||
t.Fatal("expected failure for non-numeric input")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3926,6 +3926,18 @@ abstract class AppLocalizations {
|
||||
/// **'Cover art extracted from local music files. Will re-extract on next scan.'**
|
||||
String get cacheLibraryCoverDesc;
|
||||
|
||||
/// Toggle title for ReplayGain playback normalization in the built-in player
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Volume normalization'**
|
||||
String get libraryPlaybackNormalization;
|
||||
|
||||
/// Subtitle explaining playback volume normalization
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Even out loudness between tracks using their ReplayGain or R128 tags, when present'**
|
||||
String get libraryPlaybackNormalizationSubtitle;
|
||||
|
||||
/// Cache item title for saved audio analysis results
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -2192,6 +2192,13 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2220,6 +2220,13 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover aus lokalen Musikdateien extrahiert. Wird beim nächsten Scannen neu extrahiert.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2192,6 +2192,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2192,6 +2192,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2250,6 +2250,13 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Pochettes extraites des fichiers musicaux locaux. Elles seront extraites à nouveau lors de la prochaine analyse.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2192,6 +2192,13 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2201,6 +2201,13 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2181,6 +2181,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2130,6 +2130,13 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'로컬 음악 파일에서 표지 이미지를 추출하였습니다. 다음 스캔 시 다시 추출합니다';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2192,6 +2192,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2192,6 +2192,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2221,6 +2221,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Обложка извлечена из локальных музыкальных файлов. Будет повторно извлечено при следующем сканировании.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2218,6 +2218,13 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Kapak resmi yerel müzik dosyalarından çıkarıldı. Bir sonraki taramada yeniden çıkarılacaktır.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2225,6 +2225,13 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Обкладинку витягнуто з локальних музичних файлів. Буде повторно витягнуто під час наступного сканування.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2192,6 +2192,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get cacheLibraryCoverDesc =>
|
||||
'Cover art extracted from local music files. Will re-extract on next scan.';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalization => 'Volume normalization';
|
||||
|
||||
@override
|
||||
String get libraryPlaybackNormalizationSubtitle =>
|
||||
'Even out loudness between tracks using their ReplayGain or R128 tags, when present';
|
||||
|
||||
@override
|
||||
String get cacheAudioAnalysis => 'Audio analysis cache';
|
||||
|
||||
|
||||
@@ -2868,6 +2868,14 @@
|
||||
"@cacheLibraryCoverDesc": {
|
||||
"description": "Description of what library cover cache contains"
|
||||
},
|
||||
"libraryPlaybackNormalization": "Volume normalization",
|
||||
"@libraryPlaybackNormalization": {
|
||||
"description": "Toggle title for ReplayGain playback normalization in the built-in player"
|
||||
},
|
||||
"libraryPlaybackNormalizationSubtitle": "Even out loudness between tracks using their ReplayGain or R128 tags, when present",
|
||||
"@libraryPlaybackNormalizationSubtitle": {
|
||||
"description": "Subtitle explaining playback volume normalization"
|
||||
},
|
||||
"cacheAudioAnalysis": "Audio analysis cache",
|
||||
"@cacheAudioAnalysis": {
|
||||
"description": "Cache item title for saved audio analysis results"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -395,6 +395,11 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setPlaybackNormalization(bool enabled) {
|
||||
state = state.copyWith(playbackNormalization: enabled);
|
||||
_saveSettings();
|
||||
}
|
||||
|
||||
void setEmbedMetadata(bool enabled) {
|
||||
state = state.copyWith(embedMetadata: enabled);
|
||||
_saveSettings();
|
||||
|
||||
@@ -72,6 +72,9 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
unknownTitle: l10n.unknownTitle,
|
||||
unknownArtist: l10n.unknownArtist,
|
||||
);
|
||||
setPlaybackNormalizationEnabled(
|
||||
ref.read(settingsProvider).playbackNormalization,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -484,6 +487,12 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen(settingsProvider.select((s) => s.playbackNormalization), (
|
||||
_,
|
||||
enabled,
|
||||
) {
|
||||
setPlaybackNormalizationEnabled(enabled);
|
||||
});
|
||||
final queueState = ref.watch(
|
||||
downloadQueueProvider.select((s) => s.queuedCount),
|
||||
);
|
||||
|
||||
@@ -622,6 +622,15 @@ class _LibrarySettingsPageState extends ConsumerState<LibrarySettingsPage> {
|
||||
onTap: () => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setPlayerMode('internal'),
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.graphic_eq,
|
||||
title: context.l10n.libraryPlaybackNormalization,
|
||||
subtitle: context.l10n.libraryPlaybackNormalizationSubtitle,
|
||||
value: settings.playbackNormalization,
|
||||
onChanged: (v) => ref
|
||||
.read(settingsProvider.notifier)
|
||||
.setPlaybackNormalization(v),
|
||||
showDivider: false,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -21,6 +21,17 @@ void updateMusicPlayerStrings({
|
||||
_playbackUnknownArtist = unknownArtist;
|
||||
}
|
||||
|
||||
bool _playbackNormalizationEnabled = false;
|
||||
MusicPlayerHandler? _activeMusicPlayerHandler;
|
||||
|
||||
/// Enables/disables ReplayGain volume normalization and re-applies it to the
|
||||
/// track currently playing.
|
||||
void setPlaybackNormalizationEnabled(bool enabled) {
|
||||
if (_playbackNormalizationEnabled == enabled) return;
|
||||
_playbackNormalizationEnabled = enabled;
|
||||
_activeMusicPlayerHandler?.reapplyNormalization();
|
||||
}
|
||||
|
||||
final AudioContext _musicAudioContext = AudioContext(
|
||||
android: const AudioContextAndroid(
|
||||
audioFocus: AndroidAudioFocus.none,
|
||||
@@ -100,6 +111,7 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
static const int _maxResolvedPathCacheEntries = 64;
|
||||
|
||||
MusicPlayerHandler() {
|
||||
_activeMusicPlayerHandler = this;
|
||||
_init();
|
||||
}
|
||||
|
||||
@@ -285,6 +297,64 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
playbackState.add(playbackState.value.copyWith(updatePosition: position));
|
||||
}
|
||||
|
||||
// ReplayGain normalization: resolved path -> volume multiplier.
|
||||
final Map<String, double> _normalizationVolumeCache = {};
|
||||
|
||||
/// Volume multiplier from the file's ReplayGain/R128 tags (track gain,
|
||||
/// album gain fallback; Opus R128 tags are converted to ReplayGain dB by
|
||||
/// the Go reader). 1.0 when disabled, untagged, or unreadable. Positive
|
||||
/// gains clamp at 1.0 — setVolume can only attenuate.
|
||||
Future<double> _normalizationVolumeFor(String path) async {
|
||||
if (!_playbackNormalizationEnabled) return 1.0;
|
||||
final cached = _normalizationVolumeCache[path];
|
||||
if (cached != null) return cached;
|
||||
|
||||
var volume = 1.0;
|
||||
try {
|
||||
final metadata = await PlatformBridge.readFileMetadata(path);
|
||||
final gainDb =
|
||||
_parseGainDb(metadata['replaygain_track_gain']) ??
|
||||
_parseGainDb(metadata['replaygain_album_gain']);
|
||||
if (gainDb != null) {
|
||||
volume = pow(10.0, gainDb / 20.0).toDouble().clamp(0.0, 1.0);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w('Failed to read gain tags for normalization: $e');
|
||||
}
|
||||
if (_normalizationVolumeCache.length > 128) {
|
||||
_normalizationVolumeCache.clear();
|
||||
}
|
||||
_normalizationVolumeCache[path] = volume;
|
||||
return volume;
|
||||
}
|
||||
|
||||
static double? _parseGainDb(Object? raw) {
|
||||
final text = raw?.toString();
|
||||
if (text == null || text.isEmpty) return null;
|
||||
final match = RegExp(r'-?\d+(\.\d+)?').firstMatch(text);
|
||||
return match == null ? null : double.tryParse(match.group(0)!);
|
||||
}
|
||||
|
||||
/// Re-applies normalization to the playing track when the setting flips.
|
||||
void reapplyNormalization() {
|
||||
final index = _index;
|
||||
if (index < 0 || index >= _media.length) return;
|
||||
unawaited(() async {
|
||||
final media = _media[index];
|
||||
final resolved = media.isContentUri
|
||||
? _resolvedPathCache[media.source]
|
||||
: media.source;
|
||||
if (resolved == null) return;
|
||||
final volume = await _normalizationVolumeFor(resolved);
|
||||
if (_index != index) return;
|
||||
try {
|
||||
await _player.setVolume(volume);
|
||||
} catch (e) {
|
||||
_log.w('Failed to apply normalization volume: $e');
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
Future<String?> _resolveSource(PlayableMedia media) async {
|
||||
if (!media.isContentUri) return media.source;
|
||||
|
||||
@@ -444,9 +514,14 @@ class MusicPlayerHandler extends BaseAudioHandler
|
||||
|
||||
_switchingTrack = true;
|
||||
try {
|
||||
// Set before play() so the track never starts at the wrong loudness;
|
||||
// always set (1.0 when disabled/untagged) so a previous track's
|
||||
// attenuation can't leak into the next one.
|
||||
final normalizationVolume = await _normalizationVolumeFor(resolved);
|
||||
await _player.setAudioContext(_musicAudioContext);
|
||||
await _activateAudioSession();
|
||||
await _player.stop();
|
||||
await _player.setVolume(normalizationVolume);
|
||||
await _player.play(DeviceFileSource(resolved));
|
||||
mediaItem.add(media.toMediaItem(resolvedSource: resolved));
|
||||
_broadcastPosition(Duration.zero, force: true);
|
||||
|
||||
Reference in New Issue
Block a user