using FMOD; using FMODUnity; using System.Runtime.InteropServices; using UnityEngine; public class FMODMicLoopback : MonoBehaviour { private uint LATENCY_MS = 50; private uint DRIFT_MS = 1; private uint samplesRecorded, samplesPlayed = 0; private int nativeRate, nativeChannels = 0; private uint recSoundLength = 0; uint lastPlayPos = 0; uint lastRecordPos = 0; private uint driftThreshold = 0; private uint desiredLatency = 0; private uint adjustLatency = 0; private int actualLatency = 0; uint minRecordDelta = 0xFFFFFFFF; private FMOD.CREATESOUNDEXINFO exInfo = new FMOD.CREATESOUNDEXINFO(); private FMOD.Sound recSound; private FMOD.Channel channel; // Start is called before the first frame update void Start() { /* Determine latency in samples. */ FMODUnity.RuntimeManager.CoreSystem.getRecordDriverInfo(0, out _, 0, out _, out nativeRate, out _, out nativeChannels, out _); driftThreshold = (uint)(nativeRate * DRIFT_MS) / 1000; desiredLatency = (uint)(nativeRate * LATENCY_MS) / 1000; adjustLatency = desiredLatency; actualLatency = (int)desiredLatency; /* Create user sound to record into, then start recording. */ exInfo.cbsize = Marshal.SizeOf(typeof(FMOD.CREATESOUNDEXINFO)); exInfo.numchannels = nativeChannels; exInfo.format = FMOD.SOUND_FORMAT.PCM16; exInfo.defaultfrequency = nativeRate; exInfo.length = (uint)(nativeRate * sizeof(short) * nativeChannels); FMODUnity.RuntimeManager.CoreSystem.createSound("", FMOD.MODE.LOOP_NORMAL | FMOD.MODE.OPENUSER, ref exInfo, out recSound); FMODUnity.RuntimeManager.CoreSystem.recordStart(0, recSound, true); recSound.getLength(out recSoundLength, FMOD.TIMEUNIT.PCM); } // Update is called once per frame void Update() { /* Determine how much has been recorded since we last checked */ uint recordPos = 0; FMODUnity.RuntimeManager.CoreSystem.getRecordPosition(0, out recordPos); uint recordDelta = (recordPos >= lastRecordPos) ? (recordPos - lastRecordPos) : (recordPos + recSoundLength - lastRecordPos); lastRecordPos = recordPos; samplesRecorded += recordDelta; if (recordDelta != 0 && (recordDelta < minRecordDelta)) { minRecordDelta = recordDelta; // Smallest driver granularity seen so far adjustLatency = (recordDelta <= desiredLatency) ? desiredLatency : recordDelta; // Adjust our latency if driver granularity is high } /* Delay playback until our desired latency is reached. */ if (!channel.hasHandle() && samplesRecorded >= adjustLatency) { FMODUnity.RuntimeManager.CoreSystem.getMasterChannelGroup(out FMOD.ChannelGroup mCG); FMODUnity.RuntimeManager.CoreSystem.playSound(recSound, mCG, false, out channel); } /* Determine how much has been played since we last checked. */ if (channel.hasHandle()) { uint playPos = 0; channel.getPosition(out playPos, FMOD.TIMEUNIT.PCM); uint playDelta = (playPos >= lastPlayPos) ? (playPos - lastPlayPos) : (playPos + recSoundLength - lastPlayPos); lastPlayPos = playPos; samplesPlayed += playDelta; // Compensate for any drift. int latency = (int)(samplesRecorded - samplesPlayed); actualLatency = (int)((0.97f * actualLatency) + (0.03f * latency)); int playbackRate = nativeRate; if (actualLatency < (int)(adjustLatency - driftThreshold)) { // Playback position is catching up to the record position, slow playback down by 2% playbackRate = nativeRate - (nativeRate / 50); } else if (actualLatency > (int)(adjustLatency + driftThreshold)) { // Playback is falling behind the record position, speed playback up by 2% playbackRate = nativeRate + (nativeRate / 50); } channel.setFrequency((float)playbackRate); } } private void OnDestroy() { recSound.release(); } }