Skip to content

Commit

Permalink
fix: correct bitrate calculation for user-uploaded content
Browse files Browse the repository at this point in the history
Calculate bitrate from decoder parameters instead of file size to avoid
inflated values from ID3 tags and album art in user-uploaded MP3s.
Cap bitrates to codec maximums (320 kbps for MP3, 1411 for FLAC, etc).
  • Loading branch information
roderickvd committed Jan 16, 2025
1 parent c83b6eb commit 901d6ee
Show file tree
Hide file tree
Showing 2 changed files with 65 additions and 38 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ and [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
- [player] Default to mono audio for podcasts to prevent garbled sound when channel count is missing
- [track] Return `AudioFile` instead of raw download stream

### Fixed
- [track] Correct bitrate calculation for user-uploaded MP3s by excluding ID3 tags and album art
- [track] Cap reported bitrates to codec maximums (320 kbps for MP3, 1411 kbps for FLAC, etc.)

## [0.8.1] - 2025-01-11

### Added
Expand Down
99 changes: 61 additions & 38 deletions src/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,62 @@ impl Track {
)))
}

fn init_download(&mut self, url: &Url) {
// Determine the codec and bitrate of the track.
if let Some(ExternalUrl::WithQuality(urls)) = &self.external_url {
// Livestreams specify the codec and bitrate with the URL.
let result = find_codec_bitrate(urls, url);
self.codec = result.map(|some| some.0);
self.bitrate = result.map(|some| some.1);
} else {
// For episodes, we can infer the codec from the URL.
if let Some(ExternalUrl::Direct(url)) = &self.external_url {
if let Some(extension) = url.path().split('.').last() {
if let Ok(codec) = extension.parse() {
self.codec = Some(codec);
}
}
} else if self.is_user_uploaded() {
self.codec = Some(Codec::MP3);
} else {
self.codec = self.quality.codec();
}

// For songs, the audio quality determines the codec. When the codec
// is MP3, the bitrate is constant and determined by the quality. For
// FLAC, the bitrate is variable and determined by the file size and
// duration.
//
// For episodes, we have no metadata and must rely on the file size
// and duration to determine the bitrate. This is not perfect, but it
// is a good approximation.
self.bitrate = match self.quality {
AudioQuality::Lossless | AudioQuality::Unknown => {
self.file_size
.unwrap_or_default()
.checked_div(self.duration.unwrap_or_default().as_secs())
.map(|bytes| {
let mut kbps = usize::try_from(bytes * 8 / 1000).unwrap_or(usize::MAX);

// Limit the bitrate to the maximum allowed by the quality.
// This is to prevent the bitrate from being too high due to
// metadata and visuals in the file.
let max_bitrate = match self.codec() {
Some(Codec::ADTS | Codec::MP4) => 576,
Some(Codec::MP3) => 320,
Some(Codec::FLAC) => 1411,
Some(Codec::WAV) => 3072,
None => usize::MAX,
};
kbps = kbps.min(max_bitrate);
kbps
})
}
_ => self.quality.bitrate(),
};
}
}

/// Starts downloading the track.
///
/// Initiates background download and creates `AudioFile` that:
Expand Down Expand Up @@ -1031,42 +1087,7 @@ impl Track {
info!("downloading {} {self} with unknown file size", self.typ);
}

// Determine the codec and bitrate of the track.
if let Some(ExternalUrl::WithQuality(urls)) = &self.external_url {
// Livestreams specify the codec and bitrate with the URL.
let result = find_codec_bitrate(urls, &url);
self.codec = result.map(|some| some.0);
self.bitrate = result.map(|some| some.1);
} else {
// For episodes, we can infer the codec from the URL.
if let Some(ExternalUrl::Direct(url)) = &self.external_url {
if let Some(extension) = url.path().split('.').last() {
if let Ok(codec) = extension.parse() {
self.codec = Some(codec);
}
}
} else {
self.codec = self.quality.codec();
}

// For songs, the audio quality determines the codec. When the codec
// is MP3, the bitrate is constant and determined by the quality. For
// FLAC, the bitrate is variable and determined by the file size and
// duration.
//
// For episodes, we have no metadata and must rely on the file size
// and duration to determine the bitrate. This is not perfect, but it
// is a good approximation.
self.bitrate = match self.quality {
AudioQuality::Lossless | AudioQuality::Unknown => self
.file_size
.unwrap_or_default()
.checked_div(self.duration.unwrap_or_default().as_secs())
.map(|bytes| usize::try_from(bytes * 8 / 1000).unwrap_or(usize::MAX)),

_ => self.quality.bitrate(),
};
}
self.init_download(&url);

// Calculate the prefetch size based on the bitrate and duration.
let prefetch_size = self.prefetch_size();
Expand Down Expand Up @@ -1222,9 +1243,11 @@ impl Track {
/// Returns the audio codec used for this content.
///
/// Possible codecs:
/// * MP3 - Most common, used for all content types
/// * ADTS - Some livestreams
/// * FLAC - High quality songs only
/// * AAC - Some livestreams and episodes
/// * MP3 - Most common, used for all content types
/// * MP4 - Some episodes
/// * WAV - Some episodes
#[must_use]
#[inline]
pub fn codec(&self) -> Option<Codec> {
Expand Down

0 comments on commit 901d6ee

Please sign in to comment.