Add overall bitrate, create file property tests

This commit is contained in:
Serial 2021-11-26 15:17:02 -05:00
parent 72641e2f25
commit 60e1579bb2
21 changed files with 586 additions and 159 deletions

View file

@ -128,7 +128,8 @@
clippy::new_without_default,
clippy::unused_self,
clippy::from_over_into,
clippy::upper_case_acronyms
clippy::upper_case_acronyms,
clippy::too_many_arguments
)]
mod error;

View file

@ -17,11 +17,13 @@ use tag::ApeTag;
use std::io::{Read, Seek};
use std::time::Duration;
#[derive(Clone, Debug, PartialEq)]
/// An APE file's audio properties
pub struct ApeProperties {
version: u16,
duration: Duration,
bitrate: u32,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
}
@ -30,7 +32,8 @@ impl From<ApeProperties> for FileProperties {
fn from(input: ApeProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.sample_rate),
channels: Some(input.channels),
}
@ -38,14 +41,37 @@ impl From<ApeProperties> for FileProperties {
}
impl ApeProperties {
pub const fn new(
version: u16,
duration: Duration,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
) -> Self {
Self {
version,
duration,
overall_bitrate,
audio_bitrate,
sample_rate,
channels,
}
}
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
self.audio_bitrate
}
/// Sample rate (Hz)

View file

