WavPack: Start basic implementation

This commit is contained in:
Serial 2022-05-29 22:14:40 -04:00
parent 500358a587
commit 74779369cf
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
10 changed files with 291 additions and 6 deletions

View file

@ -210,6 +210,7 @@ pub enum FileType {
Vorbis,
Speex,
WAV,
WavPack,
}
impl FileType {
@ -219,7 +220,7 @@ impl FileType {
/// | [`FileType`] | [`TagType`] |
/// |--------------------------|------------------|
/// | `AIFF`, `MP3`, `WAV` | `Id3v2` |
/// | `APE` | `Ape` |
/// | `APE` , `WavPack` | `Ape` |
/// | `FLAC`, `Opus`, `Vorbis` | `VorbisComments` |
/// | `MP4` | `Mp4Ilst` |
pub fn primary_tag_type(&self) -> TagType {
@ -234,8 +235,8 @@ impl FileType {
FileType::MP3 => TagType::Ape,
FileType::AIFF | FileType::MP3 | FileType::WAV => TagType::Id3v2,
#[cfg(all(not(feature = "ape"), feature = "id3v1"))]
FileType::MP3 => TagType::Id3v1,
FileType::APE => TagType::Ape,
FileType::MP3 | FileType::WavPack => TagType::Id3v1,
FileType::APE | FileType::WavPack => TagType::Ape,
FileType::FLAC | FileType::Opus | FileType::Vorbis | FileType::Speex => {
TagType::VorbisComments
},
@ -255,9 +256,9 @@ impl FileType {
#[cfg(feature = "aiff_text_chunks")]
FileType::AIFF if tag_type == TagType::AiffText => true,
#[cfg(feature = "id3v1")]
FileType::APE | FileType::MP3 if tag_type == TagType::Id3v1 => true,
FileType::APE | FileType::MP3 | FileType::WavPack if tag_type == TagType::Id3v1 => true,
#[cfg(feature = "ape")]
FileType::APE | FileType::MP3 if tag_type == TagType::Ape => true,
FileType::APE | FileType::MP3 | FileType::WavPack if tag_type == TagType::Ape => true,
#[cfg(feature = "vorbis_comments")]
FileType::Opus | FileType::FLAC | FileType::Vorbis | FileType::Speex => {
tag_type == TagType::VorbisComments
@ -282,6 +283,7 @@ impl FileType {
"aiff" | "aif" | "afc" | "aifc" => Some(Self::AIFF),
"mp3" => Some(Self::MP3),
"wav" | "wave" => Some(Self::WAV),
"wv" => Some(Self::WavPack),
"opus" => Some(Self::Opus),
"flac" => Some(Self::FLAC),
"ogg" => Some(Self::Vorbis),
@ -386,6 +388,7 @@ impl FileType {
None
},
82 if buf.len() >= 4 && &buf[..4] == b"wvpk" => Some(Self::WavPack),
_ if buf.len() >= 8 && &buf[4..8] == b"ftyp" => Some(Self::MP4),
_ => None,
}

View file

@ -182,6 +182,7 @@ mod probe;
pub(crate) mod properties;
pub(crate) mod tag;
mod traits;
pub mod wavpack;
pub use crate::error::{LoftyError, Result};

View file

@ -10,6 +10,7 @@ use crate::mp4::Mp4File;
use crate::ogg::opus::OpusFile;
use crate::ogg::speex::SpeexFile;
use crate::ogg::vorbis::VorbisFile;
use crate::wavpack::WavPackFile;
use std::fs::File;
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom};
@ -227,6 +228,7 @@ impl<R: Read + Seek> Probe<R> {
FileType::WAV => WavFile::read_from(reader, read_properties)?.into(),
FileType::MP4 => Mp4File::read_from(reader, read_properties)?.into(),
FileType::Speex => SpeexFile::read_from(reader, read_properties)?.into(),
FileType::WavPack => WavPackFile::read_from(reader, read_properties)?.into(),
}),
None => Err(LoftyError::new(ErrorKind::UnknownFormat)),
}
@ -293,7 +295,7 @@ mod tests {
let data: Vec<u8> = data.into_iter().flatten().copied().collect();
let data = std::io::Cursor::new(&data);
let probe = Probe::new(data).guess_file_type().unwrap();
assert_eq!(probe.file_type(), Some(crate::FileType::MP3));
assert_eq!(probe.file_type(), Some(FileType::MP3));
}
fn test_probe(path: &str, expected_file_type_guess: FileType) {

View file

@ -408,6 +408,7 @@ impl TagExt for Tag {
}
}
// TODO: Properly capitalize these
/// The tag's format
#[derive(Copy, Clone, Debug, PartialEq)]
#[non_exhaustive]

93
src/wavpack/mod.rs Normal file
View file

@ -0,0 +1,93 @@
//! WavPack specific items
mod properties;
mod read;
#[cfg(feature = "ape")]
use crate::ape::tag::ApeTag;
use crate::error::Result;
use crate::file::{AudioFile, FileType, TaggedFile};
#[cfg(feature = "id3v1")]
use crate::id3::v1::tag::Id3v1Tag;
use crate::properties::FileProperties;
use crate::tag::{Tag, TagType};
use std::io::{Read, Seek};
// Exports
pub use properties::WavPackProperties;
/// A WavPack file
#[derive(Default)]
pub struct WavPackFile {
#[cfg(feature = "id3v1")]
/// An ID3v1 tag
pub(crate) id3v1_tag: Option<Id3v1Tag>,
#[cfg(feature = "ape")]
/// An APEv1/v2 tag
pub(crate) ape_tag: Option<ApeTag>,
/// The file's audio properties
pub(crate) properties: WavPackProperties,
}
impl From<WavPackFile> for TaggedFile {
#[allow(clippy::vec_init_then_push, unused_mut)]
fn from(input: WavPackFile) -> Self {
let mut tags = Vec::<Option<Tag>>::with_capacity(2);
#[cfg(feature = "id3v1")]
tags.push(input.id3v1_tag.map(Into::into));
#[cfg(feature = "ape")]
tags.push(input.ape_tag.map(Into::into));
Self {
ty: FileType::WavPack,
properties: FileProperties::from(input.properties),
tags: tags.into_iter().flatten().collect(),
}
}
}
impl AudioFile for WavPackFile {
type Properties = WavPackProperties;
fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<Self>
where
R: Read + Seek,
{
read::read_from(reader, read_properties)
}
fn properties(&self) -> &Self::Properties {
&self.properties
}
#[allow(unreachable_code)]
fn contains_tag(&self) -> bool {
#[cfg(feature = "id3v1")]
return self.id3v1_tag.is_some();
#[cfg(feature = "ape")]
return self.ape_tag.is_some();
false
}
fn contains_tag_type(&self, tag_type: TagType) -> bool {
match tag_type {
#[cfg(feature = "ape")]
TagType::Ape => self.ape_tag.is_some(),
#[cfg(feature = "id3v1")]
TagType::Id3v1 => self.id3v1_tag.is_some(),
_ => false,
}
}
}
impl WavPackFile {
crate::macros::tag_methods! {
#[cfg(feature = "id3v1")]
id3v1_tag, Id3v1Tag;
#[cfg(feature = "ape")]
ape_tag, ApeTag
}
}

67
src/wavpack/properties.rs Normal file
View file

@ -0,0 +1,67 @@
use crate::properties::FileProperties;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[non_exhaustive]
/// A WavPack file's audio properties
pub struct WavPackProperties {
pub(crate) version: u16,
pub(crate) duration: Duration,
pub(crate) overall_bitrate: u32,
pub(crate) audio_bitrate: u32,
pub(crate) sample_rate: u32,
pub(crate) channels: u8,
pub(crate) bit_depth: u8,
pub(crate) lossless: bool,
}
impl From<WavPackProperties> for FileProperties {
fn from(input: WavPackProperties) -> Self {
Self {
duration: input.duration,
overall_bitrate: Some(input.overall_bitrate),
audio_bitrate: Some(input.audio_bitrate),
sample_rate: Some(input.sample_rate),
bit_depth: Some(input.bit_depth),
channels: Some(input.channels),
}
}
}
impl WavPackProperties {
/// Duration
pub fn duration(&self) -> Duration {
self.duration
}
/// Overall bitrate (kbps)
pub fn overall_bitrate(&self) -> u32 {
self.overall_bitrate
}
/// Audio bitrate (kbps)
pub fn audio_bitrate(&self) -> u32 {
self.audio_bitrate
}
/// Sample rate (Hz)
pub fn sample_rate(&self) -> u32 {
self.sample_rate
}
/// Channel count
pub fn channels(&self) -> u8 {
self.channels
}
/// WavPack version
pub fn version(&self) -> u16 {
self.version
}
/// Whether the audio is lossless
pub fn is_lossless(&self) -> bool {
self.lossless
}
}

62
src/wavpack/read.rs Normal file
View file

@ -0,0 +1,62 @@
use super::properties::WavPackProperties;
use super::WavPackFile;
use crate::ape::constants::APE_PREAMBLE;
use crate::ape::header::read_ape_header;
use crate::ape::tag::read::read_ape_tag;
use crate::error::Result;
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>
where
R: Read + Seek,
{
#[cfg(feature = "id3v1")]
let mut id3v1_tag = None;
#[cfg(feature = "ape")]
let mut ape_tag = None;
let ID3FindResults(id3v1_header, id3v1) = find_id3v1(reader, true)?;
if id3v1_header.is_some() {
#[cfg(feature = "id3v1")]
{
id3v1_tag = id3v1;
}
}
// 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)?;
// Next, search for an APE tag footer
//
// Starts with ['A', 'P', 'E', 'T', 'A', 'G', 'E', 'X']
// Exactly 32 bytes long
// Strongly recommended to be at the end of the file
reader.seek(SeekFrom::Current(-32))?;
let mut ape_preamble = [0; 8];
reader.read_exact(&mut ape_preamble)?;
if &ape_preamble == APE_PREAMBLE {
let ape_header = read_ape_header(reader, true)?;
#[cfg(feature = "ape")]
{
let ape = read_ape_tag(reader, ape_header)?;
ape_tag = Some(ape)
}
#[cfg(not(feature = "ape"))]
data.seek(SeekFrom::Current(ape_header.size as i64))?;
}
Ok(WavPackFile {
#[cfg(feature = "id3v1")]
id3v1_tag,
#[cfg(feature = "ape")]
ape_tag,
properties: WavPackProperties::default(),
})
}

Binary file not shown.

View file

@ -5,4 +5,5 @@ mod mpeg;
mod ogg;
pub(crate) mod util;
mod wav;
mod wavpack;
mod zero_sized;

55
tests/files/wavpack.rs Normal file
View file

@ -0,0 +1,55 @@
use crate::{set_artist, temp_file, verify_artist};
use lofty::{FileType, ItemKey, ItemValue, TagExt, TagItem, TagType};
use std::io::{Seek, Write};
#[test]
fn read() {
// Here we have a WacPack file with both an ID3v1 tag and an APE tag
let file = lofty::read_from_path("tests/files/assets/minimal/full_test.wv", false).unwrap();
assert_eq!(file.file_type(), FileType::WavPack);
// Verify the APE tag first
crate::verify_artist!(file, primary_tag, "Foo artist", 1);
// Now verify the ID3v1 tag
crate::verify_artist!(file, tag, TagType::Id3v1, "Bar artist", 1);
}
// TODO
#[test]
#[ignore]
fn write() {
let mut file = temp_file!("tests/files/assets/minimal/full_test.wv");
let mut tagged_file = lofty::read_from(&mut file, false).unwrap();
assert_eq!(tagged_file.file_type(), FileType::WavPack);
// APE
crate::set_artist!(tagged_file, primary_tag_mut, "Foo artist", 1 => file, "Bar artist");
// ID3v1
crate::set_artist!(tagged_file, tag_mut, TagType::Id3v1, "Bar artist", 1 => file, "Baz artist");
// Now reread the file
file.rewind().unwrap();
let mut tagged_file = lofty::read_from(&mut file, false).unwrap();
crate::set_artist!(tagged_file, primary_tag_mut, "Bar artist", 1 => file, "Foo artist");
crate::set_artist!(tagged_file, tag_mut, TagType::Id3v1, "Baz artist", 1 => file, "Bar artist");
}
#[test]
#[ignore]
fn remove_id3v1() {
crate::remove_tag!("tests/files/assets/minimal/full_test.wv", TagType::Id3v1);
}
#[test]
#[ignore]
fn remove_ape() {
crate::remove_tag!("tests/files/assets/minimal/full_test.wv", TagType::Ape);
}