Files
SpotiFLAC-Mobile/lib/services/replaygain_service.dart
T
zarzet b8b670642c feat(audio): add WAV and AIFF support + settings-style metadata menu
WAV/AIFF: library scan, quality probe, native tag read/write via embedded ID3 chunk (RIFF id3 / AIFF ID3), cover art, ReadFileMetadata, ExtractLyrics, and FLAC<->WAV/AIFF conversion (PCM, bit-depth preserved via ffprobe). Treat WAV/AIFF as lossless across all convert sheets (no bitrate picker, Lossless labels) via isLosslessConversionTarget. Native MIME maps for SAF. Redesign the track metadata three-dot menu to a settings-style grouped card with a single divider above Share.
2026-06-12 21:10:37 +07:00

109 lines
3.1 KiB
Dart

import 'dart:io';
import 'package:spotiflac_android/services/ffmpeg_service.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/logger.dart';
/// Standalone ReplayGain (re)scanning for existing audio files.
///
/// Computes EBU R128 loudness via FFmpeg and writes REPLAYGAIN_TRACK_* tags
/// back into the file in place:
/// - FLAC / M4A / MP4 / APE / WV / MPC -> native tag writer (PlatformBridge)
/// - MP3 / Opus / OGG / others -> FFmpeg copy-with-metadata
///
/// Handles SAF content:// URIs transparently by working on a temporary copy
/// and writing it back to the original document.
class ReplayGainService {
ReplayGainService._();
static final _log = AppLogger('ReplayGain');
static const _nativeExtensions = <String>{
'.flac',
'.m4a',
'.mp4',
'.m4b',
'.ape',
'.wv',
'.mpc',
'.wav',
'.aiff',
'.aif',
'.aifc',
};
static bool _isNativeWritableFormat(String path) {
final lower = path.toLowerCase();
return _nativeExtensions.any(lower.endsWith);
}
/// Scans [filePath] for loudness and writes track ReplayGain tags in place.
///
/// Returns `true` when tags were successfully written, `false` otherwise
/// (scan failed, write failed, or SAF write-back failed).
static Future<bool> applyToFile(String filePath) async {
if (filePath.isEmpty) return false;
final isSaf = isContentUri(filePath);
var workingPath = filePath;
String? safTempPath;
try {
if (isSaf) {
safTempPath = await PlatformBridge.copyContentUriToTemp(filePath);
if (safTempPath == null || safTempPath.isEmpty) {
_log.w('Failed to copy SAF file to temp for ReplayGain scan');
return false;
}
workingPath = safTempPath;
}
final rg = await FFmpegService.scanReplayGain(workingPath);
if (rg == null) {
_log.w('ReplayGain scan returned no result for $workingPath');
return false;
}
bool written;
if (_isNativeWritableFormat(workingPath)) {
final result = await PlatformBridge.editFileMetadata(workingPath, {
'replaygain_track_gain': rg.trackGain,
'replaygain_track_peak': rg.trackPeak,
});
written = result['error'] == null;
if (!written) {
_log.w('Native ReplayGain write failed: ${result['error']}');
}
} else {
written = await FFmpegService.writeTrackReplayGainTags(
workingPath,
rg.trackGain,
rg.trackPeak,
);
}
if (!written) return false;
if (isSaf) {
final ok = await PlatformBridge.writeTempToSaf(workingPath, filePath);
if (!ok) {
_log.w('Failed to write ReplayGain temp file back to SAF document');
}
return ok;
}
return true;
} catch (e) {
_log.e('Failed to apply ReplayGain', e);
return false;
} finally {
if (safTempPath != null) {
try {
await File(safTempPath).delete();
} catch (_) {}
}
}
}
}