mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-14 14:42:33 +00:00
WavPack: Implement property reading
This commit is contained in:
parent
508185c48c
commit
6f3d569c21
5 changed files with 286 additions and 9 deletions
|
@ -14,7 +14,7 @@ use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
|||
|
||||
use byteorder::{BigEndian, WriteBytesExt};
|
||||
|
||||
pub(in crate) fn write_to<'a, I: 'a>(data: &mut File, tag: &mut IlstRef<'a, I>) -> Result<()>
|
||||
pub(crate) fn write_to<'a, I: 'a>(data: &mut File, tag: &mut IlstRef<'a, I>) -> Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = &'a AtomData>,
|
||||
{
|
||||
|
|
|
@ -33,7 +33,7 @@ impl OGGFormat {
|
|||
}
|
||||
}
|
||||
|
||||
pub(in crate) fn write_to(file: &mut File, tag: &Tag, file_type: FileType) -> Result<()> {
|
||||
pub(crate) fn write_to(file: &mut File, tag: &Tag, file_type: FileType) -> Result<()> {
|
||||
match tag.tag_type() {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
TagType::VorbisComments => {
|
||||
|
|
|
@ -196,14 +196,14 @@ mod tests {
|
|||
};
|
||||
|
||||
const WAVPACK_PROPERTIES: WavPackProperties = WavPackProperties {
|
||||
version: 0,
|
||||
version: 1040,
|
||||
duration: Duration::from_millis(1428),
|
||||
overall_bitrate: 598,
|
||||
audio_bitrate: 598,
|
||||
audio_bitrate: 597,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
bit_depth: 16,
|
||||
lossless: false,
|
||||
lossless: true,
|
||||
};
|
||||
|
||||
fn get_properties<T>(path: &str) -> T::Properties
|
||||
|
@ -307,7 +307,6 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // TODO
|
||||
fn wavpack_properties() {
|
||||
assert_eq!(
|
||||
get_properties::<WavPackFile>("tests/files/assets/minimal/full_test.wv"),
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
use crate::error::{ErrorKind, FileDecodingError, LoftyError, Result};
|
||||
use crate::file::FileType;
|
||||
use crate::macros::try_vec;
|
||||
use crate::properties::FileProperties;
|
||||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::time::Duration;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||
#[non_exhaustive]
|
||||
/// A WavPack file's audio properties
|
||||
|
@ -65,3 +71,261 @@ impl WavPackProperties {
|
|||
self.lossless
|
||||
}
|
||||
}
|
||||
|
||||
// Thanks MultimediaWiki :)
|
||||
|
||||
// https://wiki.multimedia.cx/index.php?title=WavPack#Block_structure
|
||||
|
||||
const BYTES_PER_SAMPLE_MASK: u32 = 3;
|
||||
const BIT_DEPTH_SHL: u32 = 13;
|
||||
const BIT_DEPTH_SHIFT_MASK: u32 = 0x1F << BIT_DEPTH_SHL;
|
||||
const FLAG_INITIAL_BLOCK: u32 = 0x800;
|
||||
const FLAG_FINAL_BLOCK: u32 = 0x1000;
|
||||
const FLAG_MONO: u32 = 0x0004;
|
||||
const FLAG_DSD: u32 = 0x8000_0000;
|
||||
const FLAG_HYBRID_COMPRESSION: u32 = 8; // Hybrid profile (lossy compression)
|
||||
|
||||
// https://wiki.multimedia.cx/index.php?title=WavPack#Metadata
|
||||
|
||||
const ID_FLAG_ODD_SIZE: u8 = 0x40;
|
||||
const ID_FLAG_LARGE_SIZE: u8 = 0x80;
|
||||
|
||||
const ID_MULTICHANNEL: u8 = 0x0D;
|
||||
const ID_NON_STANDARD_SAMPLE_RATE: u8 = 0x27;
|
||||
const ID_DSD: u8 = 0xE;
|
||||
|
||||
const MIN_STREAM_VERSION: u16 = 0x402;
|
||||
const MAX_STREAM_VERSION: u16 = 0x410;
|
||||
|
||||
const SAMPLE_RATES: [u32; 16] = [
|
||||
6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000,
|
||||
192_000, 0,
|
||||
];
|
||||
|
||||
#[rustfmt::skip]
|
||||
pub(super) fn read_properties<R>(reader: &mut R, stream_length: u64) -> Result<WavPackProperties>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut properties = WavPackProperties::default();
|
||||
|
||||
let mut offset = 0;
|
||||
let mut total_samples = 0;
|
||||
loop {
|
||||
reader.seek(SeekFrom::Start(offset))?;
|
||||
|
||||
let block_header;
|
||||
match parse_wv_header(reader) {
|
||||
Ok(header) => block_header = header,
|
||||
_ => break,
|
||||
}
|
||||
|
||||
// Just skip any block with no samples
|
||||
if block_header.samples == 0 {
|
||||
offset += u64::from(block_header.block_size + 8);
|
||||
continue;
|
||||
}
|
||||
|
||||
let flags = block_header.flags;
|
||||
|
||||
let sample_rate_idx = ((flags >> 23) & 0xF) as usize;
|
||||
let sample_rate = SAMPLE_RATES[sample_rate_idx];
|
||||
|
||||
// In the case of non-standard sample rates and DSD audio, we need to actually read the
|
||||
// block to get the sample rate
|
||||
if sample_rate == 0 || flags & FLAG_DSD == FLAG_DSD {
|
||||
let mut block_contents = try_vec![0; (block_header.block_size - 24) as usize];
|
||||
if reader.read_exact(&mut block_contents).is_err() {
|
||||
// need some warning...
|
||||
break;
|
||||
}
|
||||
|
||||
if get_extended_meta_info(reader, &mut properties, block_contents.len() as u64).is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
properties.sample_rate = sample_rate;
|
||||
}
|
||||
|
||||
if (flags & FLAG_INITIAL_BLOCK) == FLAG_INITIAL_BLOCK {
|
||||
if block_header.version < MIN_STREAM_VERSION
|
||||
|| block_header.version > MAX_STREAM_VERSION
|
||||
{
|
||||
// TODO: some warning
|
||||
break;
|
||||
}
|
||||
|
||||
total_samples = block_header.total_samples;
|
||||
properties.bit_depth = ((((flags & BYTES_PER_SAMPLE_MASK) + 1) * 8) - ((flags & BIT_DEPTH_SHIFT_MASK) >> BIT_DEPTH_SHL)) as u8;
|
||||
properties.version = block_header.version;
|
||||
properties.lossless = flags & FLAG_HYBRID_COMPRESSION == 0;
|
||||
}
|
||||
|
||||
let is_mono = flags & FLAG_MONO > 0;
|
||||
properties.channels = if is_mono { 1 } else { 2 };
|
||||
|
||||
if flags & FLAG_FINAL_BLOCK == FLAG_FINAL_BLOCK {
|
||||
break;
|
||||
}
|
||||
|
||||
offset += u64::from(block_header.block_size + 8);
|
||||
}
|
||||
|
||||
if total_samples > 0 && properties.sample_rate > 0 {
|
||||
let length = u64::from(total_samples * 1000 / properties.sample_rate);
|
||||
properties.duration = Duration::from_millis(length);
|
||||
properties.audio_bitrate = (stream_length * 8 / length) as u32;
|
||||
|
||||
let file_length = reader.seek(SeekFrom::End(0))?;
|
||||
properties.overall_bitrate = (file_length * 8 / length) as u32;
|
||||
}
|
||||
|
||||
Ok(properties)
|
||||
}
|
||||
|
||||
// According to the spec, the max block size is 1MB
|
||||
const WV_BLOCK_MAX_SIZE: u32 = 1_048_576;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WVHeader {
|
||||
version: u16,
|
||||
block_size: u32,
|
||||
total_samples: u32,
|
||||
samples: u32,
|
||||
flags: u32,
|
||||
}
|
||||
|
||||
// TODO: for now, all errors are just discarded
|
||||
fn parse_wv_header<R>(reader: &mut R) -> Result<WVHeader>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let mut wv_ident = [0; 4];
|
||||
reader.read_exact(&mut wv_ident)?;
|
||||
|
||||
if &wv_ident != b"wvpk" {
|
||||
return Err(LoftyError::new(ErrorKind::UnknownFormat));
|
||||
}
|
||||
|
||||
let block_size = reader.read_u32::<LittleEndian>()?;
|
||||
if !(24..=WV_BLOCK_MAX_SIZE).contains(&block_size) {
|
||||
return Err(
|
||||
FileDecodingError::new(FileType::WavPack, "WavPack block has an invalid size").into(),
|
||||
);
|
||||
}
|
||||
|
||||
let version = reader.read_u16::<LittleEndian>()?;
|
||||
|
||||
// Skip 2 bytes
|
||||
//
|
||||
// Track number (1)
|
||||
// Track sub index (1)
|
||||
reader.seek(SeekFrom::Current(2))?;
|
||||
|
||||
let total_samples = reader.read_u32::<LittleEndian>()?;
|
||||
let _block_idx = reader.seek(SeekFrom::Current(4))?;
|
||||
let samples = reader.read_u32::<LittleEndian>()?;
|
||||
let flags = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
let _crc = reader.seek(SeekFrom::Current(4))?;
|
||||
|
||||
Ok(WVHeader {
|
||||
version,
|
||||
block_size,
|
||||
total_samples,
|
||||
samples,
|
||||
flags,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_extended_meta_info<R>(
|
||||
reader: &mut R,
|
||||
properties: &mut WavPackProperties,
|
||||
block_size: u64,
|
||||
) -> Result<()>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
while reader.stream_position()? < block_size {
|
||||
let id = reader.read_u8()?;
|
||||
|
||||
let is_large = id & ID_FLAG_LARGE_SIZE > 0;
|
||||
let mut size = if is_large {
|
||||
reader.read_u24::<LittleEndian>()? << 1
|
||||
} else {
|
||||
u32::from(reader.read_u8()?) << 1
|
||||
};
|
||||
|
||||
if id & ID_FLAG_ODD_SIZE > 0 {
|
||||
size -= 1;
|
||||
}
|
||||
|
||||
match id & 0x3F {
|
||||
ID_NON_STANDARD_SAMPLE_RATE => {
|
||||
properties.sample_rate = reader.read_u24::<LittleEndian>()?;
|
||||
},
|
||||
ID_DSD => {
|
||||
if size <= 1 {
|
||||
return Err(FileDecodingError::new(
|
||||
FileType::WavPack,
|
||||
"Encountered an invalid DSD block size",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
let rate_multiplier = u32::from(reader.read_u8()?);
|
||||
if let (sample_rate, false) =
|
||||
properties.sample_rate.overflowing_shl(rate_multiplier)
|
||||
{
|
||||
properties.sample_rate = sample_rate;
|
||||
}
|
||||
|
||||
reader.seek(SeekFrom::Current(i64::from(size - 1)))?;
|
||||
},
|
||||
ID_MULTICHANNEL => {
|
||||
if size <= 1 {
|
||||
return Err(FileDecodingError::new(
|
||||
FileType::WavPack,
|
||||
"Unable to extract channel information",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
properties.channels = reader.read_u8()?;
|
||||
let s = size - 2;
|
||||
match s {
|
||||
0..=3 => {
|
||||
reader.seek(SeekFrom::Current(i64::from(s + 1)))?;
|
||||
continue;
|
||||
},
|
||||
4 | 5 => {},
|
||||
_ => {
|
||||
return Err(FileDecodingError::new(
|
||||
FileType::WavPack,
|
||||
"Encountered invalid channel info size",
|
||||
)
|
||||
.into())
|
||||
},
|
||||
}
|
||||
|
||||
reader.seek(SeekFrom::Current(1))?;
|
||||
|
||||
properties.channels |= reader.read_u8()? & 0xF;
|
||||
properties.channels += 1;
|
||||
|
||||
// Skip the Microsoft channel mask
|
||||
reader.seek(SeekFrom::Current(i64::from(s - 1)))?;
|
||||
},
|
||||
_ => {
|
||||
reader.seek(SeekFrom::Current(i64::from(size)))?;
|
||||
},
|
||||
}
|
||||
|
||||
if id & ID_FLAG_ODD_SIZE > 0 {
|
||||
reader.seek(SeekFrom::Current(1))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -8,10 +8,14 @@ use crate::id3::{find_id3v1, find_lyrics3v2, ID3FindResults};
|
|||
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
pub(super) fn read_from<R>(reader: &mut R, _read_properties: bool) -> Result<WavPackFile>
|
||||
pub(super) fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<WavPackFile>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
let current_pos = reader.stream_position()?;
|
||||
let mut stream_length = reader.seek(SeekFrom::End(0))?;
|
||||
reader.seek(SeekFrom::Start(current_pos))?;
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
let mut id3v1_tag = None;
|
||||
#[cfg(feature = "ape")]
|
||||
|
@ -20,6 +24,7 @@ where
|
|||
let ID3FindResults(id3v1_header, id3v1) = find_id3v1(reader, true)?;
|
||||
|
||||
if id3v1_header.is_some() {
|
||||
stream_length -= 128;
|
||||
#[cfg(feature = "id3v1")]
|
||||
{
|
||||
id3v1_tag = id3v1;
|
||||
|
@ -27,7 +32,11 @@ where
|
|||
}
|
||||
|
||||
// Next, check for a Lyrics3v2 tag, and skip over it, as it's no use to us
|
||||
let ID3FindResults(_lyrics3_header, _lyrics3v2_size) = find_lyrics3v2(reader)?;
|
||||
let ID3FindResults(lyrics3_header, lyrics3v2_size) = find_lyrics3v2(reader)?;
|
||||
|
||||
if lyrics3_header.is_some() {
|
||||
stream_length -= u64::from(lyrics3v2_size);
|
||||
}
|
||||
|
||||
// Next, search for an APE tag footer
|
||||
//
|
||||
|
@ -41,6 +50,7 @@ where
|
|||
|
||||
if &ape_preamble == APE_PREAMBLE {
|
||||
let ape_header = read_ape_header(reader, true)?;
|
||||
stream_length -= u64::from(ape_header.size);
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
{
|
||||
|
@ -57,6 +67,10 @@ where
|
|||
id3v1_tag,
|
||||
#[cfg(feature = "ape")]
|
||||
ape_tag,
|
||||
properties: WavPackProperties::default(),
|
||||
properties: if read_properties {
|
||||
super::properties::read_properties(reader, stream_length)?
|
||||
} else {
|
||||
WavPackProperties::default()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue