mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2025-01-18 23:23:53 +00:00
Add overall bitrate, create file property tests
This commit is contained in:
parent
72641e2f25
commit
60e1579bb2
21 changed files with 586 additions and 159 deletions
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)?,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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("----") {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>()?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)?,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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
148
tests/properties.rs
Normal 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
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue