diff --git a/lib/models/settings.dart b/lib/models/settings.dart index dc27dcb7..df26f0b3 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -7,6 +7,7 @@ part 'settings.g.dart'; class AppSettings { static const String homeFeedProviderOff = '__off__'; static const String libraryQualityLabelBitrate = 'bitrate'; + static const String libraryQualityLabelBitDepthOnly = 'bit_depth_only'; static const String libraryQualityLabelBitDepth = 'bit_depth'; static const String libraryQualityLabelBitDepthBitrate = 'bit_depth_bitrate'; @@ -41,8 +42,8 @@ class AppSettings { /// 'last' (keep last used), 'all', 'albums', 'singles', or 'playlists'. final String defaultLibraryView; - /// Library badge text: measured bitrate, bit depth/sample rate, or the - /// combined bit depth/bitrate label. + /// Library badge text: measured bitrate, bit depth, bit depth/sample rate, + /// or the combined bit depth/bitrate label. final String libraryQualityLabelMode; final bool askQualityBeforeDownload; final bool enableLogging; diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 7d78c54c..90c9cae3 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -381,6 +381,8 @@ class SettingsNotifier extends Notifier { String _normalizeLibraryQualityLabelMode(String value) { return switch (value) { + AppSettings.libraryQualityLabelBitDepthOnly => + AppSettings.libraryQualityLabelBitDepthOnly, AppSettings.libraryQualityLabelBitDepth => AppSettings.libraryQualityLabelBitDepth, AppSettings.libraryQualityLabelBitDepthBitrate => diff --git a/lib/screens/settings/library_settings_page.dart b/lib/screens/settings/library_settings_page.dart index 0610ab94..b91a2f87 100644 --- a/lib/screens/settings/library_settings_page.dart +++ b/lib/screens/settings/library_settings_page.dart @@ -435,6 +435,9 @@ class _LibrarySettingsPageState extends ConsumerState { } String _getQualityLabelModeLabel(BuildContext context, String mode) { + if (mode == AppSettings.libraryQualityLabelBitDepthOnly) { + return context.l10n.audioAnalysisBitDepth; + } if (mode == AppSettings.libraryQualityLabelBitDepth) { return '${context.l10n.audioAnalysisBitDepth} & ' '${context.l10n.audioAnalysisSampleRate}'; @@ -454,6 +457,11 @@ class _LibrarySettingsPageState extends ConsumerState { Icons.speed_rounded, context.l10n.trackConvertBitrate, ), + ( + AppSettings.libraryQualityLabelBitDepthOnly, + Icons.tune_rounded, + context.l10n.audioAnalysisBitDepth, + ), ( AppSettings.libraryQualityLabelBitDepth, Icons.graphic_eq_rounded, diff --git a/lib/utils/audio_quality_badge_policy.dart b/lib/utils/audio_quality_badge_policy.dart index 86228c48..c7380280 100644 --- a/lib/utils/audio_quality_badge_policy.dart +++ b/lib/utils/audio_quality_badge_policy.dart @@ -11,6 +11,8 @@ final RegExp _bitDepthQualityPattern = RegExp( String normalizeLibraryQualityLabelMode(String? mode) { return switch (mode) { + AppSettings.libraryQualityLabelBitDepthOnly => + AppSettings.libraryQualityLabelBitDepthOnly, AppSettings.libraryQualityLabelBitDepth => AppSettings.libraryQualityLabelBitDepth, AppSettings.libraryQualityLabelBitDepthBitrate => @@ -32,9 +34,13 @@ String? buildLibraryAudioQualityLabel({ }) { final stored = normalizeOptionalString(storedQuality); final storedBitrate = _bitrateFromStoredQuality(stored); + final storedBitDepth = _bitDepthFromStoredQuality(stored); final effectiveBitrate = bitrateKbps != null && bitrateKbps > 0 ? bitrateKbps : storedBitrate; + final effectiveBitDepth = bitDepth != null && bitDepth > 0 + ? bitDepth + : storedBitDepth; final bitrateLabel = effectiveBitrate != null ? buildDisplayAudioQuality(bitrateKbps: effectiveBitrate, format: format) : null; @@ -42,6 +48,9 @@ String? buildLibraryAudioQualityLabel({ bitDepth != null && bitDepth > 0 && sampleRate != null && sampleRate > 0 ? buildDisplayAudioQuality(bitDepth: bitDepth, sampleRate: sampleRate) : null; + final bitDepthOnlyLabel = effectiveBitDepth != null + ? '$effectiveBitDepth-bit' + : null; final normalizedMode = normalizeLibraryQualityLabelMode(mode); if (isLossyAudioFormat(format)) { @@ -49,11 +58,13 @@ String? buildLibraryAudioQualityLabel({ } return switch (normalizedMode) { + AppSettings.libraryQualityLabelBitDepthOnly => + bitDepthOnlyLabel ?? bitrateLabel ?? stored, AppSettings.libraryQualityLabelBitDepth => bitDepthLabel ?? bitrateLabel ?? stored, AppSettings.libraryQualityLabelBitDepthBitrate => _buildBitDepthBitrateLabel( - bitDepth: bitDepth, + bitDepth: effectiveBitDepth, bitrateKbps: effectiveBitrate, ) ?? bitrateLabel ?? @@ -77,6 +88,15 @@ int? _bitrateFromStoredQuality(String? quality) { return unit == 'mbps' ? (value * 1000).round() : value.round(); } +int? _bitDepthFromStoredQuality(String? quality) { + final match = RegExp( + r'\b(\d{1,3})(?:\s*[- ]?\s*bit\b|(?=/))', + caseSensitive: false, + ).firstMatch(quality ?? ''); + final value = int.tryParse(match?.group(1) ?? ''); + return value != null && value > 0 ? value : null; +} + String? _buildBitDepthBitrateLabel({int? bitDepth, int? bitrateKbps}) { if (bitDepth == null || bitDepth <= 0 || diff --git a/test/audio_quality_badge_policy_test.dart b/test/audio_quality_badge_policy_test.dart index b64d1040..0f0c59c5 100644 --- a/test/audio_quality_badge_policy_test.dart +++ b/test/audio_quality_badge_policy_test.dart @@ -46,6 +46,33 @@ void main() { ), AppSettings.libraryQualityLabelBitDepthBitrate, ); + expect( + normalizeLibraryQualityLabelMode( + AppSettings.libraryQualityLabelBitDepthOnly, + ), + AppSettings.libraryQualityLabelBitDepthOnly, + ); + }); + + test('shows only bit depth when requested', () { + expect( + buildLibraryAudioQualityLabel( + mode: AppSettings.libraryQualityLabelBitDepthOnly, + format: 'flac', + bitrateKbps: 1411, + bitDepth: 16, + sampleRate: 44100, + ), + '16-bit', + ); + expect( + buildLibraryAudioQualityLabel( + mode: AppSettings.libraryQualityLabelBitDepthOnly, + format: 'flac', + storedQuality: '24-bit/96kHz', + ), + '24-bit', + ); }); test('restores legacy bit depth and sample rate labels', () { diff --git a/test/models_and_utils_test.dart b/test/models_and_utils_test.dart index 72733495..b8750486 100644 --- a/test/models_and_utils_test.dart +++ b/test/models_and_utils_test.dart @@ -767,7 +767,7 @@ void main() { lyricsAppleElrcWordSync: true, deduplicateDownloads: false, allowQualityVariants: true, - libraryQualityLabelMode: AppSettings.libraryQualityLabelBitDepthBitrate, + libraryQualityLabelMode: AppSettings.libraryQualityLabelBitDepthOnly, clearDownloadFallbackExtensionIds: true, clearSearchProvider: true, clearHomeFeedProvider: true, @@ -781,7 +781,7 @@ void main() { expect(updated.allowQualityVariants, isTrue); expect( updated.libraryQualityLabelMode, - AppSettings.libraryQualityLabelBitDepthBitrate, + AppSettings.libraryQualityLabelBitDepthOnly, ); expect(updated.downloadFallbackExtensionIds, isNull); expect(updated.searchProvider, isNull); @@ -812,7 +812,7 @@ void main() { autoConvertDownloads: true, autoConvertFormat: 'opus', autoConvertBitrate: '192k', - libraryQualityLabelMode: AppSettings.libraryQualityLabelBitDepthBitrate, + libraryQualityLabelMode: AppSettings.libraryQualityLabelBitDepthOnly, ); final decoded = AppSettings.fromJson(settings.toJson()); @@ -834,7 +834,7 @@ void main() { expect(decoded.lastSeenVersion, '4.5.0'); expect( decoded.libraryQualityLabelMode, - AppSettings.libraryQualityLabelBitDepthBitrate, + AppSettings.libraryQualityLabelBitDepthOnly, ); expect(decoded.deduplicateDownloads, isFalse); expect(decoded.allowQualityVariants, isTrue);