Skip to content

Commit b7adbd1

Browse files
committed
MPEG: Improve duration calculation
For files with a VBR header: Thanks to @naglis for correcting the length calculation. (issue: #412) For files without a VBR header: `rev_search_for_frame_header` would get tripped up on files with trailing data that looked like a frame sync (ex. 0xFFFF). This would also result in durations that are slightly off. For now, VBR streams are still assumed to be CBR. I have not seen a file this does not work for yet. Eventually it would be nice to have more accurate calculation, but that will require we read the *entire* file.
1 parent dde24d5 commit b7adbd1

File tree

3 files changed

+84
-56
lines changed

3 files changed

+84
-56
lines changed

lofty/src/mpeg/header.rs

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use super::constants::{BITRATES, PADDING_SIZES, SAMPLES, SAMPLE_RATES, SIDE_INFORMATION_SIZES};
2-
use crate::config::ParsingMode;
32
use crate::error::Result;
43
use crate::macros::decode_err;
54

@@ -50,11 +49,7 @@ where
5049
// Unlike `search_for_frame_sync`, since this has the `Seek` bound, it will seek the reader
5150
// back to the start of the header.
5251
const REV_FRAME_SEARCH_BOUNDS: u64 = 1024;
53-
pub(super) fn rev_search_for_frame_header<R>(
54-
input: &mut R,
55-
pos: &mut u64,
56-
parse_mode: ParsingMode,
57-
) -> Result<Option<Header>>
52+
pub(super) fn rev_search_for_frame_header<R>(input: &mut R, pos: &mut u64) -> Result<Option<Header>>
5853
where
5954
R: Read + Seek,
6055
{
@@ -70,36 +65,33 @@ where
7065
for (i, byte) in buf.iter().rev().enumerate() {
7166
frame_sync[1] = frame_sync[0];
7267
frame_sync[0] = *byte;
73-
if verify_frame_sync(frame_sync) {
74-
let relative_frame_start = (search_bounds as usize) - (i + 1);
75-
if relative_frame_start + 4 > buf.len() {
76-
if parse_mode == ParsingMode::Strict {
77-
decode_err!(@BAIL Mpeg, "Expected full frame header (incomplete stream?)")
78-
}
68+
if !verify_frame_sync(frame_sync) {
69+
continue;
70+
}
7971

80-
log::warn!("MPEG: Incomplete frame header, giving up");
81-
break;
82-
}
72+
let relative_frame_start = (search_bounds as usize) - (i + 1);
73+
if relative_frame_start + 4 > buf.len() {
74+
continue;
75+
}
8376

84-
let header = Header::read(u32::from_be_bytes([
85-
frame_sync[0],
86-
frame_sync[1],
87-
buf[relative_frame_start + 2],
88-
buf[relative_frame_start + 3],
89-
]));
90-
91-
// We need to check if the header is actually valid. For
92-
// all we know, we could be in some junk (ex. 0xFF_FF_FF_FF).
93-
if header.is_none() {
94-
continue;
95-
}
77+
let header = Header::read(u32::from_be_bytes([
78+
frame_sync[0],
79+
frame_sync[1],
80+
buf[relative_frame_start + 2],
81+
buf[relative_frame_start + 3],
82+
]));
83+
84+
// We need to check if the header is actually valid. For
85+
// all we know, we could be in some junk (ex. 0xFF_FF_FF_FF).
86+
if header.is_none() {
87+
continue;
88+
}
9689

97-
// Seek to the start of the frame sync
98-
*pos += relative_frame_start as u64;
99-
input.seek(SeekFrom::Start(*pos))?;
90+
// Seek to the start of the frame sync
91+
*pos += relative_frame_start as u64;
92+
input.seek(SeekFrom::Start(*pos))?;
10093

101-
return Ok(header);
102-
}
94+
return Ok(header);
10395
}
10496

10597
Ok(None)
@@ -326,7 +318,16 @@ impl Header {
326318
}
327319
}
328320

321+
#[derive(Copy, Clone)]
322+
pub(super) enum VbrHeaderType {
323+
Xing,
324+
Info,
325+
Vbri,
326+
}
327+
328+
#[derive(Copy, Clone)]
329329
pub(super) struct VbrHeader {
330+
pub ty: VbrHeaderType,
330331
pub frames: u32,
331332
pub size: u32,
332333
}
@@ -357,7 +358,13 @@ impl VbrHeader {
357358
let frames = reader.read_u32::<BigEndian>()?;
358359
let size = reader.read_u32::<BigEndian>()?;
359360

360-
Ok(Some(Self { frames, size }))
361+
let ty = match &header {
362+
b"Xing" => VbrHeaderType::Xing,
363+
b"Info" => VbrHeaderType::Info,
364+
_ => unreachable!(),
365+
};
366+
367+
Ok(Some(Self { ty, frames, size }))
361368
},
362369
b"VBRI" => {
363370
if reader_len < 32 {
@@ -373,7 +380,11 @@ impl VbrHeader {
373380
let size = reader.read_u32::<BigEndian>()?;
374381
let frames = reader.read_u32::<BigEndian>()?;
375382

376-
Ok(Some(Self { frames, size }))
383+
Ok(Some(Self {
384+
ty: VbrHeaderType::Vbri,
385+
frames,
386+
size,
387+
}))
377388
},
378389
_ => Ok(None),
379390
}
@@ -386,7 +397,6 @@ impl VbrHeader {
386397

387398
#[cfg(test)]
388399
mod tests {
389-
use crate::config::ParsingMode;
390400
use crate::tag::utils::test_utils::read_path;
391401

392402
use std::io::{Cursor, Read, Seek, SeekFrom};
@@ -410,7 +420,7 @@ mod tests {
410420
// We have to start these at the end to do a reverse search, of course :)
411421
let mut pos = reader.seek(SeekFrom::End(0)).unwrap();
412422

413-
let ret = super::rev_search_for_frame_header(reader, &mut pos, ParsingMode::Strict);
423+
let ret = super::rev_search_for_frame_header(reader, &mut pos);
414424

415425
if expected_reader_position.is_some() {
416426
assert!(ret.is_ok());

lofty/src/mpeg/properties.rs

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use super::header::{ChannelMode, Emphasis, Header, Layer, MpegVersion, VbrHeader};
2-
use crate::config::ParsingMode;
1+
use super::header::{ChannelMode, Emphasis, Header, Layer, MpegVersion, VbrHeader, VbrHeaderType};
32
use crate::error::Result;
43
use crate::mpeg::header::rev_search_for_frame_header;
54
use crate::properties::{ChannelMask, FileProperties};
@@ -126,9 +125,8 @@ pub(super) fn read_properties<R>(
126125
reader: &mut R,
127126
first_frame: (Header, u64),
128127
mut last_frame_offset: u64,
129-
xing_header: Option<VbrHeader>,
128+
vbr_header: Option<VbrHeader>,
130129
file_length: u64,
131-
parse_mode: ParsingMode,
132130
) -> Result<()>
133131
where
134132
R: Read + Seek,
@@ -150,15 +148,20 @@ where
150148
2
151149
};
152150

153-
if let Some(xing_header) = xing_header {
154-
if first_frame_header.sample_rate > 0 && xing_header.is_valid() {
155-
let frame_time = (u32::from(first_frame_header.samples) * 1000)
156-
.div_round(first_frame_header.sample_rate);
157-
let length = u64::from(frame_time) * u64::from(xing_header.frames);
151+
if let Some(vbr_header) = vbr_header {
152+
if first_frame_header.sample_rate > 0 && vbr_header.is_valid() {
153+
log::debug!("MPEG: Valid VBR header; using it to calculate duration");
154+
155+
let sample_rate = u64::from(first_frame_header.sample_rate);
156+
let samples_per_frame = u64::from(first_frame_header.samples);
157+
158+
let total_frames = u64::from(vbr_header.frames);
159+
160+
let length = (samples_per_frame * 1000 * total_frames).div_round(sample_rate);
158161

159162
properties.duration = Duration::from_millis(length);
160163
properties.overall_bitrate = ((file_length * 8) / length) as u32;
161-
properties.audio_bitrate = ((u64::from(xing_header.size) * 8) / length) as u32;
164+
properties.audio_bitrate = ((u64::from(vbr_header.size) * 8) / length) as u32;
162165

163166
return Ok(());
164167
}
@@ -171,15 +174,22 @@ where
171174

172175
log::warn!("MPEG: Using bitrate to estimate duration");
173176

174-
properties.audio_bitrate = first_frame_header.bitrate;
177+
// http://gabriel.mp3-tech.org/mp3infotag.html:
178+
//
179+
// "In the Info Tag, the "Xing" identification string (mostly at 0x24) of the header is replaced by "Info" in case of a CBR file."
180+
let is_cbr = matches!(vbr_header.map(|h| h.ty), Some(VbrHeaderType::Info));
181+
if is_cbr {
182+
log::debug!("MPEG: CBR detected");
183+
properties.audio_bitrate = first_frame_header.bitrate;
184+
}
175185

176186
// Search for the last frame, starting at the end of the frames
177187
reader.seek(SeekFrom::Start(last_frame_offset))?;
178188

179189
let mut last_frame = None;
180190
let mut pos = last_frame_offset;
181191
while pos > 0 {
182-
match rev_search_for_frame_header(reader, &mut pos, parse_mode) {
192+
match rev_search_for_frame_header(reader, &mut pos) {
183193
// Found a frame header
184194
Ok(Some(header)) => {
185195
// Move `last_frame_offset` back to the actual position
@@ -197,14 +207,23 @@ where
197207
}
198208
}
199209

200-
if let Some(last_frame_header) = last_frame {
201-
let stream_len = last_frame_offset - first_frame_offset + u64::from(last_frame_header.len);
202-
let length = (stream_len * 8).div_round(u64::from(properties.audio_bitrate));
210+
let Some(last_frame_header) = last_frame else {
211+
log::warn!("MPEG: Could not find last frame, properties will be incomplete");
212+
return Ok(());
213+
};
214+
215+
let stream_len = (last_frame_offset + u64::from(last_frame_header.len)) - first_frame_offset;
216+
if !is_cbr {
217+
log::debug!("MPEG: VBR detected");
203218

204-
if length > 0 {
205-
properties.overall_bitrate = ((file_length * 8) / length) as u32;
206-
properties.duration = Duration::from_millis(length);
207-
}
219+
// TODO: Actually handle VBR streams, this still assumes CBR
220+
properties.audio_bitrate = first_frame_header.bitrate;
221+
}
222+
223+
let length = (stream_len * 8).div_round(u64::from(properties.audio_bitrate));
224+
if length > 0 {
225+
properties.overall_bitrate = ((file_length * 8) / length) as u32;
226+
properties.duration = Duration::from_millis(length);
208227
}
209228

210229
Ok(())

lofty/src/mpeg/read.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ where
194194
last_frame_offset,
195195
xing_header,
196196
file_length,
197-
parse_options.parsing_mode,
198197
)?;
199198
}
200199

0 commit comments

Comments
 (0)