using TravelEar.Core; namespace TravelEar; /// /// The Sink's input when the feed point is the encoder thread (ADR-0005, config /// Fidelity.SinkFeed = Encoder): the renderer writes each processed frame (or its gate /// silence) straight into this ring, mono at the decoder rate, stamped with the encode timestamp /// so Offset still resolves by ring position (REQ-OFFSET-MEASURE). No game object, no /// provider ring, no Tap: nothing of Local Voice ever enters Unity's audio, so the no-leak /// invariant (docs/KNOWN-HAZARDS.md 1.1) holds by construction. Single producer (the encoder /// thread), single consumer (the pump). /// internal static class SinkFeed { public const int Channels = 1; /// Mono float32 at the decoder rate, sized for 2 s. public static readonly VoiceRingBuffer Ring = new(LocalVoiceDecoder.SampleRate * 2); /// Capture timestamps keyed by ring position; the pump resolves the position it reads. public static readonly FrameStampTable Stamps = new(); public static long Blocks; public static long SilenceBlocks; public static volatile float LastPeak; private static float[] _silence = Array.Empty(); // [impl->REQ-OFFSET-MEASURE] /// Encoder thread: stores one processed frame with its encode timestamp. public static void Write(ReadOnlySpan mono, long captureTimestamp) { if (mono.Length == 0) return; var peak = 0f; for (var i = 0; i < mono.Length; i++) { var a = Math.Abs(mono[i]); if (a > peak) peak = a; } LastPeak = peak; Stamps.Mark(Ring.WritePosition, mono.Length, captureTimestamp); Ring.Write(mono); Interlocked.Increment(ref Blocks); } // [impl->REQ-VOICE-CONTINUOUS] /// Encoder thread: stores a zero frame (gate closed) with no stamp, so the stream stays continuous. public static void WriteSilence(int samples) { if (samples <= 0) return; if (_silence.Length < samples) _silence = new float[samples]; Stamps.Mark(Ring.WritePosition, samples, FrameStampTable.NoStamp); Ring.Write(_silence.AsSpan(0, samples)); Interlocked.Increment(ref SilenceBlocks); } }