diff --git a/lib/screens/track_metadata_convert.dart b/lib/screens/track_metadata_convert.dart index 2457b07b..4ef41bc8 100644 --- a/lib/screens/track_metadata_convert.dart +++ b/lib/screens/track_metadata_convert.dart @@ -220,26 +220,14 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { final currentFormat = _currentFileFormat; final isLosslessSource = isLosslessConversionSource(currentFormat); - final formats = []; - if (currentFormat == 'FLAC') { - formats.addAll(['ALAC', 'WAV', 'AIFF', 'AAC', 'MP3', 'Opus']); - } else if (currentFormat == 'ALAC') { - formats.addAll(['FLAC', 'WAV', 'AIFF', 'AAC', 'MP3', 'Opus']); - } else if (currentFormat == 'M4A') { - formats.addAll(['ALAC', 'FLAC', 'WAV', 'AIFF', 'AAC', 'MP3', 'Opus']); - } else if (currentFormat == 'WAV') { - formats.addAll(['FLAC', 'ALAC', 'AIFF', 'AAC', 'MP3', 'Opus']); - } else if (currentFormat == 'AIFF') { - formats.addAll(['FLAC', 'ALAC', 'WAV', 'AAC', 'MP3', 'Opus']); - } else if (currentFormat == 'AAC') { - formats.addAll(['MP3', 'Opus']); - } else if (currentFormat == 'MP3') { - formats.addAll(['AAC', 'Opus']); - } else if (currentFormat == 'Opus') { - formats.addAll(['AAC', 'MP3']); - } else { - formats.addAll(['AAC', 'MP3', 'Opus']); - } + final formats = audioConversionTargetFormats + .where( + (target) => canConvertAudioFormat( + sourceFormat: currentFormat, + targetFormat: target, + ), + ) + .toList(growable: false); final labels = context.l10n.losslessConversionLabels; @@ -922,9 +910,7 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { newPath, ); if (convertedMetadata['error'] == null) { - convertedBitDepth = readPositiveInt( - convertedMetadata['bit_depth'], - ); + convertedBitDepth = readPositiveInt(convertedMetadata['bit_depth']); convertedSampleRate = readPositiveInt( convertedMetadata['sample_rate'], ); @@ -1134,5 +1120,4 @@ extension _TrackMetadataConvertAndCueSplit on _TrackMetadataScreenState { } } } - } diff --git a/lib/utils/audio_conversion_utils.dart b/lib/utils/audio_conversion_utils.dart index 170eacd0..cf15d916 100644 --- a/lib/utils/audio_conversion_utils.dart +++ b/lib/utils/audio_conversion_utils.dart @@ -128,8 +128,13 @@ bool canConvertAudioFormat({ required String sourceFormat, required String targetFormat, }) { - if (sourceFormat.trim().toUpperCase() == targetFormat.trim().toUpperCase()) { - return false; + final normalizedSource = sourceFormat.trim().toUpperCase(); + final normalizedTarget = targetFormat.trim().toUpperCase(); + if (normalizedSource == normalizedTarget) { + // Same-format lossless re-encoding is useful for reducing bit depth or + // sample rate while selecting the dither and resampler implementation. + return isLosslessConversionSource(normalizedSource) && + isLosslessConversionTarget(normalizedTarget); } if (isLosslessConversionTarget(targetFormat) && !isLosslessConversionSource(sourceFormat)) { diff --git a/test/audio_conversion_utils_test.dart b/test/audio_conversion_utils_test.dart new file mode 100644 index 00000000..61835d91 --- /dev/null +++ b/test/audio_conversion_utils_test.dart @@ -0,0 +1,48 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:spotiflac_android/utils/audio_conversion_utils.dart'; + +void main() { + group('same-format lossless conversion', () { + test('allows FLAC re-encoding for quality processing', () { + expect( + canConvertAudioFormat(sourceFormat: 'FLAC', targetFormat: 'FLAC'), + isTrue, + ); + expect( + canConvertAudioFormat(sourceFormat: 'flac', targetFormat: ' flac '), + isTrue, + ); + }); + + test('keeps same-format lossy conversion disabled', () { + expect( + canConvertAudioFormat(sourceFormat: 'MP3', targetFormat: 'MP3'), + isFalse, + ); + expect( + canConvertAudioFormat(sourceFormat: 'Opus', targetFormat: 'Opus'), + isFalse, + ); + }); + }); + + group('lossless processing options', () { + test('preserves both resamplers and both dither methods', () { + const soxrTpdf = LosslessConversionProcessing( + dither: 'triangular', + resampler: 'soxr', + ); + const swrHighPass = LosslessConversionProcessing( + dither: 'triangular_hp', + resampler: 'swr', + ); + + expect(soxrTpdf.normalizedResampler, 'soxr'); + expect(soxrTpdf.normalizedDither, 'triangular'); + expect(soxrTpdf.hasDither, isTrue); + expect(swrHighPass.normalizedResampler, 'swr'); + expect(swrHighPass.normalizedDither, 'triangular_hp'); + expect(swrHighPass.hasDither, isTrue); + }); + }); +}