using System.Reflection; using HarmonyLib; using NAudio.Wave; using TravelEar.Core; using UnityEngine; namespace TravelEar; /// /// The round-trip provider (docs/DESIGN.md): a mod-owned instance of the game's own /// LocalVoiceProvider, which already implements the IVoiceDataProvider ring contract /// a game VoicePlayer consumes, so no IL2CPP interface has to be implemented from managed /// code (M1 open question 3). Decoded Outbound Voice is pushed through its /// IMicrophoneSubscriber.ReceiveMicrophoneData proxy exactly as the mic feed would be. /// /// Ring semantics (from the game's bodies): mono input is duplicated to the DSP channel count, /// no resampling (the mic format must match the DSP rate), and the reader starts two DSP buffer /// sets behind the write head. /// /// /// The game's LocalVoiceProvider.Start subscribes the instance to the mic; a Harmony /// prefix skips that for the mod-owned instance only, so it never receives raw mic audio. /// /// [HarmonyPatch] internal static class RoundTripProvider { private static long _ownedPointer; private static WaveFormat _format; public static LocalVoiceProvider Provider { get; private set; } public static long FramesPushed; /// /// Capture timestamps keyed by provider ring position (REQ-OFFSET-MEASURE): every push, /// voice or silence, marks the span it wrote, so the Tap can resolve the block it is handed to /// the encode timestamp of the frame it came from. Producers (encoder and main thread) are /// serialized by ; the Tap resolves on the audio thread lock-free. /// public static readonly FrameStampTable Stamps = new(); private static readonly object PushLock = new(); /// Adds the provider to , which must be inactive so Awake/Start run after setup. public static LocalVoiceProvider Create(GameObject host) { Provider = host.AddComponent(); Interlocked.Exchange(ref _ownedPointer, (long)Provider.Pointer); _format = new WaveFormat(LocalVoiceDecoder.SampleRate, LocalVoiceDecoder.Channels); return Provider; } // [impl->REQ-VOICE-ROUNDTRIP] // [impl->REQ-OFFSET-MEASURE] /// /// Pushes one decoded frame of mono samples and marks the ring span it /// occupies ( x the ring's channel count, starting at the write head /// before the push) with ( /// for silence the mod generated). Safe from any IL2CPP-attached thread. /// public static void Push(Il2CppSystem.ArraySegment pcm, int samples, int ringChannels, long captureTimestamp) { var provider = Provider; if (provider is null) return; lock (PushLock) { var start = provider.CachedVoiceWriteHead; provider.Dissonance_Audio_Capture_IMicrophoneSubscriber_ReceiveMicrophoneData(pcm, _format); if (samples > 0) Stamps.Mark(start, samples * Math.Max(1, ringChannels), captureTimestamp); } Interlocked.Increment(ref FramesPushed); } [HarmonyTargetMethod] private static MethodBase TargetMethod() => GameSymbols.LocalVoiceProviderStart; [HarmonyPrefix] private static bool SkipMicSubscription(LocalVoiceProvider __instance) { if ((long)__instance.Pointer != Interlocked.Read(ref _ownedPointer)) return true; // the game's own provider: untouched Plugin.Logger.LogInfo("Round-trip provider: mic subscription skipped for the mod-owned LocalVoiceProvider."); return false; } }