Files
SpotiFLAC-Mobile/lib/utils/audio_conversion_utils.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

144 lines
3.5 KiB
Dart

const List<String> audioConversionTargetFormats = [
'ALAC',
'FLAC',
'WAV',
'AIFF',
'AAC',
'MP3',
'Opus',
];
bool isLosslessConversionTarget(String targetFormat) {
final normalized = targetFormat.trim().toLowerCase();
return normalized == 'alac' ||
normalized == 'flac' ||
normalized == 'wav' ||
normalized == 'aiff' ||
normalized == 'aif';
}
bool isLosslessConversionSource(String sourceFormat) {
switch (sourceFormat.trim().toUpperCase()) {
case 'FLAC':
case 'ALAC':
case 'M4A':
case 'WAV':
case 'AIFF':
case 'AIF':
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 'wav':
case 'wave':
return 'WAV';
case 'aiff':
case 'aif':
case 'aifc':
return 'AIFF';
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();
}
/// Returns the output file extension (with dot) and MIME type for a conversion
/// target format. Used when creating the converted file via SAF so WAV/AIFF and
/// the other formats get the correct extension + MIME.
({String ext, String mime}) convertTargetExtAndMime(String targetFormat) {
switch (targetFormat.trim().toLowerCase()) {
case 'opus':
return (ext: '.opus', mime: 'audio/opus');
case 'alac':
case 'aac':
return (ext: '.m4a', mime: 'audio/mp4');
case 'flac':
return (ext: '.flac', mime: 'audio/flac');
case 'wav':
return (ext: '.wav', mime: 'audio/wav');
case 'aiff':
case 'aif':
return (ext: '.aiff', mime: 'audio/aiff');
default:
return (ext: '.mp3', mime: 'audio/mpeg');
}
}
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;
}