lofty/wavpack/
properties.rs

1use crate::config::ParsingMode;
2use crate::error::Result;
3use crate::macros::{decode_err, err, parse_mode_choice, try_vec};
4use crate::properties::{ChannelMask, FileProperties};
5
6use std::io::{Read, Seek, SeekFrom};
7use std::time::Duration;
8
9use byteorder::{LittleEndian, ReadBytesExt};
10
11/// A WavPack file's audio properties
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13#[non_exhaustive]
14pub struct WavPackProperties {
15	pub(crate) version: u16,
16	pub(crate) duration: Duration,
17	pub(crate) overall_bitrate: u32,
18	pub(crate) audio_bitrate: u32,
19	pub(crate) sample_rate: u32,
20	pub(crate) channels: u16,
21	pub(crate) channel_mask: ChannelMask,
22	pub(crate) bit_depth: u8,
23	pub(crate) lossless: bool,
24}
25
26impl From<WavPackProperties> for FileProperties {
27	fn from(input: WavPackProperties) -> Self {
28		Self {
29			duration: input.duration,
30			overall_bitrate: Some(input.overall_bitrate),
31			audio_bitrate: Some(input.audio_bitrate),
32			sample_rate: Some(input.sample_rate),
33			bit_depth: Some(input.bit_depth),
34			channels: Some(input.channels as u8),
35			channel_mask: if input.channel_mask == ChannelMask(0) {
36				None
37			} else {
38				Some(input.channel_mask)
39			},
40		}
41	}
42}
43
44impl WavPackProperties {
45	/// Duration of the audio
46	pub fn duration(&self) -> Duration {
47		self.duration
48	}
49
50	/// Overall bitrate (kbps)
51	pub fn overall_bitrate(&self) -> u32 {
52		self.overall_bitrate
53	}
54
55	/// Audio bitrate (kbps)
56	pub fn audio_bitrate(&self) -> u32 {
57		self.audio_bitrate
58	}
59
60	/// Sample rate (Hz)
61	pub fn sample_rate(&self) -> u32 {
62		self.sample_rate
63	}
64
65	/// Channel count
66	///
67	/// This is a `u16` since WavPack supports "unlimited" streams
68	pub fn channels(&self) -> u16 {
69		self.channels
70	}
71
72	/// Channel mask
73	pub fn channel_mask(&self) -> ChannelMask {
74		self.channel_mask
75	}
76
77	/// WavPack version
78	pub fn version(&self) -> u16 {
79		self.version
80	}
81
82	/// Bits per sample
83	pub fn bit_depth(&self) -> u8 {
84		self.bit_depth
85	}
86
87	/// Whether the audio is lossless
88	pub fn is_lossless(&self) -> bool {
89		self.lossless
90	}
91}
92
93// Thanks MultimediaWiki :)
94
95// https://wiki.multimedia.cx/index.php?title=WavPack#Block_structure
96
97const BYTES_PER_SAMPLE_MASK: u32 = 3;
98const BIT_DEPTH_SHL: u32 = 13;
99const BIT_DEPTH_SHIFT_MASK: u32 = 0x1F << BIT_DEPTH_SHL;
100const FLAG_INITIAL_BLOCK: u32 = 0x800;
101const FLAG_FINAL_BLOCK: u32 = 0x1000;
102const FLAG_MONO: u32 = 0x0004;
103const FLAG_DSD: u32 = 0x8000_0000;
104const FLAG_HYBRID_COMPRESSION: u32 = 8; // Hybrid profile (lossy compression)
105
106// https://wiki.multimedia.cx/index.php?title=WavPack#Metadata
107
108const ID_FLAG_ODD_SIZE: u8 = 0x40;
109const ID_FLAG_LARGE_SIZE: u8 = 0x80;
110
111const ID_MULTICHANNEL: u8 = 0x0D;
112const ID_NON_STANDARD_SAMPLE_RATE: u8 = 0x27;
113const ID_DSD: u8 = 0xE;
114
115const MIN_STREAM_VERSION: u16 = 0x402;
116const MAX_STREAM_VERSION: u16 = 0x410;
117
118const SAMPLE_RATES: [u32; 16] = [
119	6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000,
120	192_000, 0,
121];
122
123#[rustfmt::skip]
124pub(super) fn read_properties<R>(reader: &mut R, stream_length: u64, parse_mode: ParsingMode) -> Result<WavPackProperties>
125where
126	R: Read + Seek,
127{
128	let mut properties = WavPackProperties::default();
129
130	let mut offset = 0;
131	let mut total_samples = 0;
132	loop {
133		reader.seek(SeekFrom::Start(offset))?;
134
135		let block_header;
136		match parse_wv_header(reader) {
137			Ok(header) => block_header = header,
138			Err(e) if parse_mode == ParsingMode::Strict => return Err(e),
139			_ => break,
140		}
141
142		let flags = block_header.flags;
143		let sample_rate_idx = ((flags >> 23) & 0xF) as usize;
144		properties.sample_rate = SAMPLE_RATES[sample_rate_idx];
145
146		// In the case of non-standard sample rates and DSD audio, we need to actually read the
147		// block to get the sample rate
148		if sample_rate_idx == 15 || flags & FLAG_DSD == FLAG_DSD {
149			let mut block_contents = try_vec![0; (block_header.block_size - 24) as usize];
150			if reader.read_exact(&mut block_contents).is_err() {
151				parse_mode_choice!(
152					parse_mode,
153					STRICT: decode_err!(@BAIL WavPack, "Block size mismatch"),
154					DEFAULT: break
155				);
156			}
157
158			if let Err(e) = get_extended_meta_info(parse_mode, &block_contents, &mut properties)
159			{
160				parse_mode_choice!(
161					parse_mode,
162					STRICT: return Err(e),
163					DEFAULT: break
164				);
165			}
166
167			// A sample rate index of 15 indicates a custom sample rate, which should have been found
168			// when we just parsed the metadata blocks
169			if sample_rate_idx == 15 && properties.sample_rate == 0 {
170				parse_mode_choice!(
171					parse_mode,
172					STRICT: decode_err!(@BAIL WavPack, "Expected custom sample rate"),
173					DEFAULT: break
174				)
175			}
176		}
177
178		if flags & FLAG_INITIAL_BLOCK == FLAG_INITIAL_BLOCK {
179			if block_header.version < MIN_STREAM_VERSION
180				|| block_header.version > MAX_STREAM_VERSION
181			{
182				parse_mode_choice!(
183					parse_mode,
184					STRICT: decode_err!(@BAIL WavPack, "Unsupported stream version encountered"),
185					DEFAULT: break
186				);
187			}
188
189			total_samples = block_header.total_samples;
190			properties.bit_depth = (((flags & BYTES_PER_SAMPLE_MASK) + 1) * 8).saturating_sub((flags & BIT_DEPTH_SHIFT_MASK) >> BIT_DEPTH_SHL) as u8;
191
192			properties.version = block_header.version;
193			properties.lossless = flags & FLAG_HYBRID_COMPRESSION == 0;
194
195
196			// https://web.archive.org/web/20150424062034/https://www.wavpack.com/file_format.txt:
197			//
198			// A flag in the header indicates whether the block is the first or the last in the
199			// sequence (for simple mono or stereo files both of these would always be set).
200			//
201			// We already checked if `FLAG_INITIAL_BLOCK` is set
202			if flags & FLAG_FINAL_BLOCK > 0 {
203				let is_mono = flags & FLAG_MONO > 0;
204				properties.channels = if is_mono { 1 } else { 2 };
205				properties.channel_mask = if is_mono { ChannelMask::mono() } else { ChannelMask::stereo() };
206			}
207		}
208
209		// Just skip any block with no samples
210		if block_header.samples == 0 {
211			offset += u64::from(block_header.block_size + 8);
212			continue;
213		}
214
215		if flags & FLAG_FINAL_BLOCK == FLAG_FINAL_BLOCK {
216			break;
217		}
218
219		offset += u64::from(block_header.block_size + 8);
220	}
221
222	// TODO: Support unknown sample counts in WavPack
223	if total_samples == !0 {
224		log::warn!("Unable to calculate duration, unknown sample counts are not yet supported");
225		return Ok(properties);
226	}
227
228	if total_samples == 0 || properties.sample_rate == 0 {
229		if parse_mode == ParsingMode::Strict {
230			decode_err!(@BAIL WavPack, "Unable to calculate duration (sample count == 0 || sample rate == 0)")
231		}
232
233		// We aren't able to determine the duration/bitrate, just early return
234		return Ok(properties);
235	}
236
237	let length = f64::from(total_samples) * 1000. / f64::from(properties.sample_rate);
238	properties.duration = Duration::from_millis((length + 0.5) as u64);
239	properties.audio_bitrate = (stream_length as f64 * 8. / length + 0.5) as u32;
240
241	let file_length = reader.seek(SeekFrom::End(0))?;
242	properties.overall_bitrate = (file_length as f64 * 8. / length + 0.5) as u32;
243
244	Ok(properties)
245}
246
247// According to the spec, the max block size is 1MB
248const WV_BLOCK_MAX_SIZE: u32 = 1_048_576;
249
250#[derive(Debug)]
251struct WVHeader {
252	version: u16,
253	block_size: u32,
254	total_samples: u32,
255	samples: u32,
256	flags: u32,
257}
258
259// NOTE: Any error here is ignored unless using `ParsingMode::Strict`
260fn parse_wv_header<R>(reader: &mut R) -> Result<WVHeader>
261where
262	R: Read + Seek,
263{
264	let mut wv_ident = [0; 4];
265	reader.read_exact(&mut wv_ident)?;
266
267	if &wv_ident != b"wvpk" {
268		err!(UnknownFormat);
269	}
270
271	let block_size = reader.read_u32::<LittleEndian>()?;
272	if !(24..=WV_BLOCK_MAX_SIZE).contains(&block_size) {
273		decode_err!(@BAIL WavPack, "WavPack block has an invalid size");
274	}
275
276	let version = reader.read_u16::<LittleEndian>()?;
277
278	// Skip 2 bytes
279	//
280	// Track number (1)
281	// Track sub index (1)
282	reader.seek(SeekFrom::Current(2))?;
283
284	let total_samples = reader.read_u32::<LittleEndian>()?;
285	let _block_idx = reader.seek(SeekFrom::Current(4))?;
286	let samples = reader.read_u32::<LittleEndian>()?;
287	let flags = reader.read_u32::<LittleEndian>()?;
288
289	let _crc = reader.seek(SeekFrom::Current(4))?;
290
291	Ok(WVHeader {
292		version,
293		block_size,
294		total_samples,
295		samples,
296		flags,
297	})
298}
299
300fn get_extended_meta_info(
301	parse_mode: ParsingMode,
302	block_content: &[u8],
303	properties: &mut WavPackProperties,
304) -> Result<()> {
305	let reader = &mut &block_content[..];
306	loop {
307		if reader.len() < 2 {
308			break;
309		}
310
311		let id = reader.read_u8()?;
312		let mut size = u32::from(reader.read_u8()?) << 1;
313
314		let is_large = id & ID_FLAG_LARGE_SIZE > 0;
315		if is_large {
316			size += u32::from(reader.read_u8()?) << 9;
317			size += u32::from(reader.read_u8()?) << 17;
318		}
319
320		if size == 0 {
321			// Empty blocks may not *always* be valid, but we only care about the validity
322			// of a few blocks.
323			continue;
324		}
325
326		if (size as usize) > reader.len() {
327			err!(SizeMismatch);
328		}
329
330		if id & ID_FLAG_ODD_SIZE > 0 {
331			size -= 1;
332		}
333
334		match id & 0x3F {
335			ID_NON_STANDARD_SAMPLE_RATE => {
336				if size < 3 {
337					decode_err!(@BAIL WavPack, "Encountered an invalid block size for non-standard sample rate");
338				}
339
340				properties.sample_rate = reader.read_u24::<LittleEndian>()?;
341				size -= 3;
342			},
343			ID_DSD => {
344				if size <= 1 {
345					decode_err!(@BAIL WavPack, "Encountered an invalid DSD block size");
346				}
347
348				let mut rate_multiplier = u32::from(reader.read_u8()?);
349				size -= 1;
350
351				if rate_multiplier > 30 {
352					parse_mode_choice!(
353						parse_mode,
354						STRICT: decode_err!(@BAIL WavPack, "Encountered an invalid sample rate multiplier"),
355						DEFAULT: break
356					)
357				}
358
359				rate_multiplier = 1 << rate_multiplier;
360				properties.sample_rate = properties.sample_rate.wrapping_mul(rate_multiplier);
361			},
362			ID_MULTICHANNEL => {
363				if size <= 1 {
364					decode_err!(@BAIL WavPack, "Unable to extract channel information");
365				}
366
367				properties.channels = u16::from(reader.read_u8()?);
368
369				// size - (id length + channel length)
370				let s = size - 2;
371				match s {
372					0 => {
373						let channel_mask = reader.read_u8()?;
374						size -= 1;
375						properties.channel_mask = ChannelMask(u32::from(channel_mask));
376					},
377					1 => {
378						let channel_mask = reader.read_u16::<LittleEndian>()?;
379						size -= 2;
380						properties.channel_mask = ChannelMask(u32::from(channel_mask));
381					},
382					2 => {
383						let channel_mask = reader.read_u24::<LittleEndian>()?;
384						size -= 3;
385						properties.channel_mask = ChannelMask(channel_mask);
386					},
387					3 => {
388						let channel_mask = reader.read_u32::<LittleEndian>()?;
389						size -= 4;
390						properties.channel_mask = ChannelMask(channel_mask);
391					},
392					4 => {
393						properties.channels |= u16::from(reader.read_u8()? & 0xF) << 8;
394						properties.channels += 1;
395
396						let channel_mask = reader.read_u24::<LittleEndian>()?;
397						size -= 4;
398
399						properties.channel_mask = ChannelMask(channel_mask);
400					},
401					5 => {
402						properties.channels |= u16::from(reader.read_u8()? & 0xF) << 8;
403						properties.channels += 1;
404
405						let channel_mask = reader.read_u32::<LittleEndian>()?;
406						size -= 5;
407
408						properties.channel_mask = ChannelMask(channel_mask);
409					},
410					_ => decode_err!(@BAIL WavPack, "Encountered invalid channel info size"),
411				}
412			},
413			_ => {},
414		}
415
416		// Skip over any remaining block size
417		if (size as usize) > reader.len() {
418			err!(SizeMismatch);
419		}
420
421		let (_, rem) = reader.split_at(size as usize);
422		*reader = rem;
423
424		if id & ID_FLAG_ODD_SIZE > 0 {
425			let _ = reader.read_u8()?;
426		}
427	}
428
429	Ok(())
430}