audiowaveform 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -1
- data/VALIDATION.md +27 -0
- data/bindings/ruby/CHANGELOG.md +8 -0
- data/bindings/ruby/README.md +5 -3
- data/bindings/ruby/lib/audiowaveform/version.rb +1 -1
- data/contract/SPEC.md +12 -7
- data/src/decode.rs +17 -10
- data/src/mp4/tests.rs +70 -16
- data/src/mp4.rs +57 -17
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1fcec27bd2209df4025fd9f56381076fc2cb5895eb16a21b8adaa8a3799cef0c
|
|
4
|
+
data.tar.gz: 55946fa15f894fd69e3e77ffdf20a9f5c648efe49c8155b19c75f89775d70f4e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f00da3a08370cde0a4222930f7571db9c0e23ff0fb5577574e2b622f69bcb5df54a64b1ff5a48ca4e3634722cc47ff4de6715cdf6e5241efe22f766660b27cb
|
|
7
|
+
data.tar.gz: f7d628c44628243c53f940c24f7f63f942226597411ae8884ff6f5dadd54fe4280f968a248908d5c7d526d8531bf11a2258a7300f91ff9678b0fe4e14f8442b9
|
data/README.md
CHANGED
|
@@ -113,7 +113,11 @@ sample timing table. It decodes priming packets normally, then excludes frames
|
|
|
113
113
|
outside the playback range before counting or aggregating peaks in either pass.
|
|
114
114
|
This supports leading empty edits followed by a single contiguous media edit
|
|
115
115
|
at normal speed. Empty edits contribute silence on the playback timeline;
|
|
116
|
-
genuine recorded silence is also preserved.
|
|
116
|
+
genuine recorded silence is also preserved. Short packet timing slots clip
|
|
117
|
+
surplus decoded samples from that packet's tail; gaps between packets contribute
|
|
118
|
+
silence. This handles recording timestamp jitter without rejecting valid audio.
|
|
119
|
+
Gaps are emitted only when the next packet arrives, so an overstated final
|
|
120
|
+
packet duration still fails validation. Edit duration uses the movie
|
|
117
121
|
timescale, and packet timestamps may be rounded by less than one media-clock
|
|
118
122
|
tick. The sample table and available decoded frames cap the end. Coarse
|
|
119
123
|
metadata can still round the reported playback duration.
|
data/VALIDATION.md
CHANGED
|
@@ -279,3 +279,30 @@ and peak/current capacities remain 8,192 and 456 bytes; normalized peaks use
|
|
|
279
279
|
2,216 bytes and stereo scratch uses 16,384 bytes. Maximum measured process RSS
|
|
280
280
|
was about 3.9 MiB. These timings are host-specific, not isolated-system results
|
|
281
281
|
or a constant-memory guarantee for decoder/container internals.
|
|
282
|
+
|
|
283
|
+
## AAC packet timing follow-up (Ruby 0.4.2)
|
|
284
|
+
|
|
285
|
+
AAC/MP4 now places decoded packets in their declared sample-table slots:
|
|
286
|
+
short slots clip packet tails, and gaps before a subsequent packet contribute
|
|
287
|
+
silence. Sub-tick rounding preserves continuous PCM, while an overstated final
|
|
288
|
+
packet duration still fails validation. Existing AAC fixture peaks are unchanged.
|
|
289
|
+
|
|
290
|
+
Owned synthetic fixtures cover the 712/1,000-frame overlap pattern, a 5,120-frame
|
|
291
|
+
gap, and mixed gaps/overlaps in stereo. Exact-count and fixed-resolution peaks,
|
|
292
|
+
mono/split output, both gains, and both bit depths match an independent batch
|
|
293
|
+
oracle. Larger gaps reuse buffer capacities and remain cancellable. Real timing
|
|
294
|
+
changes on a coarse media clock are accepted without hiding truncated tails.
|
|
295
|
+
|
|
296
|
+
Local validation on macOS arm64 with Rust 1.98.1 and Ruby 4.0.5 passes:
|
|
297
|
+
all-feature and no-default-feature Rust tests, formatting, strict Clippy in both
|
|
298
|
+
Cargo workspaces, and Ruby/RBS checks (73 tests, 1,749 assertions).
|
|
299
|
+
|
|
300
|
+
A privately supplied failing voice recording was transcoded with FFmpeg's AAC
|
|
301
|
+
encoder using `-map_metadata -1 -vn -c:a aac -q:a 2`. Version 0.4.1 rejects its
|
|
302
|
+
non-contiguous timestamps. The patched core returns 609,264 frames at 48 kHz
|
|
303
|
+
(12.693 seconds), 110 points, and one decode pass with 8,192 bytes of scratch.
|
|
304
|
+
All 220 peaks match an independent FFmpeg decode after removing the two
|
|
305
|
+
surplus packet tails (312 and 24 samples), at 8 and 16 bits with fixed gain and
|
|
306
|
+
normalization. Unadjusted FFmpeg PCM is 7 ms longer; its 8-bit peaks differ in
|
|
307
|
+
9 of 220 values at fixed gain (8 when normalized), by at most 4. The recording
|
|
308
|
+
and its derived media remain outside the repository.
|
data/bindings/ruby/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.2 (2026-09-23)
|
|
4
|
+
|
|
5
|
+
- Accept AAC/MP4 packet timing overlaps and gaps, including browser recordings
|
|
6
|
+
transcoded to AAC. Clip packets to shorter timing slots and render gaps as
|
|
7
|
+
silence with bounded, cancellable buffers. Preserve edit-list delay/padding
|
|
8
|
+
trimming, coarse-clock rounding, and truncated-tail validation. Continuous
|
|
9
|
+
inputs retain their existing peaks; valid exact-point counts use one pass.
|
|
10
|
+
|
|
3
11
|
## 0.4.1 (2026-09-22)
|
|
4
12
|
|
|
5
13
|
- Generate exact `points:` for nonfragmented AAC/MP4 in one decoding pass using
|
data/bindings/ruby/README.md
CHANGED
|
@@ -129,8 +129,10 @@ feed their PCM into `generate_pcm`. Decoder gapless trimming removes reported
|
|
|
129
129
|
delay/padding, including MP3 and Vorbis priming. Nonfragmented AAC/MP4 also trims
|
|
130
130
|
to the selected track's single, normal-speed media edit and sample timing range
|
|
131
131
|
in both passes. Leading empty edits contribute silence before the audio;
|
|
132
|
-
intentional recorded silence is preserved.
|
|
133
|
-
|
|
132
|
+
intentional recorded silence is preserved. Short packet timing slots trim the
|
|
133
|
+
packet's tail, and gaps between packets contribute silence. Rounded media
|
|
134
|
+
timestamps are supported, though coarse metadata may still round duration
|
|
135
|
+
slightly. An overstated final packet duration still raises an error.
|
|
134
136
|
Fragmented MP4 and iTunSMPB-only delay metadata remain unsupported for gapless
|
|
135
137
|
trimming. Multiple media edits, empty edits after media, or non-unit playback
|
|
136
138
|
rates raise `AudioWaveform::Error`; use external decoding and `generate_pcm`
|
|
@@ -154,7 +156,7 @@ ruby bindings/ruby/script/test_native_gem.rb PATH_TO_NATIVE_GEM
|
|
|
154
156
|
|
|
155
157
|
The normal rake task compiles, runs contract/codec/GC/cancellation tests, and
|
|
156
158
|
validates RBS. Source installation is checked with
|
|
157
|
-
`ruby bindings/ruby/script/test_source_gem.rb pkg/audiowaveform-0.4.
|
|
159
|
+
`ruby bindings/ruby/script/test_source_gem.rb pkg/audiowaveform-0.4.2.gem`.
|
|
158
160
|
Native cross-builds and ABI checks are configured in `.github/workflows`.
|
|
159
161
|
|
|
160
162
|
Release tags use `ruby-vX.Y.Z`. The release workflow verifies the tag against
|
data/contract/SPEC.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Audio peak generation contract, revision 3
|
|
2
2
|
|
|
3
|
-
Updated after Ruby 0.4.
|
|
3
|
+
Updated after Ruby 0.4.1 with AAC/MP4 playback timing. Normative
|
|
4
4
|
requirements below apply to the library. No BBC command-line, file-format,
|
|
5
5
|
rendering, or byte-for-byte implementation compatibility is required.
|
|
6
6
|
|
|
@@ -12,7 +12,7 @@ channel. A point is a minimum/maximum pair for each output channel. Resolution
|
|
|
12
12
|
arguments count **frames**, despite the historical `samples_per_pixel` name.
|
|
13
13
|
|
|
14
14
|
Let `N` be the number of playback frames (retained decoded frames plus silence
|
|
15
|
-
from supported leading empty edits), `F` the positive sample rate,
|
|
15
|
+
from supported leading empty edits and gaps between AAC/MP4 packets), `F` the positive sample rate,
|
|
16
16
|
and `C` the output channel count. General container duration estimates must not determine
|
|
17
17
|
`N`, point boundaries, or the returned duration. Channel count and sample rate
|
|
18
18
|
must remain constant throughout the selected track; otherwise return an error.
|
|
@@ -43,11 +43,16 @@ assumed encoder delay. Sum leading empty-edit durations in movie ticks before
|
|
|
43
43
|
converting to frames (rounding up), and prepend that many zero frames using
|
|
44
44
|
bounded buffers. Those frames are part of `N`, duration, and bucket placement.
|
|
45
45
|
Allow absolute packet timestamps to differ from decoded time by less than one
|
|
46
|
-
media-clock tick, without accumulating that allowance per packet.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
media-clock tick, without accumulating that allowance per packet. Outside that
|
|
47
|
+
rounding tolerance, clip decoded packets to shorter sample-table slots and fill
|
|
48
|
+
gaps before subsequent packets with silence, using bounded, cancellable buffers.
|
|
49
|
+
Convert absolute boundaries to frames by rounding up. Apply playback-range
|
|
50
|
+
trimming to both samples and silence. Never synthesize an unverified trailing
|
|
51
|
+
gap: a media end rounded beyond available audio by less than one tick is clamped
|
|
52
|
+
to the actual end; larger shortages remain errors. Sample-rate clocks retain
|
|
53
|
+
exact checks. Reject multiple media edits, empty edits after media, dwell/non-unit
|
|
54
|
+
rates, negative timestamps, and backwards timing beyond the rounding tolerance.
|
|
55
|
+
Fragmented MP4 retains the decoder timeline; iTunSMPB-only delay
|
|
51
56
|
metadata is not interpreted. Raw PCM receives no automatic trimming.
|
|
52
57
|
|
|
53
58
|
For exact points, use parsed nonfragmented AAC/MP4 bounds to generate provisional
|
data/src/decode.rs
CHANGED
|
@@ -202,7 +202,7 @@ fn resize_scratch(scratch: &mut Vec<f64>, size: usize) -> Result<(), Error> {
|
|
|
202
202
|
Ok(())
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
/// Advance the playback timeline through
|
|
205
|
+
/// Advance the playback timeline through silence using the same bounded
|
|
206
206
|
/// buffer as decoded audio, including during the counting pass and replay.
|
|
207
207
|
fn consume_silence<C: FnMut() -> bool>(
|
|
208
208
|
mut progress: Scan,
|
|
@@ -366,8 +366,8 @@ impl Session {
|
|
|
366
366
|
) -> Result<(MediaSourceStream<'static>, Scan), Error> {
|
|
367
367
|
let mut observed = None;
|
|
368
368
|
let mut frames = 0_u64;
|
|
369
|
-
let mut
|
|
370
|
-
let mut
|
|
369
|
+
let mut media_end = 0_u64;
|
|
370
|
+
let mut silence = self.playback.map_or(0, |playback| playback.leading_frames);
|
|
371
371
|
let mut demux_error = None;
|
|
372
372
|
loop {
|
|
373
373
|
poll(cancelled)?;
|
|
@@ -402,20 +402,27 @@ impl Session {
|
|
|
402
402
|
if signal.rate != playback.rate {
|
|
403
403
|
return Err(Error::InvalidAudio("AAC/MP4 sample rate changed"));
|
|
404
404
|
}
|
|
405
|
-
playback.
|
|
405
|
+
let part = playback.packet(
|
|
406
|
+
media_end,
|
|
407
|
+
decoded.frames(),
|
|
408
|
+
packet.pts.get(),
|
|
409
|
+
packet.dur.get(),
|
|
410
|
+
)?;
|
|
411
|
+
media_end = part.end;
|
|
412
|
+
silence = silence
|
|
413
|
+
.checked_add(part.silence)
|
|
414
|
+
.ok_or(Error::SizeOverflow)?;
|
|
415
|
+
part.samples
|
|
406
416
|
}
|
|
407
417
|
None => 0..decoded.frames(),
|
|
408
418
|
};
|
|
409
|
-
decoded_frames = decoded_frames
|
|
410
|
-
.checked_add(u64::try_from(decoded.frames()).map_err(|_| Error::SizeOverflow)?)
|
|
411
|
-
.ok_or(Error::SizeOverflow)?;
|
|
412
419
|
frames = consume_silence(
|
|
413
420
|
Scan {
|
|
414
421
|
signal,
|
|
415
422
|
frames,
|
|
416
423
|
track: self.track,
|
|
417
424
|
},
|
|
418
|
-
&mut
|
|
425
|
+
&mut silence,
|
|
419
426
|
scratch,
|
|
420
427
|
cancelled,
|
|
421
428
|
&mut consume,
|
|
@@ -488,13 +495,13 @@ impl Session {
|
|
|
488
495
|
frames,
|
|
489
496
|
track: self.track,
|
|
490
497
|
},
|
|
491
|
-
&mut
|
|
498
|
+
&mut silence,
|
|
492
499
|
scratch,
|
|
493
500
|
cancelled,
|
|
494
501
|
&mut consume,
|
|
495
502
|
)?;
|
|
496
503
|
if let Some(playback) = self.playback {
|
|
497
|
-
playback.verify(
|
|
504
|
+
playback.verify(media_end, frames)?;
|
|
498
505
|
}
|
|
499
506
|
Ok((
|
|
500
507
|
stream,
|
data/src/mp4/tests.rs
CHANGED
|
@@ -81,21 +81,75 @@ fn versions_clocks_and_selected_track() {
|
|
|
81
81
|
));
|
|
82
82
|
let bounds = parse(&movie(version, 1000, &tracks), 7, 44100).unwrap();
|
|
83
83
|
assert_eq!(
|
|
84
|
-
(bounds.start, bounds.end, bounds.
|
|
84
|
+
(bounds.start, bounds.end, bounds.minimum_media_frames),
|
|
85
85
|
(1024, 3229, 3229)
|
|
86
86
|
);
|
|
87
|
-
assert_eq!(bounds.slice(0, 1024
|
|
88
|
-
assert_eq!(bounds.slice(1024, 1024
|
|
89
|
-
assert_eq!(bounds.slice(3072, 1024
|
|
90
|
-
assert_eq!(bounds.slice(4096, 1024
|
|
91
|
-
assert!(bounds.
|
|
92
|
-
assert!(bounds.
|
|
87
|
+
assert_eq!(bounds.slice(0, 1024), 1024..1024);
|
|
88
|
+
assert_eq!(bounds.slice(1024, 1024), 0..1024);
|
|
89
|
+
assert_eq!(bounds.slice(3072, 1024), 0..157);
|
|
90
|
+
assert_eq!(bounds.slice(4096, 1024), 0..0);
|
|
91
|
+
assert!(bounds.packet(1024, 1024, 1024, 2048).is_err());
|
|
92
|
+
assert!(bounds.packet(0, 1024, -1, 2048).is_err());
|
|
93
93
|
assert!(bounds.verify(4096, 2205).is_ok());
|
|
94
94
|
assert!(bounds.verify(3072, 2205).is_err());
|
|
95
95
|
assert!(bounds.verify(4096, 2204).is_err());
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
#[test]
|
|
100
|
+
fn packet_slots_clip_overlaps_and_fill_only_observed_gaps() {
|
|
101
|
+
let timing = [(1, 1024), (1, 712), (1, 6144), (1, 1000), (1, 352)];
|
|
102
|
+
let bytes = movie(
|
|
103
|
+
0,
|
|
104
|
+
48000,
|
|
105
|
+
&track(1, 0, 48000, &timing, &edit(0, &[(8208, 1024, 65536)])),
|
|
106
|
+
);
|
|
107
|
+
let bounds = parse(&bytes, 1, 48000).unwrap();
|
|
108
|
+
let mut position = 0;
|
|
109
|
+
let mut retained = 0;
|
|
110
|
+
for (pts, duration, samples, silence, end) in [
|
|
111
|
+
(0, 1024, 1024..1024, 0, 1024),
|
|
112
|
+
(1024, 712, 0..712, 0, 1736),
|
|
113
|
+
(1736, 6144, 0..1024, 0, 2760),
|
|
114
|
+
(7880, 1000, 0..1000, 5120, 8880),
|
|
115
|
+
(8880, 352, 0..352, 0, 9232),
|
|
116
|
+
] {
|
|
117
|
+
let part = bounds.packet(position, 1024, pts, duration).unwrap();
|
|
118
|
+
assert_eq!(
|
|
119
|
+
(part.samples.clone(), part.silence, part.end),
|
|
120
|
+
(samples, silence, end)
|
|
121
|
+
);
|
|
122
|
+
position = part.end;
|
|
123
|
+
retained += part.silence + part.samples.len() as u64;
|
|
124
|
+
}
|
|
125
|
+
assert!(bounds.verify(position, retained).is_ok());
|
|
126
|
+
// The last decoded packet cannot invent a trailing gap to satisfy metadata.
|
|
127
|
+
assert!(bounds.verify(2760, 1736).is_err());
|
|
128
|
+
assert!(bounds.packet(1024, 1024, -1, 1024).is_err());
|
|
129
|
+
assert!(bounds.packet(1024, 1024, 0, 1024).is_err());
|
|
130
|
+
assert!(matches!(
|
|
131
|
+
bounds.packet(1024, 1024, 1024, u64::MAX),
|
|
132
|
+
Err(Error::SizeOverflow)
|
|
133
|
+
));
|
|
134
|
+
|
|
135
|
+
// An edit entirely inside a gap retains only that portion of silence.
|
|
136
|
+
let bytes = movie(
|
|
137
|
+
0,
|
|
138
|
+
48000,
|
|
139
|
+
&track(1, 0, 48000, &timing, &edit(0, &[(500, 3500, 65536)])),
|
|
140
|
+
);
|
|
141
|
+
let bounds = parse(&bytes, 1, 48000).unwrap();
|
|
142
|
+
let part = bounds.packet(2760, 1024, 7880, 1000).unwrap();
|
|
143
|
+
assert_eq!(part.silence, 500);
|
|
144
|
+
assert!(part.samples.is_empty());
|
|
145
|
+
|
|
146
|
+
// A zero-duration slot is decoded for AAC state but contributes no samples.
|
|
147
|
+
let part = bounds.packet(1024, 1024, 1024, 0).unwrap();
|
|
148
|
+
assert_eq!(part.end, 1024);
|
|
149
|
+
assert_eq!(part.silence, 0);
|
|
150
|
+
assert!(part.samples.is_empty());
|
|
151
|
+
}
|
|
152
|
+
|
|
99
153
|
#[test]
|
|
100
154
|
fn edit_end_is_added_before_rounding_and_can_cut_within_a_packet() {
|
|
101
155
|
let bytes = movie(
|
|
@@ -105,7 +159,7 @@ fn edit_end_is_added_before_rounding_and_can_cut_within_a_packet() {
|
|
|
105
159
|
);
|
|
106
160
|
let bounds = parse(&bytes, 1, 10).unwrap();
|
|
107
161
|
assert_eq!((bounds.start, bounds.end), (4, 5)); // ceil(10/3), ceil(10/3 + 1)
|
|
108
|
-
assert_eq!(bounds.slice(0, 10
|
|
162
|
+
assert_eq!(bounds.slice(0, 10), 4..5);
|
|
109
163
|
}
|
|
110
164
|
|
|
111
165
|
#[test]
|
|
@@ -204,15 +258,15 @@ fn rounded_media_timestamps_allow_less_than_one_tick_without_accumulating_drift(
|
|
|
204
258
|
);
|
|
205
259
|
let bounds = parse(&bytes, 1, 44100).unwrap();
|
|
206
260
|
for (position, timestamp) in [(0, 0), (1024, 23), (2048, 46), (3072, 70)] {
|
|
207
|
-
|
|
261
|
+
assert_eq!(bounds.boundary(timestamp, position).unwrap(), position);
|
|
208
262
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
263
|
+
assert_eq!(bounds.boundary(24, 1024).unwrap(), 1024); // rounded upward
|
|
264
|
+
assert_eq!(bounds.boundary(22, 1024).unwrap(), 971);
|
|
265
|
+
assert_eq!(bounds.boundary(25, 1024).unwrap(), 1103);
|
|
266
|
+
assert_eq!(bounds.boundary(1001, 44100).unwrap(), 44145); // one tick
|
|
267
|
+
assert_eq!(bounds.boundary(69, 3072).unwrap(), 3072);
|
|
268
|
+
assert_eq!(bounds.boundary(92, 4096).unwrap(), 4096);
|
|
269
|
+
assert_eq!(bounds.boundary(115, 5120).unwrap(), 5072); // real drift
|
|
216
270
|
assert_eq!(bounds.end, 3220);
|
|
217
271
|
assert!(bounds.verify(4096, 3220).is_ok());
|
|
218
272
|
}
|
data/src/mp4.rs
CHANGED
|
@@ -23,7 +23,7 @@ pub(crate) struct Playback {
|
|
|
23
23
|
timescale: u32,
|
|
24
24
|
start: u64,
|
|
25
25
|
end: u64,
|
|
26
|
-
|
|
26
|
+
minimum_media_frames: u64,
|
|
27
27
|
pub leading_frames: u64,
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -36,38 +36,78 @@ impl Playback {
|
|
|
36
36
|
self.end - self.start + self.leading_frames
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
/// Place decoded samples on the media timeline. A short sample-table slot
|
|
40
|
+
/// clips the packet's tail; a long slot leaves silence before the next
|
|
41
|
+
/// packet. Never fill an unverified trailing gap at EOF.
|
|
42
|
+
pub fn packet(
|
|
43
|
+
self,
|
|
44
|
+
position: u64,
|
|
45
|
+
decoded: usize,
|
|
46
|
+
pts: i64,
|
|
47
|
+
duration: u64,
|
|
48
|
+
) -> Result<PacketSlice, Error> {
|
|
40
49
|
let timestamp = u64::try_from(pts).map_err(|_| INVALID)?;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
let start = self.boundary(timestamp, position)?;
|
|
51
|
+
if start < position {
|
|
52
|
+
return Err(Error::InvalidAudio("backwards AAC/MP4 sample timing"));
|
|
53
|
+
}
|
|
54
|
+
let decoded_end = start
|
|
55
|
+
.checked_add(u64::try_from(decoded).map_err(|_| Error::SizeOverflow)?)
|
|
56
|
+
.ok_or(Error::SizeOverflow)?;
|
|
57
|
+
let timestamp_end = timestamp.checked_add(duration).ok_or(Error::SizeOverflow)?;
|
|
58
|
+
let end = self
|
|
59
|
+
.boundary(timestamp_end, decoded_end)?
|
|
60
|
+
.min(decoded_end)
|
|
61
|
+
.max(start);
|
|
62
|
+
Ok(PacketSlice {
|
|
63
|
+
silence: self.end.min(start).saturating_sub(self.start.max(position)),
|
|
64
|
+
samples: self.slice(start, end - start),
|
|
65
|
+
end,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn boundary(self, timestamp: u64, expected: u64) -> Result<u64, Error> {
|
|
70
|
+
// Preserve continuous PCM when absolute boundaries differ by less than
|
|
71
|
+
// one media-clock tick. Comparing absolute times avoids accumulating a
|
|
72
|
+
// per-packet rounding allowance or independently rounding durations.
|
|
44
73
|
if (u128::from(timestamp) * u128::from(self.rate))
|
|
45
|
-
.abs_diff(u128::from(
|
|
46
|
-
|
|
74
|
+
.abs_diff(u128::from(expected) * u128::from(self.timescale))
|
|
75
|
+
< u128::from(self.rate)
|
|
47
76
|
{
|
|
48
|
-
|
|
77
|
+
Ok(expected)
|
|
78
|
+
} else {
|
|
79
|
+
frames(u128::from(timestamp), self.timescale, self.rate)
|
|
49
80
|
}
|
|
50
|
-
let length = u64::try_from(frames).map_err(|_| Error::SizeOverflow)?;
|
|
51
|
-
Ok(self.start.saturating_sub(position).min(length) as usize
|
|
52
|
-
..self.end.saturating_sub(position).min(length) as usize)
|
|
53
81
|
}
|
|
54
82
|
|
|
55
|
-
|
|
83
|
+
fn slice(self, position: u64, length: u64) -> Range<usize> {
|
|
84
|
+
// Only called for decoded buffers, so length already fits usize.
|
|
85
|
+
self.start.saturating_sub(position).min(length) as usize
|
|
86
|
+
..self.end.saturating_sub(position).min(length) as usize
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub fn verify(self, media_end: u64, retained: u64) -> Result<(), Error> {
|
|
56
90
|
// A coarse final timestamp can round slightly beyond the decoded end.
|
|
57
91
|
// Clamp to the frames actually available, never synthesize AAC padding.
|
|
58
92
|
let expected = self
|
|
59
93
|
.end
|
|
60
|
-
.min(
|
|
94
|
+
.min(media_end)
|
|
61
95
|
.saturating_sub(self.start)
|
|
62
96
|
.checked_add(self.leading_frames)
|
|
63
97
|
.ok_or(Error::SizeOverflow)?;
|
|
64
|
-
if
|
|
98
|
+
if media_end < self.minimum_media_frames || retained != expected {
|
|
65
99
|
return Err(Error::InvalidAudio("truncated AAC/MP4 playback range"));
|
|
66
100
|
}
|
|
67
101
|
Ok(())
|
|
68
102
|
}
|
|
69
103
|
}
|
|
70
104
|
|
|
105
|
+
pub(crate) struct PacketSlice {
|
|
106
|
+
pub silence: u64,
|
|
107
|
+
pub samples: Range<usize>,
|
|
108
|
+
pub end: u64,
|
|
109
|
+
}
|
|
110
|
+
|
|
71
111
|
/// `file` is a duplicate of the decoder's open file description. Restore its
|
|
72
112
|
/// physical cursor before decoding resumes, including when metadata is invalid.
|
|
73
113
|
/// No pathname is reopened, so both decoding passes keep using the same file.
|
|
@@ -187,9 +227,9 @@ impl<R: Read + Seek, C: FnMut() -> bool> Parser<'_, R, C> {
|
|
|
187
227
|
if start > end {
|
|
188
228
|
return Err(INVALID);
|
|
189
229
|
}
|
|
190
|
-
// Require
|
|
230
|
+
// Require available media to extend strictly past the preceding media tick.
|
|
191
231
|
// With a sample-rate clock this still requires every declared frame.
|
|
192
|
-
let
|
|
232
|
+
let minimum_media_frames = if media_duration == 0 {
|
|
193
233
|
0
|
|
194
234
|
} else {
|
|
195
235
|
u64::try_from(
|
|
@@ -207,7 +247,7 @@ impl<R: Read + Seek, C: FnMut() -> bool> Parser<'_, R, C> {
|
|
|
207
247
|
timescale: media_scale,
|
|
208
248
|
start,
|
|
209
249
|
end,
|
|
210
|
-
|
|
250
|
+
minimum_media_frames,
|
|
211
251
|
leading_frames,
|
|
212
252
|
}))
|
|
213
253
|
}
|