fix: Wave 18.1 review — pre-skip subtraction, t=0 anchor, PreSkip in sidecar, stderr on cancel
This commit is contained in:
@@ -33,6 +33,7 @@ public static class OggOpusParser
|
||||
var setupHeaderEnd = -1;
|
||||
var sawOpusHead = false;
|
||||
var sawOpusTags = false;
|
||||
ushort preSkip = 0;
|
||||
|
||||
var points = new List<OpusSeekPoint>();
|
||||
ulong lastGranule = 0;
|
||||
@@ -73,6 +74,15 @@ public static class OggOpusParser
|
||||
{
|
||||
sawOpusHead = true;
|
||||
setupHeaderEnd = offset + pageTotalSize;
|
||||
|
||||
// RFC 7845 §5.1 — OpusHead layout after the 8-byte "OpusHead" magic:
|
||||
// [0] version (1 byte), [1] channel count (1 byte),
|
||||
// [2-3] pre_skip (little-endian uint16) ← at packet bytes 10-11
|
||||
// pre_skip is the number of decoder samples to discard before presenting audio;
|
||||
// all granule→time conversions must subtract it (RFC 7845 §4.3).
|
||||
if (payload.Length >= OggOpusConstants.OpusHeadMinSize)
|
||||
preSkip = BinaryPrimitives.ReadUInt16LittleEndian(
|
||||
payload.Slice(OggOpusConstants.OpusHeadPreSkipOffset, 2));
|
||||
}
|
||||
else if (sawOpusHead && !sawOpusTags && StartsWith(payload, OggOpusConstants.OpusTagsSignature))
|
||||
{
|
||||
@@ -87,19 +97,28 @@ public static class OggOpusParser
|
||||
// the byte cursor.
|
||||
if (granule != OggOpusConstants.NoGranulePosition)
|
||||
{
|
||||
var pageTime = granule / OggOpusConstants.OpusSampleRate;
|
||||
// RFC 7845 §4.3: presentation time = max(0, granule − preSkip) / 48000.
|
||||
// Use this corrected time for bucketing so that a stream with pre-skip 3840 (~80 ms)
|
||||
// does not systematically offset every indexed time by that amount.
|
||||
var correctedTime = Math.Max(0.0,
|
||||
(granule - (double)preSkip) / OggOpusConstants.OpusSampleRate);
|
||||
|
||||
if (!firstAudioPointTaken)
|
||||
{
|
||||
points.Add(new OpusSeekPoint(granule, (ulong)offset));
|
||||
// Anchor the first seek point at corrected time = 0 by storing the granule as
|
||||
// preSkip. This guarantees that a binary search for t=0 ("largest entry with
|
||||
// corrected time ≤ 0") always resolves to the first audio page's byte offset —
|
||||
// even when the real granule is slightly above preSkip due to encoder lead-in.
|
||||
points.Add(new OpusSeekPoint(preSkip, (ulong)offset));
|
||||
firstAudioPointTaken = true;
|
||||
nextBucketTime = OggOpusConstants.SeekBucketSeconds;
|
||||
}
|
||||
else if (pageTime >= nextBucketTime)
|
||||
else if (correctedTime >= nextBucketTime)
|
||||
{
|
||||
points.Add(new OpusSeekPoint(granule, (ulong)offset));
|
||||
// Advance past every bucket this page crossed so a long page does not emit a
|
||||
// backlog of entries; the next bucket is the first boundary strictly after it.
|
||||
while (nextBucketTime <= pageTime)
|
||||
while (nextBucketTime <= correctedTime)
|
||||
nextBucketTime += OggOpusConstants.SeekBucketSeconds;
|
||||
}
|
||||
|
||||
@@ -114,8 +133,11 @@ public static class OggOpusParser
|
||||
return null;
|
||||
|
||||
var setupHeader = oggBytes[..setupHeaderEnd].ToArray();
|
||||
var totalDuration = lastGranule / OggOpusConstants.OpusSampleRate;
|
||||
var index = new OggOpusSeekIndex(points, totalDuration, (ulong)oggBytes.Length);
|
||||
// RFC 7845 §4.3: total duration is also pre-skip-corrected, matching the time a listener
|
||||
// experiences (the last audio page's corrected time, clamped to ≥ 0).
|
||||
var totalDuration = Math.Max(0.0,
|
||||
(lastGranule - (double)preSkip) / OggOpusConstants.OpusSampleRate);
|
||||
var index = new OggOpusSeekIndex(points, totalDuration, (ulong)oggBytes.Length, preSkip);
|
||||
return new OggOpusWalk(setupHeader, index);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user