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