From ed4f64179c1b28f24c43a7484877ae105c32b4ea Mon Sep 17 00:00:00 2001 From: Silent <16026653+s-ilent@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:31:13 +1030 Subject: [PATCH 1/3] Add hold timer and noise floor to AGC When sound is silence/bg noise, it shouldn't update the AGC. When sound is loud, AGC will hold its current level for a short period before trying to update --- .../Local/BasisLocalMicrophoneDriver.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs index 9eb84c113..c3639347c 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs @@ -64,6 +64,8 @@ public static class BasisLocalMicrophoneDriver private static int warmupSamples = 0; private static bool inWarmup = false; private static float agcGainDb = 0f; + + private static float _agcHoldTimer = 0f; private static float[] _denoiseDry; private static float[] _tmp480; @@ -603,11 +605,27 @@ private static void UpdateAgc(float frameRms, float targetRms, float maxGainDb, { if (frameRms <= 1e-6f) frameRms = 1e-6f; + if (_agcHoldTimer > 0f) _agcHoldTimer -= 0.02f; + + // Skip updating gain if the sound is too quiet + if (frameRms < 0.003f) return; + float neededDb = 20f * Mathf.Log10(Mathf.Max(1e-6f, targetRms) / frameRms); neededDb = Mathf.Clamp(neededDb, -maxGainDb, maxGainDb); - float k = (neededDb > agcGainDb) ? Mathf.Clamp01(attack) : Mathf.Clamp01(release); - agcGainDb = Mathf.Lerp(agcGainDb, neededDb, k); + // The timer provides a cooldown period when the audio hits a new peak volume before applying additional correction. + if (neededDb < agcGainDb) + { + agcGainDb = Mathf.Lerp(agcGainDb, neededDb, Mathf.Clamp01(attack)); + _agcHoldTimer = 0.4f; + } + else + { + if (_agcHoldTimer <= 0f) + { + agcGainDb = Mathf.Lerp(agcGainDb, neededDb, Mathf.Clamp01(release)); + } + } } private static void CreateOrResizeArray(int length, ref float[] arr) From b61e8db89e9371e1021ef45fe2e53bd902289e48 Mon Sep 17 00:00:00 2001 From: Silent <16026653+s-ilent@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:37:44 +1030 Subject: [PATCH 2/3] AGC, process audio in smaller chunks With a buffer the size of the sample rate, AGC will take 1 second to take effect. By performing AGC on a smaller chunk, the changes can take effect much faster. This change also changes denoising to use the smaller buffer. RollingRMS was also corrected to not average the values directly (which is incorrect as RMS is a non-linear scale) and instead average the power/mean square before taking the sqrt. --- .../Local/BasisLocalMicrophoneDriver.cs | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs index c3639347c..69fa8db69 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs @@ -65,7 +65,11 @@ public static class BasisLocalMicrophoneDriver private static bool inWarmup = false; private static float agcGainDb = 0f; + public const int ProcessFrameSize = 960; // 20ms at 48kHz + public const int DenoiserFrameSize = 480; // 10ms at 48kHz + private static float _agcHoldTimer = 0f; + private static float[] _denoiseDry; private static float[] _tmp480; @@ -448,17 +452,17 @@ public static void ProcessAudioData(int posSnapshot) } int dataLength = GetDataLength(bufferLength, head, posSnapshot); - while (dataLength >= SampleRate) + while (dataLength >= ProcessFrameSize) { int remain = bufferLength - head; - if (remain < SampleRate) + if (remain < ProcessFrameSize) { Array.Copy(microphoneBufferArray, head, processBufferArray, 0, remain); - Array.Copy(microphoneBufferArray, 0, processBufferArray, remain, SampleRate - remain); + Array.Copy(microphoneBufferArray, 0, processBufferArray, remain, ProcessFrameSize - remain); } else { - Array.Copy(microphoneBufferArray, head, processBufferArray, 0, SampleRate); + Array.Copy(microphoneBufferArray, head, processBufferArray, 0, ProcessFrameSize); } // --- Optional AGC --- @@ -470,7 +474,7 @@ public static void ProcessAudioData(int posSnapshot) float agcAmp = DbToAmp(agcGainDb); if (!Mathf.Approximately(agcAmp, 1f)) { - for (int i = 0; i < SampleRate; i++) + for (int i = 0; i < ProcessFrameSize; i++) processBufferArray[i] *= agcAmp; } } @@ -498,8 +502,8 @@ public static void ProcessAudioData(int posSnapshot) Interlocked.Exchange(ref _scheduleMainHasAudio, 0); } - head = (head + SampleRate) % bufferLength; - dataLength -= SampleRate; + head = (head + ProcessFrameSize) % bufferLength; + dataLength -= ProcessFrameSize; } } @@ -518,12 +522,13 @@ public static void AdjustVolume(SMDMicrophone.MicSettings s) public static float GetRMS() { double sum = 0.0; - for (int i = 0; i < SampleRate; i++) + int len = processBufferArray.Length; + for (int i = 0; i < len; i++) { float v = processBufferArray[i]; sum += v * v; } - return Mathf.Sqrt((float)(sum / SampleRate)); + return Mathf.Sqrt((float)(sum / len)); } public static int GetDataLength(int len, int h, int pos) @@ -550,27 +555,24 @@ public static void ApplyDeNoise(SMDMicrophone.MicSettings s) if (_denoiseDry == null || _denoiseDry.Length != processBufferArray.Length) CreateOrResizeArray(processBufferArray.Length, ref _denoiseDry); - Array.Copy(processBufferArray, _denoiseDry, SampleRate); + Array.Copy(processBufferArray, _denoiseDry, ProcessFrameSize); - const int hop = 480; - if (SampleRate == hop) - { - Denoiser?.Denoise(processBufferArray); - } - else + int offset = 0; + + while (offset < ProcessFrameSize) { - if (_tmp480 == null || _tmp480.Length != hop) _tmp480 = new float[hop]; + // Copy from process buffer to denoiser buffer + // Todo: This is a little fragile since it relies on DenoiserFrameSize being 480 + if (_tmp480 == null || _tmp480.Length != DenoiserFrameSize) + _tmp480 = new float[DenoiserFrameSize]; - int o = 0; - while (o < SampleRate) - { - int n = Math.Min(hop, SampleRate - o); - Array.Clear(_tmp480, 0, hop); - Array.Copy(processBufferArray, o, _tmp480, 0, n); - Denoiser?.Denoise(_tmp480); - Array.Copy(_tmp480, 0, processBufferArray, o, n); - o += n; - } + Array.Copy(processBufferArray, offset, _tmp480, 0, DenoiserFrameSize); + + Denoiser?.Denoise(_tmp480); + + Array.Copy(_tmp480, 0, processBufferArray, offset, DenoiserFrameSize); + + offset += DenoiserFrameSize; } float makeup = DbToAmp(s.DenoiseMakeupDb); @@ -578,7 +580,7 @@ public static void ApplyDeNoise(SMDMicrophone.MicSettings s) if (!Mathf.Approximately(wet, 1f) || !Mathf.Approximately(s.DenoiseMakeupDb, 0f)) { - for (int i = 0; i < SampleRate; i++) + for (int i = 0; i < ProcessFrameSize; i++) { float den = processBufferArray[i] * makeup; processBufferArray[i] = Mathf.Lerp(_denoiseDry[i], den, wet); @@ -588,10 +590,21 @@ public static void ApplyDeNoise(SMDMicrophone.MicSettings s) public static void RollingRMS() { - float rms = GetRMS(); - rmsValues[rmsIndex] = rms; + double sumSq = 0.0; + int len = processBufferArray.Length; + for (int i = 0; i < len; i++) + { + float v = processBufferArray[i]; + sumSq += v * v; + } + float currentMeanSq = (float)(sumSq / len); + + rmsValues[rmsIndex] = currentMeanSq; rmsIndex = (rmsIndex + 1) % LocalOpusSettings.rmsWindowSize; - averageRms = rmsValues.Average(); + + float averagePower = rmsValues.Average(); + + averageRms = Mathf.Sqrt(averagePower); } public static bool IsTransmitWorthy() From fd0fe448b2228532101d97d4abdf4f84437164f3 Mon Sep 17 00:00:00 2001 From: Silent <16026653+s-ilent@users.noreply.github.com> Date: Thu, 19 Feb 2026 19:25:02 +1030 Subject: [PATCH 3/3] agc: Add names for constants --- .../Drivers/Local/BasisLocalMicrophoneDriver.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs index 69fa8db69..db1823116 100644 --- a/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs +++ b/Basis/Packages/com.basis.framework/Drivers/Local/BasisLocalMicrophoneDriver.cs @@ -616,11 +616,14 @@ public static bool IsTransmitWorthy() private static void UpdateAgc(float frameRms, float targetRms, float maxGainDb, float attack, float release) { + const float agcDecaySpeed = 0.020f; // ProcessFrameSize / 48000; + const float agcHoldTime = 0.400f; + if (frameRms <= 1e-6f) frameRms = 1e-6f; - if (_agcHoldTimer > 0f) _agcHoldTimer -= 0.02f; + if (_agcHoldTimer > 0f) _agcHoldTimer -= agcDecaySpeed; - // Skip updating gain if the sound is too quiet + // Skip updating gain if the sound is too quiet (below -50dB) if (frameRms < 0.003f) return; float neededDb = 20f * Mathf.Log10(Mathf.Max(1e-6f, targetRms) / frameRms); @@ -630,7 +633,7 @@ private static void UpdateAgc(float frameRms, float targetRms, float maxGainDb, if (neededDb < agcGainDb) { agcGainDb = Mathf.Lerp(agcGainDb, neededDb, Mathf.Clamp01(attack)); - _agcHoldTimer = 0.4f; + _agcHoldTimer = agcHoldTime; } else {