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);
}
}