using Dissonance.Audio.Codecs; using Dissonance.Audio.Codecs.Opus; using Il2CppInterop.Runtime; using Il2CppInterop.Runtime.InteropTypes.Arrays; using NAudio.Wave; namespace TravelEar; /// /// Decodes Outbound Voice frames with the game's own OpusDecoder (48 kHz mono, FEC on: /// the session codec Dissonance logs at start) into an IL2CPP float array that is handed straight /// to the round-trip provider, so the PCM never crosses into managed memory. One instance, /// used from the encoder thread only. /// internal sealed class LocalVoiceDecoder { public const int SampleRate = 48_000; public const int Channels = 1; /// Largest Opus frame (120 ms at 48 kHz); the game uses 2880 (60 ms). private const int MaxFrameSamples = 5760; private readonly OpusDecoder _decoder; private readonly Il2CppStructArray _pcm; private readonly Il2CppSystem.ArraySegment _output; // Layout of Il2CppSystem.Nullable> in the IL2CPP heap, read from the runtime. private readonly int _nullableValueOffset; private readonly int _nullableHasValueOffset; private readonly int _segmentSize; public LocalVoiceDecoder() { _decoder = new OpusDecoder(new WaveFormat(SampleRate, Channels), true); _pcm = new Il2CppStructArray(MaxFrameSamples); _output = new Il2CppSystem.ArraySegment(_pcm); var nullableClass = Il2CppClassPointerStore>>.NativeClassPtr; _nullableValueOffset = (int)IL2CPP.il2cpp_field_get_offset(IL2CPP.GetIl2CppField(nullableClass, "value")); _nullableHasValueOffset = (int)IL2CPP.il2cpp_field_get_offset(IL2CPP.GetIl2CppField(nullableClass, "hasValue")); uint align = 0; _segmentSize = IL2CPP.il2cpp_class_value_size(Il2CppClassPointerStore>.NativeClassPtr, ref align); if (_nullableValueOffset <= 0 || _nullableHasValueOffset <= 0 || _segmentSize <= 0) throw new InvalidOperationException($"Nullable> layout not resolved (value @{_nullableValueOffset}, hasValue @{_nullableHasValueOffset}, segment {_segmentSize} bytes)."); } // [impl->REQ-VOICE-ROUNDTRIP] /// /// Decodes one frame. Returns the sample count and a segment over the decoder's own IL2CPP /// buffer, valid until the next call. /// public int Decode(byte[] opus, out Il2CppSystem.ArraySegment pcm) { Il2CppStructArray bytes = opus; var encoded = new Il2CppSystem.ArraySegment(bytes); var input = new EncodedBuffer(WrapNullable(encoded), false); var count = _decoder.Decode(input, _output); pcm = count == MaxFrameSamples ? _output : new Il2CppSystem.ArraySegment(_pcm, 0, Math.Max(0, count)); return count; } /// Copies the first decoded samples into managed memory. public void CopyTo(float[] destination, int count) { for (var i = 0; i < count; i++) destination[i] = _pcm[i]; } /// Writes processed samples back over the decoded ones. public void CopyFrom(float[] source, int count) { for (var i = 0; i < count; i++) _pcm[i] = source[i]; } /// /// Builds Nullable<ArraySegment<byte>> by copying the segment's payload into a /// zeroed boxed Nullable. The interop-generated Nullable(T value) constructor cannot be /// used: for a struct proxy T (a managed class) it passes the boxed object pointer where /// IL2CPP expects the struct payload, so the decoder would read a garbage array (first in-game /// run of M1 T3 crashed on exactly that). /// private unsafe Il2CppSystem.Nullable> WrapNullable(Il2CppSystem.ArraySegment segment) { var nullable = new Il2CppSystem.Nullable>(); var box = (byte*)nullable.Pointer; // field offsets are boxed-relative var payload = (byte*)IL2CPP.il2cpp_object_unbox(segment.Pointer); // the segment's { array, offset, count } Buffer.MemoryCopy(payload, box + _nullableValueOffset, _segmentSize, _segmentSize); box[_nullableHasValueOffset] = 1; return nullable; } }