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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb87df06de7ce7961d87d2767b4c95aae8adbaec3f8b1db21bebf25a9ab4a9da
4
- data.tar.gz: 223edeea93936a8d61b59bd55fa3bd7950eb41b76d1a9d1410ea2bdf201e045b
3
+ metadata.gz: 1fcec27bd2209df4025fd9f56381076fc2cb5895eb16a21b8adaa8a3799cef0c
4
+ data.tar.gz: 55946fa15f894fd69e3e77ffdf20a9f5c648efe49c8155b19c75f89775d70f4e
5
5
  SHA512:
6
- metadata.gz: 9c9323f816013d497dab718a9232d46dd830cd03ae78c6cb3d6d5e8ff5c407ffc325119e23de75da454c0e409d0e8f466726928284302a88a4ec6559b7b55b5c
7
- data.tar.gz: 855a327090b96e12dd25620817e886da8086c0acf265a923279f3a25695970086156a104a3cdbc68a5d31c31e9087c75add7934ddd652f4103a424f510f4cf7f
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. Edit duration uses the movie
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.
@@ -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
@@ -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. Rounded media timestamps are
133
- supported, though coarse metadata may still round duration slightly.
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.1.gem`.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AudioWaveform
4
- VERSION = "0.4.1"
4
+ VERSION = "0.4.2"
5
5
  end
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.0 with AAC/MP4 playback trimming. Normative
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. A media end
47
- rounded beyond decoded audio by less than one tick is clamped to the actual
48
- end; larger shortages remain errors. Sample-rate clocks retain exact checks.
49
- Reject multiple media edits, empty edits after media, dwell/non-unit rates, and
50
- inconsistent packet timing. Fragmented MP4 retains the decoder timeline; iTunSMPB-only delay
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 empty edits using the same bounded
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 decoded_frames = 0_u64;
370
- let mut leading = self.playback.map_or(0, |playback| playback.leading_frames);
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.slice(decoded_frames, decoded.frames(), packet.pts.get())?
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 leading,
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 leading,
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(decoded_frames, frames)?;
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.minimum_decoded_frames),
84
+ (bounds.start, bounds.end, bounds.minimum_media_frames),
85
85
  (1024, 3229, 3229)
86
86
  );
87
- assert_eq!(bounds.slice(0, 1024, 0).unwrap(), 1024..1024);
88
- assert_eq!(bounds.slice(1024, 1024, 2048).unwrap(), 0..1024);
89
- assert_eq!(bounds.slice(3072, 1024, 6144).unwrap(), 0..157);
90
- assert_eq!(bounds.slice(4096, 1024, 8192).unwrap(), 0..0);
91
- assert!(bounds.slice(1024, 1024, 1024).is_err());
92
- assert!(bounds.slice(0, 1024, -1).is_err());
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, 0).unwrap(), 4..5);
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
- assert!(bounds.slice(position, 1024, timestamp).is_ok());
261
+ assert_eq!(bounds.boundary(timestamp, position).unwrap(), position);
208
262
  }
209
- assert!(bounds.slice(1024, 1024, 24).is_ok()); // rounded upward
210
- assert!(bounds.slice(1024, 1024, 22).is_err());
211
- assert!(bounds.slice(1024, 1024, 25).is_err());
212
- assert!(bounds.slice(44100, 1024, 1001).is_err()); // exactly one tick
213
- assert!(bounds.slice(3072, 1024, 69).is_ok());
214
- assert!(bounds.slice(4096, 1024, 92).is_ok());
215
- assert!(bounds.slice(5120, 1024, 115).is_err()); // repeated 23-tick drift
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
- minimum_decoded_frames: u64,
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
- pub fn slice(self, position: u64, frames: usize, pts: i64) -> Result<Range<usize>, Error> {
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
- // Packet boundaries may be rounded to media-clock ticks. Compare each
42
- // absolute timestamp with decoded time, allowing less than one tick;
43
- // do not accumulate a per-packet allowance that could hide real drift.
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(position) * u128::from(self.timescale))
46
- >= u128::from(self.rate)
74
+ .abs_diff(u128::from(expected) * u128::from(self.timescale))
75
+ < u128::from(self.rate)
47
76
  {
48
- return Err(Error::InvalidAudio("non-contiguous AAC/MP4 sample timing"));
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
- pub fn verify(self, decoded: u64, retained: u64) -> Result<(), Error> {
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(decoded)
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 decoded < self.minimum_decoded_frames || retained != expected {
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 decoded time to lie strictly after the preceding media tick.
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 minimum_decoded_frames = if media_duration == 0 {
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
- minimum_decoded_frames,
250
+ minimum_media_frames,
211
251
  leading_frames,
212
252
  }))
213
253
  }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: audiowaveform
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrii Dmytrenko