mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-13 22:22:31 +00:00
RIFF property reading
This commit is contained in:
parent
e839a1012a
commit
e530fc0a04
5 changed files with 279 additions and 107 deletions
|
@ -24,6 +24,11 @@ where
|
|||
|
||||
pub(crate) fn read_properties(comm: &mut &[u8], stream_len: u32) -> Result<FileProperties> {
|
||||
let channels = comm.read_u16::<BigEndian>()? as u8;
|
||||
|
||||
if channels == 0 {
|
||||
return Err(LoftyError::InvalidData("AIFF file contains 0 channels"));
|
||||
}
|
||||
|
||||
let sample_frames = comm.read_u32::<BigEndian>()?;
|
||||
let _sample_size = comm.read_u16::<BigEndian>()?;
|
||||
|
||||
|
@ -87,19 +92,16 @@ where
|
|||
let mut metadata = HashMap::<String, String>::new();
|
||||
let mut id3 = Vec::new();
|
||||
|
||||
while let (Ok(fourcc), Ok(size)) = (
|
||||
data.read_u32::<LittleEndian>(),
|
||||
data.read_u32::<BigEndian>(),
|
||||
) {
|
||||
let fourcc_b = &fourcc.to_le_bytes();
|
||||
let mut fourcc = [0; 4];
|
||||
|
||||
match fourcc_b {
|
||||
while let (Ok(()), Ok(size)) = (data.read_exact(&mut fourcc), data.read_u32::<BigEndian>()) {
|
||||
match &fourcc {
|
||||
b"NAME" | b"AUTH" | b"(c) " => {
|
||||
let mut value = vec![0; size as usize];
|
||||
data.read_exact(&mut value)?;
|
||||
|
||||
metadata.insert(
|
||||
String::from_utf8(fourcc_b.to_vec())?,
|
||||
String::from_utf8(fourcc.to_vec())?,
|
||||
String::from_utf8(value)?,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -1,12 +1,108 @@
|
|||
use crate::{LoftyError, Result};
|
||||
use crate::components::logic::iff::IffData;
|
||||
use crate::{FileProperties, LoftyError, Result};
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
|
||||
pub(crate) fn read_from<T>(data: &mut T) -> Result<HashMap<String, String>>
|
||||
const PCM: u16 = 0x0001;
|
||||
const IEEE_FLOAT: u16 = 0x0003;
|
||||
const EXTENSIBLE: u16 = 0xfffe;
|
||||
|
||||
fn verify_riff<T>(data: &mut T) -> Result<()>
|
||||
where
|
||||
T: Read + Seek,
|
||||
{
|
||||
let mut id = [0; 4];
|
||||
data.read_exact(&mut id)?;
|
||||
|
||||
if &id != b"RIFF" {
|
||||
return Err(LoftyError::Riff("RIFF file doesn't contain a RIFF chunk"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn read_properties(
|
||||
fmt: &mut &[u8],
|
||||
total_samples: u32,
|
||||
stream_len: u32,
|
||||
) -> Result<FileProperties> {
|
||||
let mut format_tag = fmt.read_u16::<LittleEndian>()?;
|
||||
let channels = fmt.read_u16::<LittleEndian>()? as u8;
|
||||
|
||||
if channels == 0 {
|
||||
return Err(LoftyError::Riff("File contains 0 channels"));
|
||||
}
|
||||
|
||||
let sample_rate = fmt.read_u32::<LittleEndian>()?;
|
||||
let bytes_per_second = fmt.read_u32::<LittleEndian>()?;
|
||||
|
||||
// Skip 2 bytes
|
||||
// Block align (2)
|
||||
let _ = fmt.read_u16::<LittleEndian>()?;
|
||||
|
||||
let bits_per_sample = fmt.read_u16::<LittleEndian>()?;
|
||||
|
||||
if format_tag == EXTENSIBLE {
|
||||
if fmt.len() < 40 {
|
||||
return Err(LoftyError::Riff(
|
||||
"Extensible format identified, invalid \"fmt \" chunk size found (< 40)",
|
||||
));
|
||||
}
|
||||
|
||||
// Skip 8 bytes
|
||||
// cbSize (Size of extra format information) (2)
|
||||
// Valid bits per sample (2)
|
||||
// Channel mask (4)
|
||||
let _ = fmt.read_u64::<LittleEndian>()?;
|
||||
|
||||
format_tag = fmt.read_u16::<LittleEndian>()?;
|
||||
}
|
||||
|
||||
let non_pcm = format_tag != PCM && format_tag != IEEE_FLOAT;
|
||||
|
||||
if non_pcm && total_samples == 0 {
|
||||
return Err(LoftyError::Riff(
|
||||
"Non-PCM format identified, no \"fact\" chunk found",
|
||||
));
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
let (duration, 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,
|
||||
)
|
||||
} 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)
|
||||
} else {
|
||||
(Duration::ZERO, 0)
|
||||
};
|
||||
|
||||
Ok(FileProperties::new(
|
||||
duration,
|
||||
Some(bitrate),
|
||||
Some(sample_rate),
|
||||
Some(channels),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn read_from<T>(data: &mut T) -> Result<IffData>
|
||||
where
|
||||
T: Read + Seek,
|
||||
{
|
||||
|
@ -14,37 +110,102 @@ where
|
|||
|
||||
data.seek(SeekFrom::Current(8))?;
|
||||
|
||||
find_info_list(data)?;
|
||||
let mut stream_len = 0_u32;
|
||||
let mut total_samples = 0_u32;
|
||||
let mut fmt = Vec::new();
|
||||
|
||||
let info_list_size = data.read_u32::<LittleEndian>()?;
|
||||
let mut metadata = HashMap::<String, String>::new();
|
||||
let mut id3 = Vec::new();
|
||||
|
||||
let mut info_list = vec![0; info_list_size as usize];
|
||||
data.read_exact(&mut info_list)?;
|
||||
let mut fourcc = [0; 4];
|
||||
|
||||
let mut cursor = Cursor::new(&*info_list);
|
||||
cursor.seek(SeekFrom::Start(4))?; // Skip the chunk ID
|
||||
while let (Ok(()), Ok(size)) = (
|
||||
data.read_exact(&mut fourcc),
|
||||
data.read_u32::<LittleEndian>(),
|
||||
) {
|
||||
match &fourcc {
|
||||
b"fmt " => {
|
||||
if fmt.is_empty() {
|
||||
let mut value = vec![0; size as usize];
|
||||
data.read_exact(&mut value)?;
|
||||
|
||||
let mut metadata: HashMap<String, String> = HashMap::new();
|
||||
fmt = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
#[allow(clippy::cast_lossless)]
|
||||
while cursor.position() < info_list_size as u64 {
|
||||
if cursor.read_u8()? != 0 {
|
||||
cursor.seek(SeekFrom::Current(-1))?;
|
||||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
},
|
||||
b"fact" => {
|
||||
if total_samples == 0 {
|
||||
total_samples = data.read_u32::<LittleEndian>()?;
|
||||
continue;
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Current(4))?;
|
||||
},
|
||||
b"data" => {
|
||||
if stream_len == 0 {
|
||||
stream_len += size
|
||||
}
|
||||
|
||||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
},
|
||||
b"LIST" => {
|
||||
let mut list_type = [0; 4];
|
||||
data.read_exact(&mut list_type)?;
|
||||
|
||||
if &list_type == b"INFO" {
|
||||
let end = data.seek(SeekFrom::Current(0))? + u64::from(size - 4);
|
||||
|
||||
while data.seek(SeekFrom::Current(0))? != end {
|
||||
let mut fourcc = vec![0; 4];
|
||||
data.read_exact(&mut fourcc)?;
|
||||
|
||||
let key = String::from_utf8(fourcc)?;
|
||||
let size = data.read_u32::<LittleEndian>()?;
|
||||
|
||||
let mut buf = vec![0; size as usize];
|
||||
data.read_exact(&mut buf)?;
|
||||
|
||||
let val = String::from_utf8(buf)?;
|
||||
metadata.insert(key.to_string(), val.trim_matches('\0').to_string());
|
||||
|
||||
if data.read_u8()? != 0 {
|
||||
data.seek(SeekFrom::Current(-1))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
b"ID3 " | b"id3 " => {
|
||||
let mut value = vec![0; size as usize];
|
||||
data.read_exact(&mut value)?;
|
||||
|
||||
id3 = value
|
||||
},
|
||||
_ => {
|
||||
data.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
},
|
||||
}
|
||||
|
||||
let mut fourcc = vec![0; 4];
|
||||
cursor.read_exact(&mut fourcc)?;
|
||||
|
||||
let key = String::from_utf8(fourcc)?;
|
||||
let size = cursor.read_u32::<LittleEndian>()?;
|
||||
|
||||
let mut buf = vec![0; size as usize];
|
||||
cursor.read_exact(&mut buf)?;
|
||||
|
||||
let val = String::from_utf8(buf)?;
|
||||
metadata.insert(key.to_string(), val.trim_matches('\0').to_string());
|
||||
}
|
||||
|
||||
if fmt.len() < 16 {
|
||||
return Err(LoftyError::Riff(
|
||||
"File does not contain a valid \"fmt \" chunk",
|
||||
));
|
||||
}
|
||||
|
||||
if stream_len == 0 {
|
||||
return Err(LoftyError::Riff("File does not contain a \"data\" chunk"));
|
||||
}
|
||||
|
||||
let properties = read_properties(&mut &*fmt, total_samples, stream_len)?;
|
||||
|
||||
let metadata = IffData {
|
||||
properties,
|
||||
metadata,
|
||||
id3: (!id3.is_empty()).then(|| id3),
|
||||
};
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
|
@ -75,77 +236,67 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn verify_riff<T>(data: &mut T) -> Result<()>
|
||||
where
|
||||
T: Read + Seek,
|
||||
{
|
||||
let mut id = [0; 4];
|
||||
data.read_exact(&mut id)?;
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "format-riff")] {
|
||||
pub(crate) fn write_to(data: &mut File, metadata: HashMap<String, String>) -> Result<()> {
|
||||
let mut packet = Vec::new();
|
||||
|
||||
if &id != b"RIFF" {
|
||||
return Err(LoftyError::Riff("RIFF file doesn't contain a RIFF chunk"));
|
||||
}
|
||||
packet.extend(b"LIST".iter());
|
||||
packet.extend(b"INFO".iter());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
for (k, v) in metadata {
|
||||
let mut val = v.as_bytes().to_vec();
|
||||
|
||||
pub(crate) fn write_to(data: &mut File, metadata: HashMap<String, String>) -> Result<()> {
|
||||
let mut packet = Vec::new();
|
||||
if val.len() % 2 != 0 {
|
||||
val.push(0)
|
||||
}
|
||||
|
||||
packet.extend(b"LIST".iter());
|
||||
packet.extend(b"INFO".iter());
|
||||
let size = val.len() as u32;
|
||||
|
||||
for (k, v) in metadata {
|
||||
let mut val = v.as_bytes().to_vec();
|
||||
packet.extend(k.as_bytes().iter());
|
||||
packet.extend(size.to_le_bytes().iter());
|
||||
packet.extend(val.iter());
|
||||
}
|
||||
|
||||
if val.len() % 2 != 0 {
|
||||
val.push(0)
|
||||
let packet_size = packet.len() - 4;
|
||||
|
||||
if packet_size > u32::MAX as usize {
|
||||
return Err(LoftyError::TooMuchData);
|
||||
}
|
||||
|
||||
let size = (packet_size as u32).to_le_bytes();
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..4 {
|
||||
packet.insert(i + 4, size[i]);
|
||||
}
|
||||
|
||||
verify_riff(data)?;
|
||||
|
||||
data.seek(SeekFrom::Current(8))?;
|
||||
|
||||
find_info_list(data)?;
|
||||
|
||||
let info_list_size = data.read_u32::<LittleEndian>()? as usize;
|
||||
data.seek(SeekFrom::Current(-8))?;
|
||||
|
||||
let info_list_start = data.seek(SeekFrom::Current(0))? as usize;
|
||||
let info_list_end = info_list_start + 8 + info_list_size;
|
||||
|
||||
data.seek(SeekFrom::Start(0))?;
|
||||
let mut file_bytes = Vec::new();
|
||||
data.read_to_end(&mut file_bytes)?;
|
||||
|
||||
let _ = file_bytes.splice(info_list_start..info_list_end, packet);
|
||||
|
||||
let total_size = (file_bytes.len() - 8) as u32;
|
||||
let _ = file_bytes.splice(4..8, total_size.to_le_bytes().to_vec());
|
||||
|
||||
data.seek(SeekFrom::Start(0))?;
|
||||
data.set_len(0)?;
|
||||
data.write_all(&*file_bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let size = val.len() as u32;
|
||||
|
||||
packet.extend(k.as_bytes().iter());
|
||||
packet.extend(size.to_le_bytes().iter());
|
||||
packet.extend(val.iter());
|
||||
}
|
||||
|
||||
let packet_size = packet.len() - 4;
|
||||
|
||||
if packet_size > u32::MAX as usize {
|
||||
return Err(LoftyError::TooMuchData);
|
||||
}
|
||||
|
||||
let size = (packet_size as u32).to_le_bytes();
|
||||
|
||||
#[allow(clippy::needless_range_loop)]
|
||||
for i in 0..4 {
|
||||
packet.insert(i + 4, size[i]);
|
||||
}
|
||||
|
||||
verify_riff(data)?;
|
||||
|
||||
data.seek(SeekFrom::Current(8))?;
|
||||
|
||||
find_info_list(data)?;
|
||||
|
||||
let info_list_size = data.read_u32::<LittleEndian>()? as usize;
|
||||
data.seek(SeekFrom::Current(-8))?;
|
||||
|
||||
let info_list_start = data.seek(SeekFrom::Current(0))? as usize;
|
||||
let info_list_end = info_list_start + 8 + info_list_size;
|
||||
|
||||
data.seek(SeekFrom::Start(0))?;
|
||||
let mut file_bytes = Vec::new();
|
||||
data.read_to_end(&mut file_bytes)?;
|
||||
|
||||
let _ = file_bytes.splice(info_list_start..info_list_end, packet);
|
||||
|
||||
let total_size = (file_bytes.len() - 8) as u32;
|
||||
let _ = file_bytes.splice(4..8, total_size.to_le_bytes().to_vec());
|
||||
|
||||
data.seek(SeekFrom::Start(0))?;
|
||||
data.set_len(0)?;
|
||||
data.write_all(&*file_bytes)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::components::logic::iff::aiff;
|
||||
use crate::components::logic::iff::{aiff, riff};
|
||||
use crate::tag::Id3Format;
|
||||
use crate::{
|
||||
Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, FileProperties, LoftyError, MimeType,
|
||||
|
@ -35,10 +35,16 @@ impl Id3v2Tag {
|
|||
FileProperties::default(), // TODO
|
||||
Id3v2InnerTag::read_from(reader)?,
|
||||
),
|
||||
Id3Format::Riff => (
|
||||
FileProperties::default(), // TODO
|
||||
Id3v2InnerTag::read_from_wav_reader(reader)?,
|
||||
),
|
||||
Id3Format::Riff => {
|
||||
let data = riff::read_from(reader)?;
|
||||
|
||||
let inner = match data.id3 {
|
||||
Some(id3) => Id3v2InnerTag::read_from(Cursor::new(id3))?,
|
||||
None => Id3v2InnerTag::new(),
|
||||
};
|
||||
|
||||
(data.properties, inner)
|
||||
},
|
||||
Id3Format::Aiff => {
|
||||
let data = aiff::read_from(reader)?;
|
||||
|
||||
|
|
|
@ -38,11 +38,13 @@ impl RiffTag {
|
|||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let data = riff::read_from(reader)?;
|
||||
|
||||
Ok(Self {
|
||||
inner: RiffInnerTag {
|
||||
data: riff::read_from(reader)?,
|
||||
data: data.metadata,
|
||||
},
|
||||
properties: FileProperties::default(), // TODO
|
||||
properties: data.properties,
|
||||
_format: TagType::RiffInfo,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -22,6 +22,13 @@ const AIFF_PROPERTIES: FileProperties = FileProperties::new(
|
|||
Some(2),
|
||||
);
|
||||
|
||||
const RIFF_PROPERTIES: FileProperties = FileProperties::new(
|
||||
Duration::from_millis(1428),
|
||||
Some(1536),
|
||||
Some(48000),
|
||||
Some(2),
|
||||
);
|
||||
|
||||
macro_rules! properties_test {
|
||||
($function:ident, $path:expr, $expected:ident) => {
|
||||
#[test]
|
||||
|
@ -36,8 +43,12 @@ macro_rules! properties_test {
|
|||
};
|
||||
}
|
||||
|
||||
properties_test!(test_aiff_id3, "tests/assets/a.aiff", AIFF_PROPERTIES);
|
||||
properties_test!(test_aiff_text, "tests/assets/a_text.aiff", AIFF_PROPERTIES);
|
||||
|
||||
properties_test!(test_opus, "tests/assets/a.opus", OPUS_PROPERTIES);
|
||||
properties_test!(test_vorbis, "tests/assets/a.ogg", VORBIS_PROPERTIES);
|
||||
properties_test!(test_flac, "tests/assets/a.flac", FLAC_PROPERTIES);
|
||||
properties_test!(test_aiff_text, "tests/assets/a_text.aiff", AIFF_PROPERTIES);
|
||||
properties_test!(test_aiff, "tests/assets/a.aiff", AIFF_PROPERTIES);
|
||||
|
||||
properties_test!(test_wav_id3, "tests/assets/a-id3.wav", RIFF_PROPERTIES);
|
||||
properties_test!(test_wav_info, "tests/assets/a.wav", RIFF_PROPERTIES);
|
||||
|
|
Loading…
Reference in a new issue