mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-13 22:22:31 +00:00
Stop using metaflac for reading, add AudioTagEdit::properties
This commit is contained in:
parent
d519fa5ea1
commit
dd0eb41549
11 changed files with 228 additions and 50 deletions
133
src/components/logic/ogg/flac.rs
Normal file
133
src/components/logic/ogg/flac.rs
Normal file
|
@ -0,0 +1,133 @@
|
|||
use super::read::{read_comments, OGGTags};
|
||||
|
||||
use crate::{FileProperties, LoftyError, OggFormat, Picture, Result};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use unicase::UniCase;
|
||||
|
||||
fn read_properties<R>(stream_info: &mut R, stream_length: u64) -> Result<FileProperties>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
// Skip 4 bytes
|
||||
// Minimum block size (2)
|
||||
// Maximum block size (2)
|
||||
stream_info.read_u16::<BigEndian>()?;
|
||||
|
||||
// Skip 6 bytes
|
||||
// Minimum frame size (3)
|
||||
// Maximum frame size (3)
|
||||
stream_info.read_uint::<BigEndian>(6)?;
|
||||
|
||||
// Read 24 bits
|
||||
// Sample rate (20)
|
||||
// Number of channels (3)
|
||||
// First bit of bits per sample (1)
|
||||
let info = stream_info.read_uint::<BigEndian>(3)?;
|
||||
|
||||
let sample_rate = info >> 4;
|
||||
let channels = (info & 0x15) + 1;
|
||||
|
||||
// There are still 4 bits remaining of the bits per sample
|
||||
// This number isn't used, so just discard it
|
||||
let total_samples_first = stream_info.read_u8()? << 4;
|
||||
|
||||
// Read the remaining 32 bits of the total samples
|
||||
let total_samples = stream_info.read_u32::<BigEndian>()? | u32::from(total_samples_first);
|
||||
|
||||
let (duration, bitrate) = if sample_rate > 0 && total_samples > 0 {
|
||||
let length = (u64::from(total_samples) * 1000) / sample_rate;
|
||||
|
||||
(
|
||||
Duration::from_millis(length),
|
||||
((stream_length * 8) / length) as u32,
|
||||
)
|
||||
} else {
|
||||
(Duration::ZERO, 0)
|
||||
};
|
||||
|
||||
Ok(FileProperties {
|
||||
duration,
|
||||
bitrate: Some(bitrate),
|
||||
sample_rate: Some(sample_rate as u32),
|
||||
channels: Some(channels as u8),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn read_from<R>(data: &mut R) -> Result<OGGTags>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut marker = [0; 4];
|
||||
data.read_exact(&mut marker)?;
|
||||
|
||||
if &marker != b"fLaC" {
|
||||
return Err(LoftyError::InvalidData(
|
||||
"FLAC file missing \"fLaC\" stream marker",
|
||||
));
|
||||
}
|
||||
|
||||
let mut byte = data.read_u8()?;
|
||||
|
||||
if (byte & 0x7f) != 0 {
|
||||
return Err(LoftyError::InvalidData(
|
||||
"FLAC file missing mandatory STREAMINFO block",
|
||||
));
|
||||
}
|
||||
|
||||
let mut last_block = (byte & 0x80) != 0;
|
||||
|
||||
let stream_info_len = data.read_uint::<BigEndian>(3)? as u32;
|
||||
|
||||
if stream_info_len < 18 {
|
||||
return Err(LoftyError::InvalidData("FLAC file has an invalid STREAMINFO block size (< 18)"))
|
||||
}
|
||||
|
||||
let mut stream_info_data = vec![0; stream_info_len as usize];
|
||||
data.read_exact(&mut stream_info_data)?;
|
||||
|
||||
let mut vendor = String::new();
|
||||
let mut comments = HashMap::<UniCase<String>, String>::new();
|
||||
let mut pictures = Vec::<Picture>::new();
|
||||
|
||||
while !last_block {
|
||||
byte = data.read_u8()?;
|
||||
last_block = (byte & 0x80) != 0;
|
||||
let block_type = byte & 0x7f;
|
||||
|
||||
let block_len = data.read_uint::<BigEndian>(3)? as u32;
|
||||
|
||||
match block_type {
|
||||
4 => {
|
||||
let mut comment_data = vec![0; block_len as usize];
|
||||
data.read_exact(&mut comment_data)?;
|
||||
|
||||
vendor = read_comments(&mut &*comment_data, &mut comments, &mut pictures)?
|
||||
},
|
||||
6 => {
|
||||
let mut picture_data = vec![0; block_len as usize];
|
||||
data.read_exact(&mut picture_data)?;
|
||||
|
||||
pictures.push(Picture::from_apic_bytes(&*picture_data)?)
|
||||
},
|
||||
_ => {
|
||||
data.seek(SeekFrom::Current(i64::from(block_len)))?;
|
||||
continue;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let stream_length = {
|
||||
let current = data.seek(SeekFrom::Current(0))?;
|
||||
let end = data.seek(SeekFrom::End(0))?;
|
||||
end - current
|
||||
};
|
||||
|
||||
let properties = read_properties(&mut &*stream_info_data, stream_length)?;
|
||||
|
||||
Ok((vendor, pictures, comments, properties, OggFormat::Flac))
|
||||
}
|
|
@ -8,6 +8,8 @@ pub(crate) mod constants;
|
|||
pub(crate) mod read;
|
||||
pub(crate) mod write;
|
||||
|
||||
#[cfg(feature = "format-flac")]
|
||||
pub(crate) mod flac;
|
||||
#[cfg(feature = "format-opus")]
|
||||
mod opus;
|
||||
#[cfg(feature = "format-vorbis")]
|
||||
|
|
|
@ -39,6 +39,50 @@ where
|
|||
Ok(properties)
|
||||
}
|
||||
|
||||
pub(crate) fn read_comments<R>(
|
||||
data: &mut R,
|
||||
storage: &mut HashMap<UniCase<String>, String>,
|
||||
pictures: &mut Vec<Picture>,
|
||||
) -> Result<String>
|
||||
where
|
||||
R: Read,
|
||||
{
|
||||
let vendor_len = data.read_u32::<LittleEndian>()?;
|
||||
|
||||
let mut vendor = vec![0; vendor_len as usize];
|
||||
data.read_exact(&mut vendor)?;
|
||||
|
||||
let vendor = match String::from_utf8(vendor) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
return Err(LoftyError::InvalidData(
|
||||
"OGG file has an invalid vendor string",
|
||||
))
|
||||
},
|
||||
};
|
||||
|
||||
let comments_total_len = data.read_u32::<LittleEndian>()?;
|
||||
|
||||
for _ in 0..comments_total_len {
|
||||
let comment_len = data.read_u32::<LittleEndian>()?;
|
||||
|
||||
let mut comment_bytes = vec![0; comment_len as usize];
|
||||
data.read_exact(&mut comment_bytes)?;
|
||||
|
||||
let comment = String::from_utf8(comment_bytes)?;
|
||||
|
||||
let split: Vec<&str> = comment.splitn(2, '=').collect();
|
||||
|
||||
if split[0] == "METADATA_BLOCK_PICTURE" {
|
||||
pictures.push(Picture::from_apic_bytes(split[1].as_bytes())?)
|
||||
} else {
|
||||
storage.insert(UniCase::from(split[0].to_string()), split[1].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vendor)
|
||||
}
|
||||
|
||||
pub(crate) fn read_from<T>(
|
||||
data: &mut T,
|
||||
header_sig: &[u8],
|
||||
|
@ -75,40 +119,9 @@ where
|
|||
let mut pictures = Vec::new();
|
||||
|
||||
let reader = &mut &md_pages[..];
|
||||
|
||||
let vendor_len = reader.read_u32::<LittleEndian>()?;
|
||||
let mut vendor = vec![0; vendor_len as usize];
|
||||
reader.read_exact(&mut vendor)?;
|
||||
|
||||
let vendor_str = match String::from_utf8(vendor) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
return Err(LoftyError::InvalidData(
|
||||
"OGG file has an invalid vendor string",
|
||||
))
|
||||
},
|
||||
};
|
||||
|
||||
let comments_total_len = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
for _ in 0..comments_total_len {
|
||||
let comment_len = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let mut comment_bytes = vec![0; comment_len as usize];
|
||||
reader.read_exact(&mut comment_bytes)?;
|
||||
|
||||
let comment = String::from_utf8(comment_bytes)?;
|
||||
|
||||
let split: Vec<&str> = comment.splitn(2, '=').collect();
|
||||
|
||||
if split[0] == "METADATA_BLOCK_PICTURE" {
|
||||
pictures.push(Picture::from_apic_bytes(split[1].as_bytes())?)
|
||||
} else {
|
||||
md.insert(UniCase::from(split[0].to_string()), split[1].to_string());
|
||||
}
|
||||
}
|
||||
let vendor = read_comments(reader, &mut md, &mut pictures)?;
|
||||
|
||||
let properties = read_properties(data, header_sig, &first_page)?;
|
||||
|
||||
Ok((vendor_str, pictures, md, properties, format))
|
||||
Ok((vendor, pictures, md, properties, format))
|
||||
}
|
||||
|
|
|
@ -46,20 +46,25 @@ where
|
|||
let last_page = find_last_page(data)?;
|
||||
let last_page_abgp = last_page.abgp;
|
||||
|
||||
last_page_abgp.checked_sub(first_page_abgp).map_or_else(|| Err(LoftyError::InvalidData(
|
||||
"OGG 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 = ((audio_size * 8) / length) as u32;
|
||||
last_page_abgp.checked_sub(first_page_abgp).map_or_else(
|
||||
|| {
|
||||
Err(LoftyError::InvalidData(
|
||||
"OGG 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 = ((audio_size * 8) / length) as u32;
|
||||
|
||||
Ok(FileProperties {
|
||||
duration,
|
||||
bitrate: Some(bitrate),
|
||||
sample_rate: Some(sample_rate),
|
||||
channels: Some(channels),
|
||||
})
|
||||
})
|
||||
Ok(FileProperties {
|
||||
duration,
|
||||
bitrate: Some(bitrate),
|
||||
sample_rate: Some(sample_rate),
|
||||
channels: Some(channels),
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn write_to(
|
||||
|
|
|
@ -80,6 +80,10 @@ impl AudioTagEdit for AiffTag {
|
|||
fn tag_type(&self) -> TagType {
|
||||
TagType::AiffText
|
||||
}
|
||||
|
||||
fn properties(&self) -> &FileProperties {
|
||||
&self.properties
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioTagWrite for AiffTag {
|
||||
|
|
|
@ -294,6 +294,10 @@ impl AudioTagEdit for ApeTag {
|
|||
fn remove_key(&mut self, key: &str) {
|
||||
self.remove_key(key)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &FileProperties {
|
||||
&self.properties
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioTagWrite for ApeTag {
|
||||
|
|
|
@ -389,6 +389,10 @@ impl AudioTagEdit for Id3v2Tag {
|
|||
fn remove_key(&mut self, key: &str) {
|
||||
self.inner.remove(key)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &FileProperties {
|
||||
&self.properties
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioTagWrite for Id3v2Tag {
|
||||
|
|
|
@ -314,6 +314,10 @@ impl AudioTagEdit for Mp4Tag {
|
|||
fn tag_type(&self) -> TagType {
|
||||
TagType::Mp4
|
||||
}
|
||||
|
||||
fn properties(&self) -> &FileProperties {
|
||||
&self.properties
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioTagWrite for Mp4Tag {
|
||||
|
|
|
@ -10,8 +10,6 @@ use crate::{
|
|||
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, FileProperties, LoftyError, OggFormat,
|
||||
Picture, PictureType, Result, TagType, ToAny, ToAnyTag,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "format-opus", feature = "format-vorbis"))]
|
||||
use crate::components::logic::ogg::read::OGGTags;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
@ -161,7 +159,7 @@ impl OggTag {
|
|||
},
|
||||
#[cfg(feature = "format-flac")]
|
||||
OggFormat::Flac => {
|
||||
let tag = metaflac::Tag::read_from(reader)?;
|
||||
let tag = ogg::flac::read_from(reader)?;
|
||||
|
||||
tag.try_into()?
|
||||
},
|
||||
|
@ -380,6 +378,10 @@ impl AudioTagEdit for OggTag {
|
|||
fn remove_key(&mut self, key: &str) {
|
||||
self.remove_key(key)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &FileProperties {
|
||||
&self.properties
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioTagWrite for OggTag {
|
||||
|
|
|
@ -150,6 +150,10 @@ impl AudioTagEdit for RiffTag {
|
|||
fn remove_key(&mut self, key: &str) {
|
||||
self.remove_key(key)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &FileProperties {
|
||||
&self.properties
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioTagWrite for RiffTag {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#[allow(clippy::wildcard_imports)]
|
||||
use crate::components::tags::*;
|
||||
use crate::{Album, AnyTag, Picture, Result, TagType};
|
||||
use crate::{Album, AnyTag, Picture, Result, TagType, FileProperties};
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::fs::{File, OpenOptions};
|
||||
|
@ -128,6 +128,9 @@ pub trait AudioTagEdit {
|
|||
///
|
||||
/// See [`get_key`][AudioTagEdit::get_key]'s note
|
||||
fn remove_key(&mut self, _key: &str) {}
|
||||
|
||||
/// Returns the [`FileProperties`][crate::FileProperties]
|
||||
fn properties(&self) -> &FileProperties;
|
||||
}
|
||||
|
||||
/// Functions for writing to a file
|
||||
|
|
Loading…
Reference in a new issue