Files
SpotiFLAC-Mobile/lib/services/playback_normalization.dart
T
zarzet 3d35058b02 fix(playback): retain ReplayGain for SAF descriptor leases
Carry the original display name through playback leases to format-aware metadata reads. Cache successful normalization results by source identity, bound the cache, and retry transient read failures.

Add regressions for descriptor hints, album gain fallback, and failed-read retries.
2026-09-06 14:04:04 +07:00

48 lines
1.5 KiB
Dart

import 'dart:math';
/// Caches successful tag reads by stable media identity. Descriptor paths are
/// short-lived and require their display-name hint to select the audio parser.
class PlaybackNormalizationCache {
final Future<Map<String, dynamic>> Function(String, {String? displayName})
readMetadata;
final void Function(Object)? onReadError;
final Map<String, double> _volumes = {};
static final _gainNumber = RegExp(r'-?\d+(\.\d+)?');
PlaybackNormalizationCache({required this.readMetadata, this.onReadError});
Future<double> volumeFor(
String path, {
String? cacheKey,
String? displayName,
}) async {
final key = cacheKey ?? path;
final cached = _volumes[key];
if (cached != null) return cached;
try {
final metadata = await readMetadata(path, displayName: displayName);
if (metadata['error'] != null) {
onReadError?.call(metadata['error'] as Object);
return 1.0;
}
final gain =
_gain(metadata['replaygain_track_gain']) ??
_gain(metadata['replaygain_album_gain']);
final volume = gain == null
? 1.0
: pow(10.0, gain / 20.0).toDouble().clamp(0.0, 1.0);
if (_volumes.length >= 128) _volumes.remove(_volumes.keys.first);
_volumes[key] = volume;
return volume;
} catch (error) {
onReadError?.call(error);
return 1.0;
}
}
static double? _gain(Object? value) {
final match = _gainNumber.firstMatch(value?.toString() ?? '');
return match == null ? null : double.tryParse(match.group(0)!);
}
}