diff --git a/Assets/_PROJECT/Fonts/Quantico-Regular SDF.asset b/Assets/_PROJECT/Fonts/Quantico-Regular SDF.asset index e874cd91..bf742133 100644 --- a/Assets/_PROJECT/Fonts/Quantico-Regular SDF.asset +++ b/Assets/_PROJECT/Fonts/Quantico-Regular SDF.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8839e7329815fdb3540dfb8e24181684ab80075f6a1ff7057bb26fa14917591 -size 2125853 +oid sha256:7aa1f89228063dfbc194dd878e6dd980cfdf901b78b899679b18d4199c5d6a06 +size 2135993 diff --git a/Assets/_PROJECT/Prefabs/ModelGeneration/Radio.prefab b/Assets/_PROJECT/Prefabs/ModelGeneration/Radio.prefab index 505756af..a96473b8 100644 --- a/Assets/_PROJECT/Prefabs/ModelGeneration/Radio.prefab +++ b/Assets/_PROJECT/Prefabs/ModelGeneration/Radio.prefab @@ -1074,7 +1074,7 @@ ParticleSystem: rateOverTime: serializedVersion: 2 minMaxState: 0 - scalar: 50 + scalar: 3 minScalar: 10 maxCurve: serializedVersion: 2 @@ -5310,6 +5310,8 @@ MonoBehaviour: radioButton: {fileID: 1588960997487099811} computerScreen: {fileID: 0} particles: {fileID: 4781044067799935830} + minimumEmissionRate: 3 + maximumEmissionRate: 50 --- !u!114 &198665210576292642 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/_PROJECT/Scripts/ModeGeneration/FMODWhisperBridge.cs b/Assets/_PROJECT/Scripts/ModeGeneration/FMODWhisperBridge.cs index 98e44952..19b58f4f 100644 --- a/Assets/_PROJECT/Scripts/ModeGeneration/FMODWhisperBridge.cs +++ b/Assets/_PROJECT/Scripts/ModeGeneration/FMODWhisperBridge.cs @@ -64,6 +64,11 @@ public class FMODWhisperBridge : MonoBehaviour private bool isRecordingActivated = false; private bool _skipOneFeedFrame = false; + // --- Speech Volume Measurement --- + private float currentVolumeRms = 0f; // Smoothed RMS for external access + private float volumeSmoothing = 0.15f; // How fast the meter reacts (0.1–0.3 good) + + private void Awake() { if (!whisper) whisper = FindObjectOfType(); @@ -254,6 +259,29 @@ public class FMODWhisperBridge : MonoBehaviour Debug.Log("[FMOD→Whisper] Stream deactivated (Whisper stopped; FMOD still recording)."); } + /// + /// Returns current microphone level in dBFS (decibels relative to full-scale). + /// 0 dBFS = digital clipping; normal speech is typically around -35 to -20 dBFS. + /// + public float GetCurrentVolumeDb() + { + // Guard from log(0) + const float minRms = 1e-7f; + float rms = Mathf.Clamp(currentVolumeRms, minRms, 1f); + return 20f * Mathf.Log10(rms); + } + + /// + /// Returns a UI-friendly 0..1 loudness from the current dBFS value. + /// Adjust the dB range to your content/environment if needed. + /// + public float GetNormalizedVolume01() + { + float db = GetCurrentVolumeDb(); // typically ~ -60 .. -15 during use + return Mathf.Clamp01(Mathf.InverseLerp(-60f, -15f, db)); + } + + private void Update() { // Always tick FMOD @@ -295,11 +323,35 @@ public class FMODWhisperBridge : MonoBehaviour try { + + float rmsAccumulator = 0f; + int rmsSampleCount = 0; + + // 1) Measure volume + if (len1 > 0) + { + ComputeRmsFromPcm16(p1, len1, ref rmsAccumulator, ref rmsSampleCount); + } + if (len2 > 0) + { + ComputeRmsFromPcm16(p2, len2, ref rmsAccumulator, ref rmsSampleCount); + } + + if (rmsSampleCount > 0) + { + float rms = Mathf.Sqrt(rmsAccumulator / rmsSampleCount); + + // Smooth the value + currentVolumeRms = Mathf.Lerp(currentVolumeRms, rms, 1f - Mathf.Pow(1f - volumeSmoothing, Time.deltaTime * 60f)); + } + + // 2) Feed audio to Whisper ONLY when active if (shouldFeed && !_skipOneFeedFrame) { if (len1 > 0) CopyPcm16ToFloatAndFeed(p1, len1); if (len2 > 0) CopyPcm16ToFloatAndFeed(p2, len2); } + // If skipping, we just discard this frame to ensure no stale data leaks. } finally @@ -427,4 +479,30 @@ public class FMODWhisperBridge : MonoBehaviour } } + /// + /// Computes RMS (root mean square) from a PCM16 block using only safe code. + /// Uses the shared _shortOverlay buffer (no allocations). + /// Accumulates results into accumulator + sampleCount. + /// + private void ComputeRmsFromPcm16(IntPtr src, uint byteLen, ref float accumulator, ref int sampleCount) + { + // Number of PCM16 samples (2 bytes per sample) + int samples = (int)(byteLen / 2); + if (samples <= 0) return; + + // Ensure overlay buffer exists & is large enough + EnsureShortOverlay(samples, out short[] sBuf); + + // Copy PCM16 into managed buffer (safe) + Marshal.Copy(src, sBuf, 0, samples); + + // Accumulate squared amplitude + for (int i = 0; i < samples; i++) + { + float v = sBuf[i] / 32768f; // normalize to [-1..1] + accumulator += v * v; + } + + sampleCount += samples; + } } diff --git a/Assets/_PROJECT/Scripts/ModeGeneration/ShapeDetection/RadioTransmitter.cs b/Assets/_PROJECT/Scripts/ModeGeneration/ShapeDetection/RadioTransmitter.cs index e7cd4c7c..66229249 100644 --- a/Assets/_PROJECT/Scripts/ModeGeneration/ShapeDetection/RadioTransmitter.cs +++ b/Assets/_PROJECT/Scripts/ModeGeneration/ShapeDetection/RadioTransmitter.cs @@ -14,9 +14,14 @@ public class RadioTransmitter : XRGrabInteractable public FMODWhisperBridge fmodWhisperBridge; public ReleasableButton radioButton; public TextMeshProUGUI computerScreen; + + [Header("Particle System Config")] public ParticleSystem particles; + public float baseEmissionRate = 3; + public float maxExtraEmissionRate = 50; private bool isProcessing; + private ParticleSystem.EmissionModule particleEmission; // Start is called before the first frame update void Start() @@ -26,14 +31,20 @@ public class RadioTransmitter : XRGrabInteractable radioButton.Lock(); isProcessing = false; - var emission = particles.emission; - emission.enabled = false; + particleEmission = particles.emission; + particleEmission.enabled = false; } // Update is called once per frame void Update() { - particles.transform.LookAt(computerScreen.transform.position); + if (particles.emission.enabled) + { + particles.transform.LookAt(computerScreen.transform.position); + Debug.Log("speech volume normalized: " + fmodWhisperBridge.GetNormalizedVolume01()); + Debug.Log("speech volume decibels: " + fmodWhisperBridge.GetCurrentVolumeDb()); + particleEmission.rateOverTime = baseEmissionRate + fmodWhisperBridge.GetNormalizedVolume01() * maxExtraEmissionRate; + } } protected async override void OnSelectEntered(SelectEnterEventArgs args) @@ -55,8 +66,7 @@ public class RadioTransmitter : XRGrabInteractable fmodWhisperBridge.OnWhisperSegmentFinished += OnPlayerSpeechFinished; fmodWhisperBridge.ActivateRecording(); - var emission = particles.emission; - emission.enabled = true; + particleEmission.enabled = true; } } @@ -81,7 +91,6 @@ public class RadioTransmitter : XRGrabInteractable fmodWhisperBridge.OnWhisperSegmentFinished -= OnPlayerSpeechFinished; fmodWhisperBridge.DeactivateRecording(); - var emission = particles.emission; - emission.enabled = false; + particleEmission.enabled = false; } }