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