mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 02:17:25 +02:00
feat(ui): show sample rate and bit depth for lossless tracks
The Quality column appends the bitrate for lossy formats, so `MP3 320` tells you what you are listening to. Lossless formats have no meaningful bitrate to append, so they all render as a bare `FLAC`, and a CD rip is indistinguishable from a 96/24 remaster sitting next to it in the list. Append the PCM stream characteristics instead, giving `FLAC 96/24`. Sample rate is shown in kHz without a trailing zero, so 44100 reads as `44.1` and 96000 as `96`. When the bit depth is unknown the rate is shown on its own, and when neither is known the format is rendered exactly as before. DSD is left showing the container alone: it carries a 1-bit stream at MHz rates, where `2822.4/1` would say nothing useful. Modulation is still an open question in #1438 and wants its own representation. Part of #1438.
This commit is contained in:
parent
3da2b590e7
commit
c4e805d049
2 changed files with 75 additions and 4 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,51 @@ import { QualityInfo } from './QualityInfo'
|
|||
describe('<QualityInfo />', () => {
|
||||
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(<QualityInfo record={info} />)
|
||||
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(<QualityInfo record={info} />)
|
||||
expect(screen.getByText('FLAC 96/24')).toBeInTheDocument()
|
||||
})
|
||||
it('renders CD quality without a trailing zero', () => {
|
||||
const info = { suffix: 'FLAC', sampleRate: 44100, bitDepth: 16 }
|
||||
render(<QualityInfo record={info} />)
|
||||
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(<QualityInfo record={info} />)
|
||||
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(<QualityInfo record={info} />)
|
||||
expect(screen.getByText('FLAC 192')).toBeInTheDocument()
|
||||
})
|
||||
it('renders stream details for other lossless containers', () => {
|
||||
const info = { suffix: 'WAV', sampleRate: 48000, bitDepth: 32 }
|
||||
render(<QualityInfo record={info} />)
|
||||
expect(screen.getByText('WAV 48/32')).toBeInTheDocument()
|
||||
})
|
||||
it('leaves DSD showing the container alone', () => {
|
||||
const info = { suffix: 'DSF', sampleRate: 2822400, bitDepth: 1 }
|
||||
render(<QualityInfo record={info} />)
|
||||
expect(screen.getByText('DSF')).toBeInTheDocument()
|
||||
})
|
||||
it('ignores stream details for lossy formats', () => {
|
||||
const info = { suffix: 'MP3', bitRate: 320, sampleRate: 44100 }
|
||||
render(<QualityInfo record={info} />)
|
||||
expect(screen.getByText('MP3 320')).toBeInTheDocument()
|
||||
})
|
||||
it('only render suffix and bitrate for lossy formats', () => {
|
||||
const info = {
|
||||
suffix: 'MP3',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue