const List audioConversionTargetFormats = [ 'ALAC', 'FLAC', 'WAV', 'AIFF', 'AAC', 'MP3', 'Opus', ]; const List losslessConversionSampleRateOptions = [ 192000, 96000, 48000, 44100, ]; const List losslessConversionBitDepthOptions = [16, 24]; List availableLosslessBitDepthOptions(int? sourceBitDepth) { if (sourceBitDepth == null || sourceBitDepth <= 0) { return losslessConversionBitDepthOptions; } return losslessConversionBitDepthOptions .where((depth) => depth < sourceBitDepth) .toList(); } List availableLosslessSampleRateOptions(int? sourceSampleRate) { if (sourceSampleRate == null || sourceSampleRate <= 0) { return losslessConversionSampleRateOptions; } return losslessConversionSampleRateOptions .where((rate) => rate < sourceSampleRate) .toList(); } int? lowestKnownPositiveInt(Iterable values) { int? lowest; for (final value in values) { if (value == null || value <= 0) continue; if (lowest == null || value < lowest) { lowest = value; } } return lowest; } class LosslessConversionQuality { final int? maxBitDepth; final int? maxSampleRate; const LosslessConversionQuality({this.maxBitDepth, this.maxSampleRate}); bool get hasCaps => maxBitDepth != null || maxSampleRate != null; int? effectiveBitDepth(int? sourceBitDepth) { if (maxBitDepth == null) return sourceBitDepth; if (sourceBitDepth == null || sourceBitDepth <= 0) return maxBitDepth; return sourceBitDepth > maxBitDepth! ? maxBitDepth : sourceBitDepth; } int? effectiveSampleRate(int? sourceSampleRate) { if (maxSampleRate == null) return sourceSampleRate; if (sourceSampleRate == null || sourceSampleRate <= 0) { return maxSampleRate; } return sourceSampleRate > maxSampleRate! ? maxSampleRate : sourceSampleRate; } } 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 losslessBitDepthLabel(int? bitDepth) { return bitDepth == null ? 'Original' : '$bitDepth-bit'; } String losslessSampleRateLabel(int? sampleRate) { if (sampleRate == null) return 'Original'; final khz = sampleRate / 1000; final precision = sampleRate % 1000 == 0 ? 0 : 1; return '${khz.toStringAsFixed(precision)} kHz'; } String losslessQualityLabel(LosslessConversionQuality quality) { final parts = []; if (quality.maxBitDepth != null) { parts.add(losslessBitDepthLabel(quality.maxBitDepth)); } if (quality.maxSampleRate != null) { parts.add(losslessSampleRateLabel(quality.maxSampleRate)); } return parts.isEmpty ? 'Original quality' : parts.join(' / '); } String convertedAudioQualityLabel({ required String targetFormat, required String bitrate, LosslessConversionQuality losslessQuality = const LosslessConversionQuality(), int? actualBitDepth, int? actualSampleRate, }) { final upper = targetFormat.toUpperCase(); if (isLosslessConversionTarget(targetFormat)) { if (actualBitDepth != null && actualBitDepth > 0 && actualSampleRate != null && actualSampleRate > 0) { return '$upper ${losslessBitDepthLabel(actualBitDepth)}/${losslessSampleRateLabel(actualSampleRate)}'; } if (losslessQuality.hasCaps) { return '$upper ${losslessQualityLabel(losslessQuality)}'; } return '$upper Lossless'; } return '$upper ${bitrate.trim().toLowerCase()}'; } int? readPositiveAudioInt(Object? value) { if (value is num) { final intValue = value.toInt(); return intValue > 0 ? intValue : null; } final parsed = int.tryParse(value?.toString() ?? ''); return parsed != null && parsed > 0 ? parsed : 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; }