mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-14 14:42:33 +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 read;
|
||||||
pub(crate) mod write;
|
pub(crate) mod write;
|
||||||
|
|
||||||
|
#[cfg(feature = "format-flac")]
|
||||||
|
pub(crate) mod flac;
|
||||||
#[cfg(feature = "format-opus")]
|
#[cfg(feature = "format-opus")]
|
||||||
mod opus;
|
mod opus;
|
||||||
#[cfg(feature = "format-vorbis")]
|
#[cfg(feature = "format-vorbis")]
|
||||||
|
|
|
@ -39,6 +39,50 @@ where
|
||||||
Ok(properties)
|
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>(
|
pub(crate) fn read_from<T>(
|
||||||
data: &mut T,
|
data: &mut T,
|
||||||
header_sig: &[u8],
|
header_sig: &[u8],
|
||||||
|
@ -75,40 +119,9 @@ where
|
||||||
let mut pictures = Vec::new();
|
let mut pictures = Vec::new();
|
||||||
|
|
||||||
let reader = &mut &md_pages[..];
|
let reader = &mut &md_pages[..];
|
||||||
|
let vendor = read_comments(reader, &mut md, &mut pictures)?;
|
||||||
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 properties = read_properties(data, header_sig, &first_page)?;
|
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 = find_last_page(data)?;
|
||||||
let last_page_abgp = last_page.abgp;
|
let last_page_abgp = last_page.abgp;
|
||||||
|
|
||||||
last_page_abgp.checked_sub(first_page_abgp).map_or_else(|| Err(LoftyError::InvalidData(
|
last_page_abgp.checked_sub(first_page_abgp).map_or_else(
|
||||||
"OGG file contains incorrect PCM values",
|
|| {
|
||||||
)), |frame_count| {
|
Err(LoftyError::InvalidData(
|
||||||
let length = frame_count * 1000 / u64::from(sample_rate);
|
"OGG file contains incorrect PCM values",
|
||||||
let duration = Duration::from_millis(length as u64);
|
))
|
||||||
let bitrate = ((audio_size * 8) / length) as u32;
|
},
|
||||||
|
|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 {
|
Ok(FileProperties {
|
||||||
duration,
|
duration,
|
||||||
bitrate: Some(bitrate),
|
bitrate: Some(bitrate),
|
||||||
sample_rate: Some(sample_rate),
|
sample_rate: Some(sample_rate),
|
||||||
channels: Some(channels),
|
channels: Some(channels),
|
||||||
})
|
})
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_to(
|
pub fn write_to(
|
||||||
|
|
|
@ -80,6 +80,10 @@ impl AudioTagEdit for AiffTag {
|
||||||
fn tag_type(&self) -> TagType {
|
fn tag_type(&self) -> TagType {
|
||||||
TagType::AiffText
|
TagType::AiffText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn properties(&self) -> &FileProperties {
|
||||||
|
&self.properties
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioTagWrite for AiffTag {
|
impl AudioTagWrite for AiffTag {
|
||||||
|
|
|
@ -294,6 +294,10 @@ impl AudioTagEdit for ApeTag {
|
||||||
fn remove_key(&mut self, key: &str) {
|
fn remove_key(&mut self, key: &str) {
|
||||||
self.remove_key(key)
|
self.remove_key(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn properties(&self) -> &FileProperties {
|
||||||
|
&self.properties
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioTagWrite for ApeTag {
|
impl AudioTagWrite for ApeTag {
|
||||||
|
|
|
@ -389,6 +389,10 @@ impl AudioTagEdit for Id3v2Tag {
|
||||||
fn remove_key(&mut self, key: &str) {
|
fn remove_key(&mut self, key: &str) {
|
||||||
self.inner.remove(key)
|
self.inner.remove(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn properties(&self) -> &FileProperties {
|
||||||
|
&self.properties
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioTagWrite for Id3v2Tag {
|
impl AudioTagWrite for Id3v2Tag {
|
||||||
|
|
|
@ -314,6 +314,10 @@ impl AudioTagEdit for Mp4Tag {
|
||||||
fn tag_type(&self) -> TagType {
|
fn tag_type(&self) -> TagType {
|
||||||
TagType::Mp4
|
TagType::Mp4
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn properties(&self) -> &FileProperties {
|
||||||
|
&self.properties
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioTagWrite for Mp4Tag {
|
impl AudioTagWrite for Mp4Tag {
|
||||||
|
|
|
@ -10,8 +10,6 @@ use crate::{
|
||||||
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, FileProperties, LoftyError, OggFormat,
|
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, FileProperties, LoftyError, OggFormat,
|
||||||
Picture, PictureType, Result, TagType, ToAny, ToAnyTag,
|
Picture, PictureType, Result, TagType, ToAny, ToAnyTag,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(feature = "format-opus", feature = "format-vorbis"))]
|
|
||||||
use crate::components::logic::ogg::read::OGGTags;
|
use crate::components::logic::ogg::read::OGGTags;
|
||||||
|
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
@ -161,7 +159,7 @@ impl OggTag {
|
||||||
},
|
},
|
||||||
#[cfg(feature = "format-flac")]
|
#[cfg(feature = "format-flac")]
|
||||||
OggFormat::Flac => {
|
OggFormat::Flac => {
|
||||||
let tag = metaflac::Tag::read_from(reader)?;
|
let tag = ogg::flac::read_from(reader)?;
|
||||||
|
|
||||||
tag.try_into()?
|
tag.try_into()?
|
||||||
},
|
},
|
||||||
|
@ -380,6 +378,10 @@ impl AudioTagEdit for OggTag {
|
||||||
fn remove_key(&mut self, key: &str) {
|
fn remove_key(&mut self, key: &str) {
|
||||||
self.remove_key(key)
|
self.remove_key(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn properties(&self) -> &FileProperties {
|
||||||
|
&self.properties
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioTagWrite for OggTag {
|
impl AudioTagWrite for OggTag {
|
||||||
|
|
|
@ -150,6 +150,10 @@ impl AudioTagEdit for RiffTag {
|
||||||
fn remove_key(&mut self, key: &str) {
|
fn remove_key(&mut self, key: &str) {
|
||||||
self.remove_key(key)
|
self.remove_key(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn properties(&self) -> &FileProperties {
|
||||||
|
&self.properties
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AudioTagWrite for RiffTag {
|
impl AudioTagWrite for RiffTag {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#[allow(clippy::wildcard_imports)]
|
#[allow(clippy::wildcard_imports)]
|
||||||
use crate::components::tags::*;
|
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::borrow::Cow;
|
||||||
use std::fs::{File, OpenOptions};
|
use std::fs::{File, OpenOptions};
|
||||||
|
@ -128,6 +128,9 @@ pub trait AudioTagEdit {
|
||||||
///
|
///
|
||||||
/// See [`get_key`][AudioTagEdit::get_key]'s note
|
/// See [`get_key`][AudioTagEdit::get_key]'s note
|
||||||
fn remove_key(&mut self, _key: &str) {}
|
fn remove_key(&mut self, _key: &str) {}
|
||||||
|
|
||||||
|
/// Returns the [`FileProperties`][crate::FileProperties]
|
||||||
|
fn properties(&self) -> &FileProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Functions for writing to a file
|
/// Functions for writing to a file
|
||||||
|
|
Loading…
Reference in a new issue