using System.Buffers.Binary; namespace DeepDrftContent.Processors.Opus; /// /// A single seek-index entry: an authoritative 48 kHz (Opus granule /// positions are always sample counts at 48 kHz, so time = granulepos / 48000) paired with the exact /// byte offset of the Ogg page that carries it. Every is a real page-start /// boundary, so a Range: bytes={ByteOffset}- fetch lands the decoder Ogg-sync-aligned. /// /// The page's end granule position (48 kHz sample count). /// The byte offset of the page start in the Opus file. public readonly record struct OpusSeekPoint(ulong GranulePosition, ulong ByteOffset) { /// Time in seconds this granule position represents (granulepos / 48 kHz). public double TimeSeconds => GranulePosition / OggOpusConstants.OpusSampleRate; } /// /// The accurate, precomputed transfer function from seek-time to true file byte offset for one Ogg /// Opus stream (§3.4a A). Built once at transcode time by walking the encoded stream; the client reads /// it back and binary-searches instead of doing inaccurate VBR byte-rate math. /// One entry per 0.5 s of audio (), each snapped to the /// nearest enclosing page start, plus the totals needed to clamp a seek to range. /// /// Ordered (granulepos, byteOffset) entries, ascending. The first is always the /// first audio page (offset just past the setup headers). /// Total stream duration from the final granule position. /// Total Opus file byte length, for clamping a seek past the end. public sealed record OggOpusSeekIndex( IReadOnlyList Points, double TotalDurationSeconds, ulong TotalByteLength) { /// /// Serializes the index to the compact little-endian binary blob the sidecar stores. Layout: /// [uint64 totalByteLength][double totalDurationSeconds][uint32 pointCount] then /// pointCount × (uint64 granulepos, uint64 byteOffset). Fixed-width records keep the client /// parse to a single typed-array read. /// public byte[] ToBytes() { var size = OggOpusConstants.SeekIndexHeaderSize + Points.Count * OggOpusConstants.SeekPointSize; var bytes = new byte[size]; var span = bytes.AsSpan(); BinaryPrimitives.WriteUInt64LittleEndian(span[..8], TotalByteLength); BinaryPrimitives.WriteDoubleLittleEndian(span.Slice(8, 8), TotalDurationSeconds); BinaryPrimitives.WriteUInt32LittleEndian(span.Slice(16, 4), (uint)Points.Count); var cursor = OggOpusConstants.SeekIndexHeaderSize; foreach (var point in Points) { BinaryPrimitives.WriteUInt64LittleEndian(span.Slice(cursor, 8), point.GranulePosition); BinaryPrimitives.WriteUInt64LittleEndian(span.Slice(cursor + 8, 8), point.ByteOffset); cursor += OggOpusConstants.SeekPointSize; } return bytes; } /// /// Parses a blob produced by . Returns null if the blob is too short or its /// declared point count does not fit — the storage contract is exact, so a malformed blob is a /// corruption signal, not a recoverable shape. (Provided so tests and any future server-side reader /// share one codec with the writer.) /// public static OggOpusSeekIndex? FromBytes(ReadOnlySpan bytes) { if (bytes.Length < OggOpusConstants.SeekIndexHeaderSize) return null; var totalByteLength = BinaryPrimitives.ReadUInt64LittleEndian(bytes[..8]); var totalDuration = BinaryPrimitives.ReadDoubleLittleEndian(bytes.Slice(8, 8)); var count = BinaryPrimitives.ReadUInt32LittleEndian(bytes.Slice(16, 4)); var expected = OggOpusConstants.SeekIndexHeaderSize + (long)count * OggOpusConstants.SeekPointSize; if (bytes.Length < expected) return null; var points = new OpusSeekPoint[count]; var cursor = OggOpusConstants.SeekIndexHeaderSize; for (var i = 0; i < count; i++) { var granule = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(cursor, 8)); var offset = BinaryPrimitives.ReadUInt64LittleEndian(bytes.Slice(cursor + 8, 8)); points[i] = new OpusSeekPoint(granule, offset); cursor += OggOpusConstants.SeekPointSize; } return new OggOpusSeekIndex(points, totalDuration, totalByteLength); } }