Files
SpotiFLAC-Mobile/lib/utils/audio_conversion_utils.dart
T
zarzet ff86869c33 feat: audio analysis rescan and AAC conversion support
Audio Analysis:
- Add rescan capability by bumping cache version
- Display channel layout (stereo, 5.1, etc.) and bitrate
- Use astats filter for more accurate peak/RMS measurements
- Support more formats: mp4, ac3, eac3, mka, wv, ape, tta, aif
- Only report bit depth for codecs that store it (FLAC, ALAC, WAV)
- Validate cache for SAF content:// URIs

Conversion:
- Add AAC as conversion target format
- Recognize ALAC as lossless source
- Prevent accidental deletion when source and target URI match
- Store format and bitrate in database after conversion

Utilities:
- Add audio_conversion_utils.dart for centralized conversion logic
- Add isSameContentUri() helper for safe URI comparison
2026-05-14 15:46:55 +07:00

106 lines
2.4 KiB
Dart

const List<String> audioConversionTargetFormats = [
'ALAC',
'FLAC',
'AAC',
'MP3',
'Opus',
];
bool isLosslessConversionTarget(String targetFormat) {
final normalized = targetFormat.trim().toLowerCase();
return normalized == 'alac' || normalized == 'flac';
}
bool isLosslessConversionSource(String sourceFormat) {
switch (sourceFormat.trim().toUpperCase()) {
case 'FLAC':
case 'ALAC':
case 'M4A':
return true;
default:
return false;
}
}
bool canConvertAudioFormat({
required String sourceFormat,
required String targetFormat,
}) {
if (sourceFormat.trim().toUpperCase() == targetFormat.trim().toUpperCase()) {
return false;
}
if (isLosslessConversionTarget(targetFormat) &&
!isLosslessConversionSource(sourceFormat)) {
return false;
}
return true;
}
String? convertibleAudioSourceFormat({
String? storedFormat,
String? filePath,
String? fileName,
}) {
final fromStored = _convertibleAudioFormatLabel(storedFormat);
if (fromStored != null) return fromStored;
final name = (fileName != null && fileName.trim().isNotEmpty)
? fileName
: filePath;
if (name == null || name.trim().isEmpty) return null;
final normalizedName = name.trim().toLowerCase();
final dotIndex = normalizedName.lastIndexOf('.');
if (dotIndex < 0 || dotIndex == normalizedName.length - 1) {
return null;
}
return _convertibleAudioFormatLabel(normalizedName.substring(dotIndex + 1));
}
String? _convertibleAudioFormatLabel(String? rawFormat) {
final format = rawFormat?.trim().toLowerCase();
if (format == null || format.isEmpty) return null;
switch (format) {
case 'flac':
return 'FLAC';
case 'alac':
return 'ALAC';
case 'm4a':
case 'mp4':
return 'M4A';
case 'aac':
case 'mp4a':
return 'AAC';
case 'mp3':
return 'MP3';
case 'opus':
case 'ogg':
return 'Opus';
case 'eac3':
case 'ec-3':
return 'EAC3';
case 'ac3':
case 'ac-3':
return 'AC3';
case 'ac4':
case 'ac-4':
return 'AC4';
default:
return null;
}
}
String normalizedConvertedAudioFormat(String targetFormat) {
return targetFormat.trim().toLowerCase();
}
int? convertedAudioBitrateKbps({
required String targetFormat,
required String bitrate,
}) {
if (isLosslessConversionTarget(targetFormat)) return null;
final match = RegExp(r'(\d+)').firstMatch(bitrate);
return match != null ? int.tryParse(match.group(1)!) : null;
}