mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-13 14:12:31 +00:00
WavPack: Start basic implementation
This commit is contained in:
parent
500358a587
commit
74779369cf
10 changed files with 291 additions and 6 deletions
13
src/file.rs
13
src/file.rs
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
93
src/wavpack/mod.rs
Normal 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
67
src/wavpack/properties.rs
Normal 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
62
src/wavpack/read.rs
Normal 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(),
|
||||
})
|
||||
}
|
BIN
tests/files/assets/minimal/full_test.wv
Normal file
BIN
tests/files/assets/minimal/full_test.wv
Normal file
Binary file not shown.
|
@ -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
55
tests/files/wavpack.rs
Normal 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);
|
||||
}
|
Loading…
Reference in a new issue