WAV: Improve audio bitrate calculation

This commit is contained in:
Serial 2024-05-07 15:03:46 -04:00 committed by Alex
parent d067933be3
commit f340ebf39b
3 changed files with 113 additions and 68 deletions

View file

@ -48,11 +48,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
``` ```
- Renamed `Popularimeter` -> `PopularimeterFrame` - Renamed `Popularimeter` -> `PopularimeterFrame`
- Renamed `SynchronizedText` -> `SynchronizedTextFrame` - Renamed `SynchronizedText` -> `SynchronizedTextFrame`
- **MP4**: Bitrate calculation is now more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/398))
### Fixed ### Fixed
- **ID3v2**: Disallow 4 character TXXX/WXXX frame descriptions from being converted to `ItemKey` ([issue](https://github.com/Serial-ATA/lofty-rs/issues/309)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/394)) - **ID3v2**: Disallow 4 character TXXX/WXXX frame descriptions from being converted to `ItemKey` ([issue](https://github.com/Serial-ATA/lofty-rs/issues/309)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/394))
- **MPEG**: Durations estimated by bitrate are more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/395)) - **MPEG**: Durations estimated by bitrate are more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/395))
- **MP4**: Bitrate calculation is now more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/398))
- **WAV**: Bitrate calculation is now more accurate ([PR](https://github.com/Serial-ATA/lofty-rs/pull/399))
## [0.19.2] - 2024-04-26 ## [0.19.2] - 2024-04-26

View file

@ -106,95 +106,147 @@ impl WavProperties {
} }
} }
#[derive(Copy, Clone, Debug)]
struct ExtensibleFmtChunk {
valid_bits_per_sample: u16,
channel_mask: ChannelMask,
}
#[derive(Copy, Clone, Debug)]
struct FmtChunk {
format_tag: u16,
channels: u8,
sample_rate: u32,
bytes_per_second: u32,
block_align: u16,
bits_per_sample: u16,
extensible_info: Option<ExtensibleFmtChunk>,
}
fn read_fmt_chunk<R>(reader: &mut R, len: usize) -> Result<FmtChunk>
where
R: ReadBytesExt,
{
let format_tag = reader.read_u16::<LittleEndian>()?;
let channels = reader.read_u16::<LittleEndian>()?;
let sample_rate = reader.read_u32::<LittleEndian>()?;
let bytes_per_second = reader.read_u32::<LittleEndian>()?;
let block_align = reader.read_u16::<LittleEndian>()?;
let bits_per_sample = reader.read_u16::<LittleEndian>()?;
let mut fmt_chunk = FmtChunk {
format_tag,
channels: channels as u8,
sample_rate,
bytes_per_second,
block_align,
bits_per_sample,
extensible_info: None,
};
if format_tag == EXTENSIBLE {
if len < 40 {
decode_err!(@BAIL Wav, "Extensible format identified, invalid \"fmt \" chunk size found (< 40)");
}
// cbSize (Size of extra format information) (2)
let _cb_size = reader.read_u16::<LittleEndian>()?;
// Valid bits per sample (2)
let valid_bits_per_sample = reader.read_u16::<LittleEndian>()?;
// Channel mask (4)
let channel_mask = ChannelMask(reader.read_u32::<LittleEndian>()?);
fmt_chunk.format_tag = reader.read_u16::<LittleEndian>()?;
fmt_chunk.extensible_info = Some(ExtensibleFmtChunk {
valid_bits_per_sample,
channel_mask,
});
}
Ok(fmt_chunk)
}
pub(super) fn read_properties( pub(super) fn read_properties(
fmt: &mut &[u8], fmt: &mut &[u8],
mut total_samples: u32, mut total_samples: u32,
stream_len: u32, stream_len: u32,
file_length: u64, file_length: u64,
) -> Result<WavProperties> { ) -> Result<WavProperties> {
let mut format_tag = fmt.read_u16::<LittleEndian>()?; if fmt.len() < 16 {
let channels = fmt.read_u16::<LittleEndian>()? as u8; decode_err!(@BAIL Wav, "File does not contain a valid \"fmt \" chunk");
}
if stream_len == 0 {
decode_err!(@BAIL Wav, "File does not contain a \"data\" chunk");
}
let FmtChunk {
format_tag,
channels,
sample_rate,
bytes_per_second,
block_align,
bits_per_sample,
extensible_info,
} = read_fmt_chunk(fmt, fmt.len())?;
if channels == 0 { if channels == 0 {
decode_err!(@BAIL Wav, "File contains 0 channels"); decode_err!(@BAIL Wav, "File contains 0 channels");
} }
let sample_rate = fmt.read_u32::<LittleEndian>()?;
let bytes_per_second = fmt.read_u32::<LittleEndian>()?;
let block_align = fmt.read_u16::<LittleEndian>()?;
let bits_per_sample = fmt.read_u16::<LittleEndian>()?;
let bytes_per_sample = block_align / u16::from(channels); let bytes_per_sample = block_align / u16::from(channels);
let bit_depth;
let mut bit_depth = if bits_per_sample > 0 { match extensible_info {
bits_per_sample as u8 Some(ExtensibleFmtChunk {
} else { valid_bits_per_sample,
(bytes_per_sample * 8) as u8 ..
}) if valid_bits_per_sample > 0 => bit_depth = valid_bits_per_sample as u8,
_ if bits_per_sample > 0 => bit_depth = bits_per_sample as u8,
_ => bit_depth = (bytes_per_sample * 8) as u8,
}; };
let channel_mask; let channel_mask = extensible_info.map(|info| info.channel_mask);
if format_tag == EXTENSIBLE {
if fmt.len() + 16 < 40 {
decode_err!(@BAIL Wav, "Extensible format identified, invalid \"fmt \" chunk size found (< 40)");
}
// cbSize (Size of extra format information) (2) let pcm = format_tag == PCM || format_tag == IEEE_FLOAT;
let _cb_size = fmt.read_u16::<LittleEndian>()?; if !pcm && total_samples == 0 {
// Valid bits per sample (2)
let valid_bits_per_sample = fmt.read_u16::<LittleEndian>()?;
// Channel mask (4)
channel_mask = Some(ChannelMask(fmt.read_u32::<LittleEndian>()?));
if valid_bits_per_sample > 0 {
bit_depth = valid_bits_per_sample as u8;
}
format_tag = fmt.read_u16::<LittleEndian>()?;
} else {
channel_mask = None;
}
let non_pcm = format_tag != PCM && format_tag != IEEE_FLOAT;
if non_pcm && total_samples == 0 {
decode_err!(@BAIL Wav, "Non-PCM format identified, no \"fact\" chunk found"); decode_err!(@BAIL Wav, "Non-PCM format identified, no \"fact\" chunk found");
} }
if bits_per_sample > 0 { if bits_per_sample > 0 && (total_samples == 0 || !pcm) {
total_samples = stream_len / u32::from(u16::from(channels) * ((bits_per_sample + 7) / 8)) total_samples = stream_len / u32::from(u16::from(channels) * ((bits_per_sample + 7) / 8))
} else if !non_pcm {
total_samples = 0
} }
let duration; let mut duration = Duration::ZERO;
let overall_bitrate; let mut overall_bitrate = 0;
let audio_bitrate; let mut audio_bitrate = 0;
if bytes_per_second > 0 {
audio_bitrate = (bytes_per_second * 8).div_round(1000);
}
if sample_rate > 0 && total_samples > 0 { if sample_rate > 0 && total_samples > 0 {
log::debug!("Calculating duration and bitrate from total samples");
let length = (u64::from(total_samples) * 1000).div_round(u64::from(sample_rate)); let length = (u64::from(total_samples) * 1000).div_round(u64::from(sample_rate));
if length == 0 { duration = Duration::from_millis(length);
duration = Duration::ZERO; if length > 0 {
overall_bitrate = 0;
audio_bitrate = 0;
} else {
duration = Duration::from_millis(length);
overall_bitrate = (file_length * 8).div_round(length) as u32; overall_bitrate = (file_length * 8).div_round(length) as u32;
audio_bitrate = (u64::from(stream_len) * 8).div_round(length) as u32; if audio_bitrate == 0 {
log::warn!("Estimating audio bitrate from stream length");
audio_bitrate = (u64::from(stream_len) * 8).div_round(length) as u32;
}
} }
} else if stream_len > 0 && bytes_per_second > 0 { } else if stream_len > 0 && bytes_per_second > 0 {
log::debug!("Calculating duration and bitrate from stream length/byte rate");
let length = (u64::from(stream_len) * 1000).div_round(u64::from(bytes_per_second)); let length = (u64::from(stream_len) * 1000).div_round(u64::from(bytes_per_second));
if length == 0 { duration = Duration::from_millis(length);
duration = Duration::ZERO; if length > 0 {
overall_bitrate = 0;
audio_bitrate = 0;
} else {
duration = Duration::from_millis(length);
overall_bitrate = (file_length * 8).div_round(length) as u32; overall_bitrate = (file_length * 8).div_round(length) as u32;
audio_bitrate = (bytes_per_second * 8).div_round(1000);
} }
} else { } else {
duration = Duration::ZERO; log::warn!("Unable to calculate duration and bitrate");
overall_bitrate = 0;
audio_bitrate = 0;
}; };
Ok(WavProperties { Ok(WavProperties {

View file

@ -107,14 +107,6 @@ where
} }
let properties = if parse_options.read_properties { let properties = if parse_options.read_properties {
if fmt.len() < 16 {
decode_err!(@BAIL Wav, "File does not contain a valid \"fmt \" chunk");
}
if stream_len == 0 {
decode_err!(@BAIL Wav, "File does not contain a \"data\" chunk");
}
let file_length = data.stream_position()?; let file_length = data.stream_position()?;
super::properties::read_properties(&mut &*fmt, total_samples, stream_len, file_length)? super::properties::read_properties(&mut &*fmt, total_samples, stream_len, file_length)?