diff --git a/ui/src/common/QualityInfo.jsx b/ui/src/common/QualityInfo.jsx index 57a8251a4..c2f3b00d4 100644 --- a/ui/src/common/QualityInfo.jsx +++ b/ui/src/common/QualityInfo.jsx @@ -9,6 +9,24 @@ import { calculateGain } from '../utils/calculateReplayGain' const llFormats = new Set(config.losslessFormats.split(',')) const placeholder = 'N/A' +// Lossless streams have no meaningful bitrate to show, so their quality is +// described by the PCM characteristics instead: `FLAC 96/24`. +const formatSampleRate = (sampleRate) => { + const khz = sampleRate / 1000 + return Number.isInteger(khz) ? String(khz) : khz.toFixed(1) +} + +const losslessDetail = (sampleRate, bitDepth) => { + // DSD carries a 1-bit stream at MHz rates, where `2822.4/1` would say + // nothing useful. Those formats keep showing the container alone until + // modulation gets its own representation. + if (bitDepth === 1 || !(sampleRate > 0)) { + return '' + } + const rate = formatSampleRate(sampleRate) + return bitDepth > 0 ? `${rate}/${bitDepth}` : rate +} + const useStyle = makeStyles( (theme) => ({ chip: { @@ -30,14 +48,27 @@ export const QualityInfo = ({ isDirectPlay, }) => { const classes = useStyle() - let { suffix, bitRate, rgAlbumGain, rgAlbumPeak, rgTrackGain, rgTrackPeak } = - record + let { + suffix, + bitRate, + sampleRate, + bitDepth, + rgAlbumGain, + rgAlbumPeak, + rgTrackGain, + rgTrackPeak, + } = record let info = placeholder if (suffix) { suffix = suffix.toUpperCase() info = suffix - if (!llFormats.has(suffix) && bitRate > 0) { + if (llFormats.has(suffix)) { + const detail = losslessDetail(sampleRate, bitDepth) + if (detail) { + info += ' ' + detail + } + } else if (bitRate > 0) { info += ' ' + bitRate } } diff --git a/ui/src/common/QualityInfo.test.jsx b/ui/src/common/QualityInfo.test.jsx index 174ee8a85..fb9553ad1 100644 --- a/ui/src/common/QualityInfo.test.jsx +++ b/ui/src/common/QualityInfo.test.jsx @@ -5,11 +5,51 @@ import { QualityInfo } from './QualityInfo' describe('', () => { afterEach(cleanup) - it('only render suffix for lossless formats', () => { + it('only renders suffix for lossless formats without stream details', () => { const info = { suffix: 'FLAC', bitRate: 1008 } render() expect(screen.getByText('FLAC')).toBeInTheDocument() }) + it('renders sample rate and bit depth for lossless formats', () => { + const info = { + suffix: 'FLAC', + bitRate: 1008, + sampleRate: 96000, + bitDepth: 24, + } + render() + expect(screen.getByText('FLAC 96/24')).toBeInTheDocument() + }) + it('renders CD quality without a trailing zero', () => { + const info = { suffix: 'FLAC', sampleRate: 44100, bitDepth: 16 } + render() + expect(screen.getByText('FLAC 44.1/16')).toBeInTheDocument() + }) + it('keeps one decimal for rates that are not whole kHz', () => { + const info = { suffix: 'FLAC', sampleRate: 88200, bitDepth: 24 } + render() + expect(screen.getByText('FLAC 88.2/24')).toBeInTheDocument() + }) + it('renders the sample rate alone when bit depth is unknown', () => { + const info = { suffix: 'FLAC', sampleRate: 192000 } + render() + expect(screen.getByText('FLAC 192')).toBeInTheDocument() + }) + it('renders stream details for other lossless containers', () => { + const info = { suffix: 'WAV', sampleRate: 48000, bitDepth: 32 } + render() + expect(screen.getByText('WAV 48/32')).toBeInTheDocument() + }) + it('leaves DSD showing the container alone', () => { + const info = { suffix: 'DSF', sampleRate: 2822400, bitDepth: 1 } + render() + expect(screen.getByText('DSF')).toBeInTheDocument() + }) + it('ignores stream details for lossy formats', () => { + const info = { suffix: 'MP3', bitRate: 320, sampleRate: 44100 } + render() + expect(screen.getByText('MP3 320')).toBeInTheDocument() + }) it('only render suffix and bitrate for lossy formats', () => { const info = { suffix: 'MP3',