mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-13 06:02:32 +00:00
WAV: Improve audio bitrate calculation
This commit is contained in:
parent
d067933be3
commit
f340ebf39b
3 changed files with 113 additions and 68 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)?
|
||||||
|
|
Loading…
Reference in a new issue