@ -7,7 +7,12 @@ use std::time::Duration;
use byteorder::{LittleEndian, ReadBytesExt};
pub fn properties_gt_3980<R>(data: &mut R, version: u16, stream_len: u64) -> Result<ApeProperties>
pub fn properties_gt_3980<R>(
data: &mut R,
version: u16,
stream_len: u64,
file_length: u64,
) -> Result<ApeProperties>
where
R: Read + Seek,
{
@ -60,7 +65,8 @@ where
let sample_rate = header_read.read_u32::<LittleEndian>()?;
let (duration, bitrate) = get_duration_bitrate(
let (duration, overall_bitrate, audio_bitrate) = get_duration_bitrate(
file_length,
total_frames,
final_frame_blocks,
blocks_per_frame,
@ -71,13 +77,19 @@ where
Ok(ApeProperties {
version,
duration,
bitrate,
overall_bitrate,
audio_bitrate,
sample_rate,
channels: channels as u8,
})
}
pub fn properties_lt_3980<R>(data: &mut R, version: u16, stream_len: u64) -> Result<ApeProperties>
pub fn properties_lt_3980<R>(
data: &mut R,
version: u16,
stream_len: u64,
file_length: u64,
) -> Result<ApeProperties>
where
R: Read + Seek,
{
@ -124,7 +136,8 @@ where
let final_frame_blocks = data.read_u32::<LittleEndian>()?;
let (duration, bitrate) = get_duration_bitrate(
let (duration, overall_bitrate, audio_bitrate) = get_duration_bitrate(
file_length,
total_frames,
final_frame_blocks,
blocks_per_frame,
@ -135,19 +148,21 @@ where
Ok(ApeProperties {
version,
duration,
bitrate,
overall_bitrate,
audio_bitrate,
sample_rate,
channels: channels as u8,
})
}
fn get_duration_bitrate(
file_length: u64,
total_frames: u32,
final_frame_blocks: u32,
blocks_per_frame: u32,
sample_rate: u32,
stream_len: u64,
) -> (Duration, u32) {
) -> (Duration, u32, u32) {
let mut total_samples = u64::from(final_frame_blocks);
if total_samples > 1 {
@ -156,10 +171,16 @@ fn get_duration_bitrate(
if sample_rate > 0 {
let length = (total_samples * 1000) / u64::from(sample_rate);
let bitrate = ((stream_len * 8) / length) as u32;
(Duration::from_millis(length), bitrate)
let overall_bitrate = ((file_length * 8) / length) as u32;
let audio_bitrate = ((stream_len * 8) / length) as u32;
(
Duration::from_millis(length),
overall_bitrate,
audio_bitrate,
)
} else {
(Duration::ZERO, 0)
(Duration::ZERO, 0, 0)
}
}

View file

@ -16,7 +16,7 @@ use crate::id3::v2::Id3v2Tag;
use crate::logic::ape::tag::ApeTag;
use byteorder::{LittleEndian, ReadBytesExt};
fn read_properties<R>(data: &mut R, stream_len: u64) -> Result<ApeProperties>
fn read_properties<R>(data: &mut R, stream_len: u64, file_length: u64) -> Result<ApeProperties>
where
R: Read + Seek,
{
@ -26,9 +26,9 @@ where
// Property reading differs between versions
if version >= 3980 {
properties_gt_3980(data, version, stream_len)
properties_gt_3980(data, version, stream_len, file_length)
} else {
properties_lt_3980(data, version, stream_len)
properties_lt_3980(data, version, stream_len, file_length)
}
}
@ -138,6 +138,8 @@ where
ape_tag = Some(ape)
}
let file_length = data.seek(SeekFrom::Current(0))?;
// Go back to the MAC header to read properties
data.seek(SeekFrom::Start(mac_start))?;
@ -148,6 +150,6 @@ where
id3v2_tag,
#[cfg(feature = "ape")]
ape_tag,
properties: read_properties(data, stream_len)?,
properties: read_properties(data, stream_len, file_length)?,
})
}

View file

@ -6,7 +6,11 @@ use std::time::Duration;
use byteorder::{BigEndian, ReadBytesExt};
pub(super) fn read_properties(comm: &mut &[u8], stream_len: u32) -> Result<FileProperties> {
pub(super) fn read_properties(
comm: &mut &[u8],
stream_len: u32,
file_length: u64,
) -> Result<FileProperties> {
let channels = comm.read_u16::<BigEndian>()? as u8;
if channels == 0 {
@ -45,21 +49,23 @@ pub(super) fn read_properties(comm: &mut &[u8], stream_len: u32) -> Result<FileP
let sample_rate = float.round() as u32;
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
let (duration, overall_bitrate, audio_bitrate) = if sample_rate > 0 && sample_frames > 0 {
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
(
Duration::from_millis(length),
(u64::from(stream_len * 8) / length) as u32,
Some(((file_length * 8) / length) as u32),
Some((u64::from(stream_len * 8) / length) as u32),
)
} else {
(Duration::ZERO, 0)
(Duration::ZERO, None, None)
};
Ok(FileProperties::new(
Ok(FileProperties {
duration,
Some(bitrate),
Some(sample_rate),
Some(channels),
))
overall_bitrate,
audio_bitrate,
sample_rate: Some(sample_rate),
channels: Some(channels),
})
}

View file

@ -90,7 +90,11 @@ where
return Err(LoftyError::Aiff("File does not contain a \"SSND\" chunk"));
}
let properties = super::properties::read_properties(&mut &*comm.unwrap(), stream_len)?;
let properties = super::properties::read_properties(
&mut &*comm.unwrap(),
stream_len,
data.seek(SeekFrom::Current(0))?,
)?;
Ok(AiffFile {
properties,

View file

@ -10,6 +10,7 @@ const IEEE_FLOAT: u16 = 0x0003;
const EXTENSIBLE: u16 = 0xfffe;
#[allow(missing_docs, non_camel_case_types)]
#[derive(Debug, Copy, Clone, PartialEq)]
/// A WAV file's format
pub enum WavFormat {
PCM,
@ -17,11 +18,13 @@ pub enum WavFormat {
Other(u16),
}
#[derive(Debug, Copy, Clone, PartialEq)]
/// A WAV file's audio properties
pub struct WavProperties {
format: WavFormat,
duration: Duration,
bitrate: u32,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
}
@ -30,7 +33,8 @@ impl From<WavProperties> for FileProperties {
fn from(input: WavProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.sample_rate),
channels: Some(input.channels),
}
@ -38,14 +42,37 @@ impl From<WavProperties> for FileProperties {
}
impl WavProperties {
pub const fn new(
format: WavFormat,
duration: Duration,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
) -> Self {
Self {
format,
duration,
overall_bitrate,
audio_bitrate,
sample_rate,
channels,
}
}
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
self.audio_bitrate
}
/// Sample rate (Hz)
@ -66,8 +93,9 @@ impl WavProperties {
pub(super) fn read_properties(
fmt: &mut &[u8],
total_samples: u32,
mut total_samples: u32,
stream_len: u32,
file_length: u64,
) -> Result<WavProperties> {
let mut format_tag = fmt.read_u16::<LittleEndian>()?;
let channels = fmt.read_u16::<LittleEndian>()? as u8;
@ -81,7 +109,7 @@ pub(super) fn read_properties(
// Skip 2 bytes
// Block align (2)
let _ = fmt.read_u16::<LittleEndian>()?;
fmt.read_u16::<LittleEndian>()?;
let bits_per_sample = fmt.read_u16::<LittleEndian>()?;
@ -96,7 +124,7 @@ pub(super) fn read_properties(
// cbSize (Size of extra format information) (2)
// Valid bits per sample (2)
// Channel mask (4)
let _ = fmt.read_u64::<LittleEndian>()?;
fmt.read_u64::<LittleEndian>()?;
format_tag = fmt.read_u16::<LittleEndian>()?;
}
@ -109,27 +137,36 @@ pub(super) fn read_properties(
));
}
let sample_frames = if non_pcm {
total_samples
} else if bits_per_sample > 0 {
stream_len / u32::from(u16::from(channels) * ((bits_per_sample + 7) / 8))
} else {
0
};
if bits_per_sample > 0 {
total_samples = stream_len / u32::from(u16::from(channels) * ((bits_per_sample + 7) / 8))
} else if !non_pcm {
total_samples = 0
}
let (duration, bitrate) = if sample_rate > 0 && sample_frames > 0 {
let length = (u64::from(sample_frames) * 1000) / u64::from(sample_rate);
let (duration, overall_bitrate, audio_bitrate) = if sample_rate > 0 && total_samples > 0 {
let length = (u64::from(total_samples) * 1000) / u64::from(sample_rate);
let overall_bitrate = ((file_length * 8) / length) as u32;
let audio_bitrate = (u64::from(stream_len * 8) / length) as u32;
(
Duration::from_millis(length),
(u64::from(stream_len * 8) / length) as u32,
overall_bitrate,
audio_bitrate,
)
} else if bytes_per_second > 0 {
let length = (u64::from(stream_len) * 1000) / u64::from(bytes_per_second);
(Duration::from_millis(length), (bytes_per_second * 8) / 1000)
let overall_bitrate = ((file_length * 8) / length) as u32;
let audio_bitrate = (bytes_per_second * 8) / 1000;
(
Duration::from_millis(length),
overall_bitrate,
audio_bitrate,
)
} else {
(Duration::ZERO, 0)
(Duration::ZERO, 0, 0)
};
Ok(WavProperties {
@ -139,7 +176,8 @@ pub(super) fn read_properties(
other => WavFormat::Other(other),
},
duration,
bitrate,
overall_bitrate,
audio_bitrate,
sample_rate,
channels,
})

View file

@ -101,8 +101,15 @@ where
return Err(LoftyError::Wav("File does not contain a \"data\" chunk"));
}
let file_length = data.seek(SeekFrom::Current(0))?;
Ok(WavFile {
properties: super::properties::read_properties(&mut &*fmt, total_samples, stream_len)?,
properties: super::properties::read_properties(
&mut &*fmt,
total_samples,
stream_len,
file_length,
)?,
#[cfg(feature = "riff_info_list")]
riff_info: (!riff_info.items.is_empty()).then(|| riff_info),
#[cfg(feature = "id3v2")]

View file

@ -5,11 +5,11 @@ use std::io::Read;
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) fn verify_frame_sync(frame_sync: u16) -> bool {
(frame_sync & 0xffe0) == 0xffe0
pub(crate) fn verify_frame_sync(frame_sync: [u8; 2]) -> bool {
frame_sync[0] == 0xFF && frame_sync[1] >> 5 == 0b111
}
#[derive(PartialEq, Copy, Clone)]
#[derive(PartialEq, Copy, Clone, Debug)]
#[allow(missing_docs)]
/// MPEG Audio version
pub enum MpegVersion {
@ -18,7 +18,7 @@ pub enum MpegVersion {
V2_5,
}
#[derive(Copy, Clone)]
#[derive(Copy, Clone, Debug, PartialEq)]
#[allow(missing_docs)]
/// MPEG layer
pub enum Layer {
@ -27,7 +27,7 @@ pub enum Layer {
Layer3 = 3,
}
#[derive(Copy, Clone, PartialEq)]
#[derive(Copy, Clone, PartialEq, Debug)]
#[allow(missing_docs)]
/// Channel mode
pub enum ChannelMode {

View file

@ -14,13 +14,15 @@ use header::{ChannelMode, Layer, MpegVersion};
use std::io::{Read, Seek};
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq)]
/// An MP3 file's audio properties
pub struct Mp3Properties {
version: MpegVersion,
layer: Layer,
channel_mode: ChannelMode,
duration: Duration,
bitrate: u32,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
}
@ -29,7 +31,8 @@ impl From<Mp3Properties> for FileProperties {
fn from(input: Mp3Properties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.sample_rate),
channels: Some(input.channels),
}
@ -37,14 +40,41 @@ impl From<Mp3Properties> for FileProperties {
}
impl Mp3Properties {
pub const fn new(
version: MpegVersion,
layer: Layer,
channel_mode: ChannelMode,
duration: Duration,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
) -> Self {
Self {
version,
layer,
channel_mode,
duration,
overall_bitrate,
audio_bitrate,
sample_rate,
channels,
}
}
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn audio_bitrate(&self) -> u32 {
self.audio_bitrate
}
/// Sample rate (Hz)

View file

@ -1,6 +1,8 @@
use super::header::{verify_frame_sync, Header, XingHeader};
use super::{Mp3File, Mp3Properties};
use crate::error::{LoftyError, Result};
use crate::id3::v2::Id3v2Tag;
use crate::logic::ape::tag::ApeTag;
use crate::logic::id3::unsynch_u32;
use crate::logic::id3::v1::tag::Id3v1Tag;
use crate::logic::id3::v2::read::parse_id3v2;
@ -8,43 +10,43 @@ use crate::logic::id3::v2::read::parse_id3v2;
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use crate::id3::v2::Id3v2Tag;
use crate::logic::ape::tag::ApeTag;
use byteorder::{BigEndian, ByteOrder, ReadBytesExt};
fn read_properties(
first_frame: (Header, u64),
mut first_frame: (Header, u64),
last_frame: (Header, u64),
xing_header: Option<XingHeader>,
file_length: u64,
) -> Mp3Properties {
let (duration, bitrate) = {
if let Some(xing_header) = xing_header {
if first_frame.0.samples > 0 && first_frame.0.sample_rate > 0 {
let (duration, overall_bitrate, audio_bitrate) = {
match xing_header {
Some(xing_header) if first_frame.0.sample_rate > 0 => {
let frame_time =
u32::from(first_frame.0.samples) * 1000 / first_frame.0.sample_rate;
let length = u64::from(frame_time) * u64::from(xing_header.frames);
let overall_bitrate = ((file_length * 8) / length) as u32;
let audio_bitrate = ((u64::from(xing_header.size) * 8) / length) as u32;
(
Duration::from_millis(length),
((u64::from(xing_header.size) * 8) / length) as u32,
overall_bitrate,
audio_bitrate,
)
} else {
(Duration::ZERO, first_frame.0.bitrate)
}
} else if first_frame.0.bitrate > 0 {
let bitrate = first_frame.0.bitrate;
},
_ if first_frame.0.bitrate > 0 => {
let audio_bitrate = first_frame.0.bitrate;
let stream_length = last_frame.1 - first_frame.1 + u64::from(first_frame.0.len);
let stream_length = last_frame.1 - first_frame.1 + u64::from(first_frame.0.len);
let length = (stream_length * 8) / u64::from(audio_bitrate);
let length = if stream_length > 0 {
Duration::from_millis((stream_length * 8) / u64::from(bitrate))
} else {
Duration::ZERO
};
let overall_bitrate = ((file_length * 8) / length) as u32;
(length, bitrate)
} else {
(Duration::ZERO, 0)
let duration = Duration::from_millis(length);
(duration, overall_bitrate, audio_bitrate)
},
_ => (Duration::ZERO, 0, 0),
}
};
@ -53,7 +55,8 @@ fn read_properties(
layer: first_frame.0.layer,
channel_mode: first_frame.0.channel_mode,
duration,
bitrate,
overall_bitrate,
audio_bitrate,
sample_rate: first_frame.0.sample_rate,
channels: first_frame.0.channels as u8,
}
@ -80,7 +83,7 @@ where
while let Ok(()) = data.read_exact(&mut header) {
match header {
_ if verify_frame_sync(u16::from_be_bytes([header[0], header[1]])) => {
_ if verify_frame_sync([header[0], header[1]]) => {
let start = data.seek(SeekFrom::Current(0))? - 4;
let header = Header::read(u32::from_be_bytes(header))?;
data.seek(SeekFrom::Current(i64::from(header.len - 4)))?;
@ -139,6 +142,8 @@ where
return Err(LoftyError::Mp3("Unable to find an MPEG frame"));
}
let file_length = data.seek(SeekFrom::Current(0))?;
let first_mpeg_frame = (first_mpeg_frame.0.unwrap(), first_mpeg_frame.1);
let last_mpeg_frame = (last_mpeg_frame.0.unwrap(), last_mpeg_frame.1);
@ -158,6 +163,6 @@ where
id3v1_tag,
#[cfg(feature = "ape")]
ape_tag,
properties: read_properties(first_mpeg_frame, last_mpeg_frame, xing_header),
properties: read_properties(first_mpeg_frame, last_mpeg_frame, xing_header, file_length),
})
}

View file

@ -7,12 +7,14 @@ use crate::types::tag::{Tag, TagType};
use std::convert::TryInto;
#[cfg(feature = "mp4_atoms")]
#[derive(Default)]
/// An Mp4
pub struct Ilst {
pub(crate) atoms: Vec<Atom>,
}
#[cfg(feature = "mp4_atoms")]
impl From<Ilst> for Tag {
fn from(input: Ilst) -> Self {
let mut tag = Self::new(TagType::Mp4Atom);
@ -46,6 +48,7 @@ impl From<Ilst> for Tag {
}
}
#[cfg(feature = "mp4_atoms")]
impl From<Tag> for Ilst {
fn from(input: Tag) -> Self {
let mut ilst = Self::default();
@ -76,12 +79,13 @@ impl From<Tag> for Ilst {
}
}
#[cfg(feature = "mp4_atoms")]
pub struct Atom {
ident: AtomIdent,
data: AtomData,
}
#[derive(Eq, PartialEq)]
#[derive(Eq, PartialEq, Debug)]
pub enum AtomIdent {
/// A four byte identifier
///
@ -104,6 +108,7 @@ pub enum AtomIdent {
Freeform { mean: String, name: String },
}
#[cfg(feature = "mp4_atoms")]
/// The data of an atom
///
/// NOTES:
@ -137,15 +142,18 @@ pub enum AtomData {
},
}
#[cfg(feature = "mp4_atoms")]
pub(crate) struct IlstRef<'a> {
atoms: Box<dyn Iterator<Item = AtomRef<'a>> + 'a>,
}
#[cfg(feature = "mp4_atoms")]
pub(crate) struct AtomRef<'a> {
ident: AtomIdentRef<'a>,
data: AtomDataRef<'a>,
}
#[cfg(feature = "mp4_atoms")]
impl<'a> Into<AtomRef<'a>> for &'a Atom {
fn into(self) -> AtomRef<'a> {
AtomRef {
@ -155,11 +163,13 @@ impl<'a> Into<AtomRef<'a>> for &'a Atom {
}
}
#[cfg(feature = "mp4_atoms")]
pub(crate) enum AtomIdentRef<'a> {
Fourcc([u8; 4]),
Freeform { mean: &'a str, name: &'a str },
}
#[cfg(feature = "mp4_atoms")]
impl<'a> Into<AtomIdentRef<'a>> for &'a AtomIdent {
fn into(self) -> AtomIdentRef<'a> {
match self {
@ -169,6 +179,7 @@ impl<'a> Into<AtomIdentRef<'a>> for &'a AtomIdent {
}
}
#[cfg(feature = "mp4_atoms")]
impl<'a> From<AtomIdentRef<'a>> for AtomIdent {
fn from(input: AtomIdentRef<'a>) -> Self {
match input {
@ -181,6 +192,7 @@ impl<'a> From<AtomIdentRef<'a>> for AtomIdent {
}
}
#[cfg(feature = "mp4_atoms")]
pub(crate) enum AtomDataRef<'a> {
UTF8(&'a str),
UTF16(&'a str),
@ -190,6 +202,7 @@ pub(crate) enum AtomDataRef<'a> {
Unknown { code: u32, data: &'a [u8] },
}
#[cfg(feature = "mp4_atoms")]
impl<'a> Into<AtomDataRef<'a>> for &'a AtomData {
fn into(self) -> AtomDataRef<'a> {
match self {
@ -203,6 +216,7 @@ impl<'a> Into<AtomDataRef<'a>> for &'a AtomData {
}
}
#[cfg(feature = "mp4_atoms")]
impl<'a> Into<IlstRef<'a>> for &'a Ilst {
fn into(self) -> IlstRef<'a> {
IlstRef {
@ -211,6 +225,7 @@ impl<'a> Into<IlstRef<'a>> for &'a Ilst {
}
}
#[cfg(feature = "mp4_atoms")]
impl<'a> Into<IlstRef<'a>> for &'a Tag {
fn into(self) -> IlstRef<'a> {
let iter =
@ -230,6 +245,7 @@ impl<'a> Into<IlstRef<'a>> for &'a Tag {
}
}
#[cfg(feature = "mp4_atoms")]
fn item_key_to_ident(key: &ItemKey) -> Option<AtomIdentRef> {
key.map_key(&TagType::Mp4Atom, true).and_then(|ident| {
if ident.starts_with("----") {

View file

@ -1,5 +1,4 @@
mod atom_info;
#[cfg(feature = "mp4_atoms")]
pub(crate) mod ilst;
mod moov;
mod properties;
@ -16,6 +15,7 @@ use std::io::{Read, Seek};
use std::time::Duration;
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq)]
/// An MP4 file's audio codec
pub enum Mp4Codec {
AAC,
@ -23,11 +23,13 @@ pub enum Mp4Codec {
Unknown(String),
}
#[derive(Debug, Clone, PartialEq)]
/// An MP4 file's audio properties
pub struct Mp4Properties {
codec: Mp4Codec,
duration: Duration,
bitrate: u32,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
}
@ -36,7 +38,8 @@ impl From<Mp4Properties> for FileProperties {
fn from(input: Mp4Properties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.sample_rate),
channels: Some(input.channels),
}
@ -44,14 +47,37 @@ impl From<Mp4Properties> for FileProperties {
}
impl Mp4Properties {
pub const fn new(
codec: Mp4Codec,
duration: Duration,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
) -> Self {
Self {
codec,
duration,
overall_bitrate,
audio_bitrate,
sample_rate,
channels,
}
}
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn audio_bitrate(&self) -> u32 {
self.audio_bitrate
}
/// Sample rate (Hz)

View file

@ -11,7 +11,11 @@ use std::time::Duration;
use byteorder::{BigEndian, ReadBytesExt};
pub(crate) fn read_properties<R>(data: &mut R, traks: &[Trak]) -> Result<Mp4Properties>
pub(crate) fn read_properties<R>(
data: &mut R,
traks: &[Trak],
file_length: u64,
) -> Result<Mp4Properties>
where
R: Read + Seek,
{
@ -106,7 +110,8 @@ where
let mut properties = Mp4Properties {
codec: Mp4Codec::Unknown(String::new()),
duration,
bitrate: 0,
overall_bitrate: 0,
audio_bitrate: 0,
sample_rate: 0,
channels: 0,
};
@ -125,13 +130,13 @@ where
// Version (1)
// Flags (3)
// Number of entries (4)
stsd_reader.seek(SeekFrom::Start(8))?;
stsd_reader.seek(SeekFrom::Current(8))?;
let atom = AtomInfo::read(&mut stsd_reader)?;
if let AtomIdent::Fourcc(ref fourcc) = atom.ident {
match fourcc {
b"mp4a" => mp4a_properties(&mut stsd_reader, &mut properties)?,
b"mp4a" => mp4a_properties(&mut stsd_reader, &mut properties, file_length)?,
b"alac" => alac_properties(&mut stsd_reader, &mut properties)?,
unknown => {
if let Ok(codec) = std::str::from_utf8(unknown) {
@ -147,37 +152,39 @@ where
Ok(properties)
}
fn mp4a_properties<R>(data: &mut R, properties: &mut Mp4Properties) -> Result<()>
fn mp4a_properties<R>(stsd: &mut R, properties: &mut Mp4Properties, file_length: u64) -> Result<()>
where
R: Read + Seek,
{
properties.codec = Mp4Codec::AAC;
// Skipping 16 bytes
// Reserved (6)
// Data reference index (2)
// Version (2)
// Revision level (2)
// Vendor (4)
data.seek(SeekFrom::Current(16))?;
stsd.seek(SeekFrom::Current(16))?;
properties.channels = data.read_u16::<BigEndian>()? as u8;
properties.channels = stsd.read_u16::<BigEndian>()? as u8;
// Skipping 4 bytes
// Sample size (2)
// Compression ID (2)
data.seek(SeekFrom::Current(4))?;
stsd.seek(SeekFrom::Current(4))?;
properties.sample_rate = data.read_u32::<BigEndian>()?;
properties.sample_rate = stsd.read_u32::<BigEndian>()?;
data.seek(SeekFrom::Current(2))?;
stsd.seek(SeekFrom::Current(2))?;
// This information is often followed by an esds (elementary stream descriptor) atom containing the bitrate
if let Ok(esds) = AtomInfo::read(data) {
if let Ok(esds) = AtomInfo::read(stsd) {
// There are 4 bytes we expect to be zeroed out
// Version (1)
// Flags (3)
if esds.ident == AtomIdent::Fourcc(*b"esds") && data.read_u32::<BigEndian>()? == 0 {
if esds.ident == AtomIdent::Fourcc(*b"esds") && stsd.read_u32::<BigEndian>()? == 0 {
let mut descriptor = [0; 4];
data.read_exact(&mut descriptor)?;
stsd.read_exact(&mut descriptor)?;
// [0x03, 0x80, 0x80, 0x80] marks the start of the elementary stream descriptor.
// 0x03 being the object descriptor
@ -186,11 +193,11 @@ where
// Descriptor length (1)
// Elementary stream ID (2)
// Flags (1)
let _info = data.read_u32::<BigEndian>()?;
let _info = stsd.read_u32::<BigEndian>()?;
// There is another descriptor embedded in the previous one
let mut specific_config = [0; 4];
data.read_exact(&mut specific_config)?;
stsd.read_exact(&mut specific_config)?;
// [0x04, 0x80, 0x80, 0x80] marks the start of the descriptor configuration
if specific_config == [0x04, 0x80, 0x80, 0x80] {
@ -201,12 +208,16 @@ where
// Buffer size (3)
// Max bitrate (4)
let mut info = [0; 10];
data.read_exact(&mut info)?;
stsd.read_exact(&mut info)?;
let average_bitrate = data.read_u32::<BigEndian>()?;
let average_bitrate = stsd.read_u32::<BigEndian>()?;
let overall_bitrate =
u128::from(file_length * 8) / properties.duration.as_millis();
if average_bitrate > 0 {
properties.bitrate = average_bitrate / 1000
properties.overall_bitrate = overall_bitrate as u32;
properties.audio_bitrate = average_bitrate / 1000
}
}
}
@ -236,6 +247,8 @@ where
if let Ok(alac) = AtomInfo::read(data) {
if alac.ident == AtomIdent::Fourcc(*b"alac") {
properties.codec = Mp4Codec::ALAC;
// Skipping 13 bytes
// Version (4)
// Samples per frame (4)
@ -253,7 +266,7 @@ where
// Max frame size (4)
data.seek(SeekFrom::Current(6))?;
properties.bitrate = data.read_u32::<BigEndian>()?;
properties.audio_bitrate = data.read_u32::<BigEndian>()?;
properties.sample_rate = data.read_u32::<BigEndian>()?;
}
}

View file

@ -36,10 +36,12 @@ where
Moov::find(data)?;
let moov = Moov::parse(data)?;
let file_length = data.seek(SeekFrom::End(0))?;
Ok(Mp4File {
ftyp,
ilst: moov.meta,
properties: read_properties(data, &moov.traks)?,
properties: read_properties(data, &moov.traks, file_length)?,
})
}

View file

@ -31,7 +31,11 @@ where
Ok(block)
}
fn read_properties<R>(stream_info: &mut R, stream_length: u64) -> Result<FileProperties>
fn read_properties<R>(
stream_info: &mut R,
stream_length: u64,
file_length: u64,
) -> Result<FileProperties>
where
R: Read,
{
@ -58,23 +62,25 @@ where
// Read the remaining 32 bits of the total samples
let total_samples = stream_info.read_u32::<BigEndian>()? | (info << 28);
let (duration, bitrate) = if sample_rate > 0 && total_samples > 0 {
let (duration, overall_bitrate, audio_bitrate) = if sample_rate > 0 && total_samples > 0 {
let length = (u64::from(total_samples) * 1000) / u64::from(sample_rate);
(
Duration::from_millis(length),
((stream_length * 8) / length) as u32,
Some(((file_length * 8) / length) as u32),
Some(((stream_length * 8) / length) as u32),
)
} else {
(Duration::ZERO, 0)
(Duration::ZERO, None, None)
};
Ok(FileProperties::new(
Ok(FileProperties {
duration,
Some(bitrate),
Some(sample_rate as u32),
Some(channels as u8),
))
overall_bitrate,
audio_bitrate,
sample_rate: Some(sample_rate as u32),
channels: Some(channels as u8),
})
}
pub(in crate::logic::ogg) fn read_from<R>(data: &mut R) -> Result<FlacFile>
@ -111,13 +117,14 @@ where
}
}
let stream_length = {
let (stream_length, file_length) = {
let current = data.seek(SeekFrom::Current(0))?;
let end = data.seek(SeekFrom::End(0))?;
end - current
(end - current, end)
};
let properties = read_properties(&mut &*stream_info.content, stream_length)?;
let properties = read_properties(&mut &*stream_info.content, stream_length, file_length)?;
Ok(FlacFile {
properties,

View file

@ -8,10 +8,12 @@ use std::time::Duration;
use byteorder::{LittleEndian, ReadBytesExt};
use ogg_pager::Page;
#[derive(Debug, Copy, Clone, PartialEq)]
/// An Opus file's audio properties
pub struct OpusProperties {
duration: Duration,
bitrate: u32,
overall_bitrate: u32,
audio_bitrate: u32,
channels: u8,
version: u8,
input_sample_rate: u32,
@ -21,7 +23,8 @@ impl From<OpusProperties> for FileProperties {
fn from(input: OpusProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.input_sample_rate),
channels: Some(input.channels),
}
@ -29,14 +32,37 @@ impl From<OpusProperties> for FileProperties {
}
impl OpusProperties {
pub const fn new(
duration: Duration,
overall_bitrate: u32,
audio_bitrate: u32,
channels: u8,
version: u8,
input_sample_rate: u32,
) -> Self {
Self {
duration,
overall_bitrate,
audio_bitrate,
channels,
version,
input_sample_rate,
}
}
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn audio_bitrate(&self) -> u32 {
self.audio_bitrate
}
/// Channel count
@ -62,12 +88,12 @@ pub(in crate::logic::ogg) fn read_properties<R>(
where
R: Read + Seek,
{
let stream_len = {
let (stream_len, file_length) = {
let current = data.seek(SeekFrom::Current(0))?;
let end = data.seek(SeekFrom::End(0))?;
data.seek(SeekFrom::Start(current))?;
end - first_page.start
(end - first_page.start, end)
};
let first_page_abgp = first_page.abgp;
@ -93,11 +119,14 @@ where
|frame_count| {
let length = frame_count * 1000 / 48000;
let duration = Duration::from_millis(length as u64);
let bitrate = (audio_size * 8 / length) as u32;
let overall_bitrate = ((file_length * 8) / length) as u32;
let audio_bitrate = (audio_size * 8 / length) as u32;
Ok(OpusProperties {
duration,
bitrate,
overall_bitrate,
audio_bitrate,
channels,
version,
input_sample_rate,

View file

@ -2,29 +2,32 @@ use super::find_last_page;
use crate::error::{LoftyError, Result};
use crate::types::properties::FileProperties;
use std::io::{Read, Seek};
use std::io::{Read, Seek, SeekFrom};
use std::time::Duration;
use byteorder::{LittleEndian, ReadBytesExt};
use ogg_pager::Page;
#[derive(Copy, Clone, Debug, PartialEq)]
/// An OGG Vorbis file's audio properties
pub struct VorbisProperties {
duration: Duration,
bitrate: u32,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
version: u32,
bitrate_maximum: u32,
bitrate_nominal: u32,
bitrate_minimum: u32,
bitrate_maximum: i32,
bitrate_nominal: i32,
bitrate_minimum: i32,
}
impl From<VorbisProperties> for FileProperties {
fn from(input: VorbisProperties) -> Self {
Self {
duration: input.duration,
bitrate: Some(input.bitrate),
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.sample_rate),
channels: Some(input.channels),
}
@ -32,14 +35,43 @@ impl From<VorbisProperties> for FileProperties {
}
impl VorbisProperties {
pub const fn new(
duration: Duration,
overall_bitrate: u32,
audio_bitrate: u32,
sample_rate: u32,
channels: u8,
version: u32,
bitrate_maximum: i32,
bitrate_nominal: i32,
bitrate_minimum: i32,
) -> Self {
Self {
duration,
overall_bitrate,
audio_bitrate,
sample_rate,
channels,
version,
bitrate_maximum,
bitrate_nominal,
bitrate_minimum,
}
}
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> u32 {
self.bitrate
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn audio_bitrate(&self) -> u32 {
self.audio_bitrate
}
/// Sample rate (Hz)
@ -58,17 +90,17 @@ impl VorbisProperties {
}
/// Maximum bitrate
pub fn bitrate_max(&self) -> u32 {
pub fn bitrate_max(&self) -> i32 {
self.bitrate_maximum
}
/// Nominal bitrate
pub fn bitrate_nominal(&self) -> u32 {
pub fn bitrate_nominal(&self) -> i32 {
self.bitrate_nominal
}
/// Minimum bitrate
pub fn bitrate_min(&self) -> u32 {
pub fn bitrate_min(&self) -> i32 {
self.bitrate_minimum
}
}
@ -90,23 +122,28 @@ where
let channels = first_page_content.read_u8()?;
let sample_rate = first_page_content.read_u32::<LittleEndian>()?;
let bitrate_maximum = first_page_content.read_u32::<LittleEndian>()?;
let bitrate_nominal = first_page_content.read_u32::<LittleEndian>()?;
let bitrate_minimum = first_page_content.read_u32::<LittleEndian>()?;
let bitrate_maximum = first_page_content.read_i32::<LittleEndian>()?;
let bitrate_nominal = first_page_content.read_i32::<LittleEndian>()?;
let bitrate_minimum = first_page_content.read_i32::<LittleEndian>()?;
let last_page = find_last_page(data)?;
let last_page_abgp = last_page.abgp;
let file_length = data.seek(SeekFrom::End(0))?;
last_page_abgp.checked_sub(first_page_abgp).map_or_else(
|| Err(LoftyError::Vorbis("File contains incorrect PCM values")),
|frame_count| {
let length = frame_count * 1000 / u64::from(sample_rate);
let duration = Duration::from_millis(length as u64);
let bitrate = bitrate_nominal / 1000;
let overall_bitrate = ((file_length * 8) / length) as u32;
let audio_bitrate = bitrate_nominal as u64 / 1000;
Ok(VorbisProperties {
duration,
bitrate,
overall_bitrate,
audio_bitrate: audio_bitrate as u32,
sample_rate,
channels,
version,

View file

@ -191,13 +191,13 @@ impl FileType {
if &ident == b"MAC" {
Ok(Self::APE)
} else if verify_frame_sync(u16::from_be_bytes([ident[0], ident[1]])) {
} else if verify_frame_sync([ident[0], ident[1]]) {
Ok(Self::MP3)
} else {
Err(LoftyError::UnknownFormat)
}
},
_ if verify_frame_sync(u16::from_be_bytes([sig[0], sig[1]])) => Ok(Self::MP3),
_ if verify_frame_sync([sig[0], sig[1]]) => Ok(Self::MP3),
70 if sig.starts_with(b"FORM") => {
let mut id_remaining = [0; 2];
data.read_exact(&mut id_remaining)?;

View file

@ -1,9 +1,11 @@
use std::time::Duration;
#[derive(Debug, PartialEq, Clone)]
/// Various *immutable* audio properties
pub struct FileProperties {
pub(crate) duration: Duration,
pub(crate) bitrate: Option<u32>,
pub(crate) overall_bitrate: Option<u32>,
pub(crate) audio_bitrate: Option<u32>,
pub(crate) sample_rate: Option<u32>,
pub(crate) channels: Option<u8>,
}
@ -12,7 +14,8 @@ impl Default for FileProperties {
fn default() -> Self {
Self {
duration: Duration::ZERO,
bitrate: None,
overall_bitrate: None,
audio_bitrate: None,
sample_rate: None,
channels: None,
}
@ -20,16 +23,17 @@ impl Default for FileProperties {
}
impl FileProperties {
/// Create a new FileProperties
pub const fn new(
duration: Duration,
bitrate: Option<u32>,
overall_bitrate: Option<u32>,
audio_bitrate: Option<u32>,
sample_rate: Option<u32>,
channels: Option<u8>,
) -> Self {
Self {
duration,
bitrate,
overall_bitrate,
audio_bitrate,
sample_rate,
channels,
}
@ -40,9 +44,14 @@ impl FileProperties {
self.duration
}
/// Bitrate (kbps)
pub fn bitrate(&self) -> Option<u32> {
self.bitrate
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> Option<u32> {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn audio_bitrate(&self) -> Option<u32> {
self.audio_bitrate
}
/// Sample rate (Hz)

148
tests/properties.rs Normal file
View file

@ -0,0 +1,148 @@
use lofty::ape::{ApeFile, ApeProperties};
use lofty::iff::{AiffFile, WavFile, WavFormat, WavProperties};
use lofty::mp3::{ChannelMode, Layer, Mp3File, Mp3Properties, MpegVersion};
use lofty::mp4::{Mp4Codec, Mp4File, Mp4Properties};
use lofty::ogg::{FlacFile, OpusFile, OpusProperties, VorbisFile, VorbisProperties};
use lofty::{AudioFile, FileProperties};
use std::fs::File;
use std::time::Duration;
const AIFF_PROPERTIES: FileProperties = FileProperties::new(
Duration::from_millis(1428),
Some(1542),
Some(1536),
Some(48000),
Some(2),
);
const APE_PROPERTIES: ApeProperties =
ApeProperties::new(3990, Duration::from_millis(1428), 360, 360, 48000, 2);
const FLAC_PROPERTIES: FileProperties = FileProperties::new(
Duration::from_millis(1428),
Some(321),
Some(275),
Some(48000),
Some(2),
);
const MP3_PROPERTIES: Mp3Properties = Mp3Properties::new(
MpegVersion::V1,
Layer::Layer3,
ChannelMode::Stereo,
Duration::from_millis(1464),
64,
62,
48000,
2,
);
const MP4_PROPERTIES: Mp4Properties = Mp4Properties::new(
Mp4Codec::AAC,
Duration::from_millis(1449),
135,
124,
48000,
2,
);
const OPUS_PROPERTIES: OpusProperties =
OpusProperties::new(Duration::from_millis(1428), 121, 120, 2, 1, 48000);
const VORBIS_PROPERTIES: VorbisProperties = VorbisProperties::new(
Duration::from_millis(1450),
96,
112,
48000,
2,
0,
0,
112000,
0,
);
const WAV_PROPERTIES: WavProperties = WavProperties::new(
WavFormat::PCM,
Duration::from_millis(1428),
1542,
1536,
48000,
2,
);
fn get_properties<T>(path: &str) -> T::Properties
where
T: AudioFile,
<T as AudioFile>::Properties: Clone,
{
let mut f = File::open(path).unwrap();
let audio_file = T::read_from(&mut f).unwrap();
audio_file.properties().clone()
}
#[test]
fn aiff_properties() {
assert_eq!(
get_properties::<AiffFile>("tests/assets/a.aiff"),
AIFF_PROPERTIES
);
}
#[test]
fn ape_properties() {
assert_eq!(
get_properties::<ApeFile>("tests/assets/a.ape"),
APE_PROPERTIES
);
}
#[test]
fn flac_properties() {
assert_eq!(
get_properties::<FlacFile>("tests/assets/a.flac"),
FLAC_PROPERTIES
)
}
#[test]
fn mp3_properties() {
assert_eq!(
get_properties::<Mp3File>("tests/assets/a.mp3"),
MP3_PROPERTIES
)
}
#[test]
fn mp4_properties() {
assert_eq!(
get_properties::<Mp4File>("tests/assets/a.m4a"),
MP4_PROPERTIES
)
}
#[test]
fn opus_properties() {
assert_eq!(
get_properties::<OpusFile>("tests/assets/a.opus"),
OPUS_PROPERTIES
)
}
#[test]
fn vorbis_properties() {
assert_eq!(
get_properties::<VorbisFile>("tests/assets/a.ogg"),
VORBIS_PROPERTIES
)
}
#[test]
fn wav_properties() {
assert_eq!(
get_properties::<WavFile>("tests/assets/a.wav"),
WAV_PROPERTIES
)
}