mirror of
https://github.com/navidrome/navidrome.git
synced 2026-07-30 16:56:22 -04:00
getTranscodeDecision was copying the source file's bit depth into the transcodeStream details, so a 24-bit FLAC negotiated to Opus reported audioBitdepth=24. Lossy codecs (Opus, MP3, AAC) have no PCM bit depth, and ffmpeg only honors a bit depth constraint (-sample_fmt) for lossless outputs, so the value was both meaningless and misleading to clients that use it as a quality indicator. Only set the transcoded stream's bit depth when the target format is lossless; a zero value omits audioBitdepth from the response. This also makes audioBitdepth codec limitations a no-op for lossy targets instead of rejecting the profile. Lossless targets (e.g. FLAC->FLAC downconvert) keep reporting and clamping bit depth as before.
94 lines
3.0 KiB
Go
94 lines
3.0 KiB
Go
package stream
|
|
|
|
import "strings"
|
|
|
|
// normalizeProbeCodec maps ffprobe codec_name values to the simplified internal
|
|
// codec names used throughout Navidrome (matching inferCodecFromSuffix output).
|
|
// Most ffprobe names match directly; this handles the exceptions.
|
|
func normalizeProbeCodec(codec string) string {
|
|
c := strings.ToLower(codec)
|
|
// DSD variants: dsd_lsbf_planar, dsd_msbf_planar, dsd_lsbf, dsd_msbf
|
|
if strings.HasPrefix(c, "dsd") {
|
|
return "dsd"
|
|
}
|
|
// PCM variants: pcm_s16le, pcm_s24le, pcm_s32be, pcm_f32le, etc.
|
|
if strings.HasPrefix(c, "pcm_") {
|
|
return "pcm"
|
|
}
|
|
return c
|
|
}
|
|
|
|
// isLosslessFormat returns true if the format is a known lossless audio codec/format.
|
|
// Detection is based on codec name only, not bit depth — some lossy codecs (e.g. ADPCM)
|
|
// report non-zero bits_per_sample in ffprobe, so bit depth alone is not a reliable signal.
|
|
//
|
|
// Note: core/ffmpeg has a separate isLosslessOutputFormat that covers only formats
|
|
// ffmpeg can produce as output (a smaller set).
|
|
func isLosslessFormat(format string) bool {
|
|
switch strings.ToLower(format) {
|
|
case "flac", "alac", "wav", "aiff", "ape", "wv", "wavpack", "tta", "tak", "shn", "dsd", "pcm":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// normalizeSourceSampleRate adjusts the source sample rate for codecs that store
|
|
// it differently than PCM. Currently handles DSD (÷8):
|
|
// DSD64=2822400→352800, DSD128=5644800→705600, etc.
|
|
// For other codecs, returns the rate unchanged.
|
|
func normalizeSourceSampleRate(sampleRate int, codec string) int {
|
|
if strings.EqualFold(codec, "dsd") && sampleRate > 0 {
|
|
return sampleRate / 8
|
|
}
|
|
return sampleRate
|
|
}
|
|
|
|
// targetBitDepth returns the bit depth for a transcoded stream: 0 for lossy
|
|
// targets (they have no PCM bit depth), otherwise the source depth, with DSD
|
|
// adjusted to the 24-bit PCM that ffmpeg produces.
|
|
func targetBitDepth(srcBitDepth int, srcCodec string, targetIsLossless bool) int {
|
|
if !targetIsLossless {
|
|
return 0
|
|
}
|
|
if strings.EqualFold(srcCodec, "dsd") && srcBitDepth == 1 {
|
|
return 24
|
|
}
|
|
return srcBitDepth
|
|
}
|
|
|
|
// codecFixedOutputSampleRate returns the mandatory output sample rate for codecs
|
|
// that always resample regardless of input (e.g., Opus always outputs 48000Hz).
|
|
// Returns 0 if the codec has no fixed output rate.
|
|
func codecFixedOutputSampleRate(codec string) int {
|
|
switch strings.ToLower(codec) {
|
|
case "opus":
|
|
return 48000
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// codecMaxSampleRate returns the hard maximum output sample rate for a codec.
|
|
// Returns 0 if the codec has no hard limit.
|
|
func codecMaxSampleRate(codec string) int {
|
|
switch strings.ToLower(codec) {
|
|
case "mp3":
|
|
return 48000
|
|
case "aac":
|
|
return 96000
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// codecMaxChannels returns the hard maximum number of audio channels a codec
|
|
// supports. Returns 0 if the codec has no hard limit (or is unknown), in which
|
|
// case the source/profile constraints applied upstream are authoritative.
|
|
func codecMaxChannels(codec string) int {
|
|
switch strings.ToLower(codec) {
|
|
case "mp3":
|
|
return 2
|
|
case "opus":
|
|
return 8
|
|
}
|
|
return 0
|
|
}
|