feature: Phase 18.1 — derive Opus 320 + seek-index sidecar at ingest
Background-job transcode (ffmpeg/libopus) after source store; pure C# Ogg walker builds the 0.5s-bucketed granule→byte seek index + captures the OpusHead/OpusTags setup header into a per-track sidecar in a new track-opus vault. Best-effort, additive, regenerated on replace-audio.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace DeepDrftContent.Processors.Opus;
|
||||
|
||||
/// <summary>
|
||||
/// The single derived sidecar artifact per track (§3.4a B, recommended design): the Opus setup header
|
||||
/// (<c>OpusHead</c> + <c>OpusTags</c>) followed by the granule→byte seek index. The client fetches this
|
||||
/// once on track load and parses it into its <c>OpusSeekData</c>, so it always has both the setup bytes
|
||||
/// (to prepend to any mid-stream slice) and the accurate seek transfer function before it ever issues a
|
||||
/// Range fetch — including a window that opens away from byte 0 (UC9).
|
||||
/// </summary>
|
||||
/// <param name="SetupHeaderBytes">The verbatim OpusHead + OpusTags pages.</param>
|
||||
/// <param name="SeekIndex">The bucketed granule→byte seek index.</param>
|
||||
public sealed record OpusSidecar(byte[] SetupHeaderBytes, OggOpusSeekIndex SeekIndex)
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializes to <c>[uint32 setupHeaderLength][setup-header bytes][seek-index blob]</c>. The
|
||||
/// length prefix lets the client split the two regions with one read; the seek-index blob carries
|
||||
/// its own self-describing header (<see cref="OggOpusSeekIndex.ToBytes"/>), so it needs no trailing
|
||||
/// length.
|
||||
/// </summary>
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var indexBytes = SeekIndex.ToBytes();
|
||||
var bytes = new byte[4 + SetupHeaderBytes.Length + indexBytes.Length];
|
||||
var span = bytes.AsSpan();
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(span[..4], (uint)SetupHeaderBytes.Length);
|
||||
SetupHeaderBytes.CopyTo(span.Slice(4));
|
||||
indexBytes.CopyTo(span.Slice(4 + SetupHeaderBytes.Length));
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a blob produced by <see cref="ToBytes"/>. Returns null on any structural inconsistency
|
||||
/// (short blob, length prefix that overruns, or an unparseable index) — the format is exact, so a
|
||||
/// malformed blob is corruption.
|
||||
/// </summary>
|
||||
public static OpusSidecar? FromBytes(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
if (bytes.Length < 4)
|
||||
return null;
|
||||
|
||||
var setupLength = BinaryPrimitives.ReadUInt32LittleEndian(bytes[..4]);
|
||||
var indexStart = 4 + (long)setupLength;
|
||||
if (bytes.Length < indexStart)
|
||||
return null;
|
||||
|
||||
var setupHeader = bytes.Slice(4, (int)setupLength).ToArray();
|
||||
var index = OggOpusSeekIndex.FromBytes(bytes.Slice((int)indexStart));
|
||||
if (index is null)
|
||||
return null;
|
||||
|
||||
return new OpusSidecar(setupHeader, index);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user