RIFF property reading

This commit is contained in:
Serial 2021-07-28 15:32:39 -04:00
parent e839a1012a
commit e530fc0a04
5 changed files with 279 additions and 107 deletions

View file

@ -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)?,
);
},

View file

@ -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(())
}

View file

@ -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)?;

View file

@ -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,
})
}

View file

@ -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);