WavPack: Implement property reading

This commit is contained in:
Serial 2022-05-30 14:55:20 -04:00
parent 508185c48c
commit 6f3d569c21
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
5 changed files with 286 additions and 9 deletions

View file

@ -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>,
{

View file

@ -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 => {

View file

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

View file

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

View file

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