diff --git a/lib/services/ffmpeg_service.dart b/lib/services/ffmpeg_service.dart index e6763c5f..640dfb11 100644 --- a/lib/services/ffmpeg_service.dart +++ b/lib/services/ffmpeg_service.dart @@ -595,7 +595,7 @@ class FFmpegService { if (!hasSampleRate && !hasSampleFormat && !processing.hasDither) return; final options = [ - 'resampler=${processing.normalizedResampler}', + ...losslessResamplerFilterOptions(processing), if (hasSampleRate) 'osr=$targetSampleRate', if (hasSampleFormat) 'osf=${outputSampleFormat.trim()}', if (processing.hasDither) 'dither_method=${processing.normalizedDither}', diff --git a/lib/utils/audio_conversion_utils.dart b/lib/utils/audio_conversion_utils.dart index 5f3d934c..79192b7d 100644 --- a/lib/utils/audio_conversion_utils.dart +++ b/lib/utils/audio_conversion_utils.dart @@ -27,6 +27,9 @@ const List losslessDitherOptions = [ const List losslessResamplerOptions = ['swr', 'soxr']; +const int soxrVhqPrecisionBits = 28; +const double soxrPassbandEndRatio = 0.95; + class LosslessConversionProcessing { final String dither; final String resampler; @@ -49,6 +52,19 @@ class LosslessConversionProcessing { bool get hasDither => normalizedDither != 'none'; } +List losslessResamplerFilterOptions( + LosslessConversionProcessing processing, +) { + final resampler = processing.normalizedResampler; + return [ + 'resampler=$resampler', + if (resampler == 'soxr') ...[ + 'precision=$soxrVhqPrecisionBits', + 'cutoff=$soxrPassbandEndRatio', + ], + ]; +} + List availableLosslessBitDepthOptions(int? sourceBitDepth) { if (sourceBitDepth == null || sourceBitDepth <= 0) { return losslessConversionBitDepthOptions; diff --git a/test/audio_conversion_utils_test.dart b/test/audio_conversion_utils_test.dart index a53480c0..b9275f5f 100644 --- a/test/audio_conversion_utils_test.dart +++ b/test/audio_conversion_utils_test.dart @@ -44,6 +44,18 @@ void main() { expect(swrHighPass.normalizedDither, 'triangular_hp'); expect(swrHighPass.hasDither, isTrue); }); + + test('uses VHQ and a 95 percent passband only for SoXR', () { + const soxr = LosslessConversionProcessing(resampler: 'soxr'); + const swr = LosslessConversionProcessing(resampler: 'swr'); + + expect(losslessResamplerFilterOptions(soxr), [ + 'resampler=soxr', + 'precision=28', + 'cutoff=0.95', + ]); + expect(losslessResamplerFilterOptions(swr), ['resampler=swr']); + }); }); group('kept conversion output identity', () {