Skip to main content

lofty/ogg/speex/
properties.rs

1use crate::error::Result;
2use crate::macros::decode_err;
3use crate::ogg::find_last_page;
4use crate::properties::FileProperties;
5use crate::util::math::RoundedDivision;
6
7use std::io::{Read, Seek, SeekFrom};
8use std::time::Duration;
9
10use byteorder::{LittleEndian, ReadBytesExt};
11use ogg_pager::{Packets, PageHeader};
12
13/// A Speex file's audio properties
14#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
15#[non_exhaustive]
16pub struct SpeexProperties {
17	pub(crate) duration: Duration,
18	pub(crate) version: u32,
19	pub(crate) sample_rate: u32,
20	pub(crate) mode: u32,
21	pub(crate) channels: u8,
22	pub(crate) vbr: bool,
23	pub(crate) overall_bitrate: u32,
24	pub(crate) audio_bitrate: u32,
25	pub(crate) nominal_bitrate: i32,
26}
27
28impl From<SpeexProperties> for FileProperties {
29	fn from(input: SpeexProperties) -> Self {
30		Self {
31			duration: input.duration,
32			overall_bitrate: Some(input.overall_bitrate),
33			audio_bitrate: Some(input.audio_bitrate),
34			sample_rate: Some(input.sample_rate),
35			bit_depth: None,
36			channels: Some(input.channels),
37			channel_mask: None,
38		}
39	}
40}
41
42impl SpeexProperties {
43	/// Duration of the audio
44	pub fn duration(&self) -> Duration {
45		self.duration
46	}
47
48	/// Speex version
49	pub fn version(&self) -> u32 {
50		self.version
51	}
52
53	/// Sample rate
54	pub fn sample_rate(&self) -> u32 {
55		self.sample_rate
56	}
57
58	/// Speex encoding mode
59	pub fn mode(&self) -> u32 {
60		self.mode
61	}
62
63	/// Channel count
64	pub fn channels(&self) -> u8 {
65		self.channels
66	}
67
68	/// Whether the file makes use of variable bitrate
69	pub fn vbr(&self) -> bool {
70		self.vbr
71	}
72
73	/// Overall bitrate (kbps)
74	pub fn overall_bitrate(&self) -> u32 {
75		self.overall_bitrate
76	}
77
78	/// Audio bitrate (kbps)
79	pub fn audio_bitrate(&self) -> u32 {
80		self.audio_bitrate
81	}
82
83	/// Audio bitrate (bps)
84	pub fn nominal_bitrate(&self) -> i32 {
85		self.nominal_bitrate
86	}
87}
88
89pub(in crate::ogg) fn read_properties<R>(
90	data: &mut R,
91	first_page_header: &PageHeader,
92	packets: &Packets,
93) -> Result<SpeexProperties>
94where
95	R: Read + Seek,
96{
97	log::debug!("Reading Speex properties");
98
99	// Safe to unwrap, it is impossible to get to this point without an
100	// identification header.
101	let identification_packet = packets.get(0).unwrap();
102
103	if identification_packet.len() < 80 {
104		decode_err!(@BAIL Speex, "Header packet too small");
105	}
106
107	let mut properties = SpeexProperties::default();
108
109	// The content we need comes 28 bytes into the packet
110	//
111	// Skipping:
112	// Speex string ("Speex   ", 8)
113	// Speex version (20)
114	let identification_packet_reader = &mut &identification_packet[28..];
115
116	properties.version = identification_packet_reader.read_u32::<LittleEndian>()?;
117	if properties.version > 1 {
118		decode_err!(@BAIL Speex, "Unknown Speex stream version");
119	}
120
121	// Total size of the speex header
122	let _header_size = identification_packet_reader.read_u32::<LittleEndian>()?;
123
124	properties.sample_rate = identification_packet_reader.read_u32::<LittleEndian>()?;
125	properties.mode = identification_packet_reader.read_u32::<LittleEndian>()?;
126
127	// Version ID of the bitstream
128	let _mode_bitstream_version = identification_packet_reader.read_u32::<LittleEndian>()?;
129
130	let channels = identification_packet_reader.read_u32::<LittleEndian>()?;
131
132	if channels != 1 && channels != 2 {
133		decode_err!(@BAIL Speex, "Found invalid channel count, must be mono or stereo");
134	}
135
136	properties.channels = channels as u8;
137	properties.nominal_bitrate = identification_packet_reader.read_i32::<LittleEndian>()?;
138
139	// The size of the frames in samples
140	let _frame_size = identification_packet_reader.read_u32::<LittleEndian>()?;
141
142	properties.vbr = identification_packet_reader.read_u32::<LittleEndian>()? == 1;
143
144	let last_page = find_last_page(data);
145	let file_length = data.seek(SeekFrom::End(0))?;
146
147	// The stream length is the entire file minus the two mandatory metadata packets
148	let metadata_packets_length = packets.iter().take(2).map(<[u8]>::len).sum::<usize>();
149	let stream_length = file_length.saturating_sub(metadata_packets_length as u64);
150
151	// This is used for bitrate calculation, it should be the length in
152	// milliseconds, but if we can't determine it then we'll just use 1000.
153	let mut length = 1000;
154	if let Ok(last_page) = last_page {
155		let first_page_abgp = first_page_header.abgp;
156		let last_page_abgp = last_page.header().abgp;
157
158		if properties.sample_rate > 0 {
159			let total_samples = last_page_abgp.saturating_sub(first_page_abgp);
160
161			// Best case scenario
162			if total_samples > 0 {
163				length = (total_samples * 1000).div_round(u64::from(properties.sample_rate));
164				properties.duration = Duration::from_millis(length);
165			} else {
166				log::warn!(
167					"Speex: The file contains invalid PCM values, unable to calculate length"
168				);
169			}
170		} else {
171			log::warn!("Speex: Sample rate = 0, unable to calculate length");
172		}
173	}
174
175	if properties.nominal_bitrate > 0 {
176		properties.overall_bitrate = (file_length.saturating_mul(8) / length) as u32;
177		properties.audio_bitrate = (properties.nominal_bitrate as u64 / 1000) as u32;
178	} else {
179		log::warn!("Nominal bitrate = 0, estimating bitrate from file length");
180
181		properties.overall_bitrate = file_length.saturating_mul(8).div_round(length) as u32;
182		properties.audio_bitrate = stream_length.saturating_mul(8).div_round(length) as u32;
183	}
184
185	Ok(properties)
186}