33d6f34d8a
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.
58 lines
2.4 KiB
C#
58 lines
2.4 KiB
C#
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);
|
|
}
|
|
}
|