diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp index 9c4d34ae..cdb327c9 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp @@ -13,6 +13,8 @@ namespace constexpr int FOV_MAX = 110; constexpr int FOV_SLIDER_MAX = 100; static const int MAX_VOICE_VOLUME_PERCENT = 200; + static const int MIN_VOICE_ACTIVATION_GAIN_PERCENT = 25; + static const int MAX_VOICE_ACTIVATION_GAIN_PERCENT = 400; static const int MAX_DEVICE_LABELS = 64; int ClampFov(int value) @@ -56,6 +58,7 @@ namespace } } + [[maybe_unused]] static void SetControlY(UIScene &scene, UIControl &control, int y) { IggyName yName = scene.registerFastName(L"y"); @@ -63,6 +66,7 @@ namespace control.UpdateControl(); } + [[maybe_unused]] static void SetControlHeight(UIScene &scene, UIControl &control, int height) { IggyName heightName = scene.registerFastName(L"height"); @@ -170,12 +174,14 @@ void UIScene_SettingsGraphicsMenu::initVoiceChatSliders() vcm.init(); } - // Reuse graphics checkboxes as voice mode switches. const bool proximityEnabled = vcm.isProximityEnabled(); + const bool voiceActivationEnabled = vcm.getVoiceInputMode() == VoiceChatManager::VOICE_INPUT_VOICE_ACTIVATION; m_checkboxClouds.init(UIString(L"Proximity"), eControl_Clouds, proximityEnabled); - m_checkboxBedrockFog.init(UIString(L"Voice Activate"), eControl_BedrockFog, !proximityEnabled); - + m_checkboxBedrockFog.init(UIString(L"Voice Activation"), eControl_BedrockFog, voiceActivationEnabled); + m_checkboxCustomSkinAnim.init(UIString(L"Select Input Device"), eControl_CustomSkinAnim, false); + refreshVoiceDeviceCheckboxLabel(); enforceVoiceModeSwitch(-1); + applyVoiceModeFromCheckboxes(); WCHAR tempString[256]; @@ -191,88 +197,15 @@ void UIScene_SettingsGraphicsMenu::initVoiceChatSliders() swprintf(tempString, 256, L"Voice Chat Volume: %d%%", voiceVolume); m_sliderGamma.init(tempString, eControl_Gamma, 0, MAX_VOICE_VOLUME_PERCENT, voiceVolume); - vector captureNames; - vector playbackNames; - vcm.getCaptureDeviceNames(captureNames, true); - vcm.getPlaybackDeviceNames(playbackNames, true); + int voiceActivationGain = vcm.getVoiceActivationGainPercent(); + if (voiceActivationGain < MIN_VOICE_ACTIVATION_GAIN_PERCENT) voiceActivationGain = MIN_VOICE_ACTIVATION_GAIN_PERCENT; + if (voiceActivationGain > MAX_VOICE_ACTIVATION_GAIN_PERCENT) voiceActivationGain = MAX_VOICE_ACTIVATION_GAIN_PERCENT; + swprintf(tempString, 256, L"Voice Activation Gain: %d%%", voiceActivationGain); + m_sliderFOV.init(tempString, eControl_FOV, MIN_VOICE_ACTIVATION_GAIN_PERCENT, MAX_VOICE_ACTIVATION_GAIN_PERCENT, voiceActivationGain); - wchar_t captureLabels[MAX_DEVICE_LABELS][256]; - wchar_t playbackLabels[MAX_DEVICE_LABELS][256]; - int captureCount = 0; - int playbackCount = 0; - BuildDeviceLabels(captureNames, L"Input", captureLabels, captureCount); - BuildDeviceLabels(playbackNames, L"Output", playbackLabels, playbackCount); - - int captureIndex = vcm.getSelectedCaptureMenuIndex(); - int playbackIndex = vcm.getSelectedPlaybackMenuIndex(); - if (captureIndex < 0) captureIndex = 0; - if (captureIndex >= captureCount) captureIndex = 0; - if (playbackIndex < 0) playbackIndex = 0; - if (playbackIndex >= playbackCount) playbackIndex = 0; - - m_sliderFOV.setAllPossibleLabels(captureCount, captureLabels); - m_sliderFOV.init(captureLabels[captureIndex], eControl_FOV, 0, captureCount - 1, captureIndex); - - m_sliderInterfaceOpacity.setAllPossibleLabels(playbackCount, playbackLabels); - m_sliderInterfaceOpacity.init(playbackLabels[playbackIndex], eControl_InterfaceOpacity, 0, playbackCount - 1, playbackIndex); + refreshVoiceDeviceSlider(); doHorizontalResizeCheck(); - - // Collapse the unused third checkbox row once, without any persistent layout workaround. - m_checkboxBedrockFog.UpdateControl(); - m_checkboxCustomSkinAnim.UpdateControl(); - m_sliderRenderDistance.UpdateControl(); - m_sliderGamma.UpdateControl(); - m_sliderFOV.UpdateControl(); - m_sliderInterfaceOpacity.UpdateControl(); - - int collapseAmount = m_checkboxCustomSkinAnim.getYPos() - m_checkboxBedrockFog.getYPos(); - if (collapseAmount > 0 && collapseAmount < 128) - { - SetControlY(*this, m_sliderRenderDistance, m_sliderRenderDistance.getYPos() - collapseAmount); - SetControlY(*this, m_sliderGamma, m_sliderGamma.getYPos() - collapseAmount); - SetControlY(*this, m_sliderFOV, m_sliderFOV.getYPos() - collapseAmount); - SetControlY(*this, m_sliderInterfaceOpacity, m_sliderInterfaceOpacity.getYPos() - collapseAmount); - } - - // Match the panel height to the collapsed content once. - m_sliderInterfaceOpacity.UpdateControl(); - IggyValuePath *root = IggyPlayerRootPath(getMovie()); - static const char *kPanelCandidates[] = - { - "MainPanel", - "Panel", - "BackgroundPanel", - "Background", - "Dialog", - "Window" - }; - const int bottomPadding = 16; - for (int i = 0; i < static_cast(sizeof(kPanelCandidates) / sizeof(kPanelCandidates[0])); ++i) - { - UIControl panel; - if (!panel.setupControl(this, root, kPanelCandidates[i])) - { - continue; - } - - panel.UpdateControl(); - if (panel.getHeight() <= 0) - { - continue; - } - - const int desiredHeight = (m_sliderInterfaceOpacity.getYPos() + m_sliderInterfaceOpacity.getHeight() + bottomPadding) - panel.getYPos(); - if (desiredHeight > 0) - { - SetControlHeight(*this, panel, desiredHeight); - } - break; - } - - // Hide the unused checkbox without movie recenter/reflow. - m_checkboxCustomSkinAnim.setVisible(false); - m_checkboxCustomSkinAnim.setHidden(true); } UIScene_SettingsGraphicsMenu::~UIScene_SettingsGraphicsMenu() @@ -282,15 +215,6 @@ UIScene_SettingsGraphicsMenu::~UIScene_SettingsGraphicsMenu() void UIScene_SettingsGraphicsMenu::tick() { UIScene::tick(); - - if (!m_bVoiceChatMode) - { - return; - } - - // Keep mode switches mutually exclusive even if UI events arrive out-of-order. - enforceVoiceModeSwitch(-1); - applyVoiceModeFromCheckboxes(); } wstring UIScene_SettingsGraphicsMenu::getMoviePath() @@ -439,17 +363,21 @@ void UIScene_SettingsGraphicsMenu::handleCheckboxToggled(F64 controlId, bool sel { case eControl_Clouds: m_checkboxClouds.setChecked(selected); - m_checkboxBedrockFog.setChecked(!selected); enforceVoiceModeSwitch(eControl_Clouds); applyVoiceModeFromCheckboxes(); break; case eControl_BedrockFog: m_checkboxBedrockFog.setChecked(selected); - m_checkboxClouds.setChecked(!selected); enforceVoiceModeSwitch(eControl_BedrockFog); applyVoiceModeFromCheckboxes(); break; + + case eControl_CustomSkinAnim: + m_checkboxCustomSkinAnim.setChecked(selected); + refreshVoiceDeviceCheckboxLabel(); + refreshVoiceDeviceSlider(); + break; } } @@ -474,37 +402,35 @@ void UIScene_SettingsGraphicsMenu::handleVoiceSliderMove(int controlId, int valu break; case eControl_FOV: - if (vcm.selectCaptureMenuIndex(value)) - { - vector names; - vcm.getCaptureDeviceNames(names, true); - if (value >= 0 && value < static_cast(names.size())) - { - swprintf(tempString, 256, L"Input: %ls", names[value].c_str()); - m_sliderFOV.setLabel(tempString); - } - } + vcm.setVoiceActivationGainPercent(value); m_sliderFOV.handleSliderMove(value); + swprintf(tempString, 256, L"Voice Activation Gain: %d%%", value); + m_sliderFOV.setLabel(tempString); break; case eControl_InterfaceOpacity: - if (vcm.selectPlaybackMenuIndex(value)) + if (m_checkboxCustomSkinAnim.IsChecked()) { - vector names; - vcm.getPlaybackDeviceNames(names, true); - if (value >= 0 && value < static_cast(names.size())) + if (vcm.selectPlaybackMenuIndex(value)) { - swprintf(tempString, 256, L"Output: %ls", names[value].c_str()); - m_sliderInterfaceOpacity.setLabel(tempString); + refreshVoiceDeviceSlider(); + } + } + else + { + if (vcm.selectCaptureMenuIndex(value)) + { + refreshVoiceDeviceSlider(); } } - m_sliderInterfaceOpacity.handleSliderMove(value); break; } } void UIScene_SettingsGraphicsMenu::enforceVoiceModeSwitch(int preferredControl) { + (void)preferredControl; + if (!m_bVoiceChatMode) { return; @@ -513,18 +439,9 @@ void UIScene_SettingsGraphicsMenu::enforceVoiceModeSwitch(int preferredControl) bool proximity = m_checkboxClouds.IsChecked(); bool voiceActivate = m_checkboxBedrockFog.IsChecked(); - if (proximity == voiceActivate) + if (voiceActivate && !proximity) { - if (preferredControl == eControl_BedrockFog) - { - proximity = false; - voiceActivate = true; - } - else - { - proximity = true; - voiceActivate = false; - } + proximity = true; } m_checkboxClouds.setChecked(proximity); @@ -537,7 +454,40 @@ void UIScene_SettingsGraphicsMenu::applyVoiceModeFromCheckboxes() enforceVoiceModeSwitch(-1); const bool proximityEnabled = m_checkboxClouds.IsChecked(); vcm.setProximityEnabled(proximityEnabled); - vcm.setVoiceInputMode(!proximityEnabled + vcm.setVoiceInputMode(m_checkboxBedrockFog.IsChecked() ? VoiceChatManager::VOICE_INPUT_VOICE_ACTIVATION : VoiceChatManager::VOICE_INPUT_PUSH_TO_TALK); } + +void UIScene_SettingsGraphicsMenu::refreshVoiceDeviceCheckboxLabel() +{ + const bool usePlaybackDevice = m_checkboxCustomSkinAnim.IsChecked(); + m_checkboxCustomSkinAnim.setLabel(usePlaybackDevice ? UIString(L"Select Output Device") : UIString(L"Select Input Device"), true, true); +} + +void UIScene_SettingsGraphicsMenu::refreshVoiceDeviceSlider() +{ + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + const bool usePlaybackDevice = m_checkboxCustomSkinAnim.IsChecked(); + + vector deviceNames; + if (usePlaybackDevice) + { + vcm.getPlaybackDeviceNames(deviceNames, true); + } + else + { + vcm.getCaptureDeviceNames(deviceNames, true); + } + + wchar_t deviceLabels[MAX_DEVICE_LABELS][256]; + int deviceCount = 0; + BuildDeviceLabels(deviceNames, usePlaybackDevice ? L"Output" : L"Input", deviceLabels, deviceCount); + + int selectedIndex = usePlaybackDevice ? vcm.getSelectedPlaybackMenuIndex() : vcm.getSelectedCaptureMenuIndex(); + if (selectedIndex < 0) selectedIndex = 0; + if (selectedIndex >= deviceCount) selectedIndex = 0; + + m_sliderInterfaceOpacity.setAllPossibleLabels(deviceCount, deviceLabels); + m_sliderInterfaceOpacity.init(deviceLabels[selectedIndex], eControl_InterfaceOpacity, 0, deviceCount - 1, selectedIndex); +} diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h index cce478fc..ffcf45ae 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h @@ -37,6 +37,8 @@ private: void handleVoiceSliderMove(int controlId, int value); void enforceVoiceModeSwitch(int preferredControl); void applyVoiceModeFromCheckboxes(); + void refreshVoiceDeviceCheckboxLabel(); + void refreshVoiceDeviceSlider(); public: UIScene_SettingsGraphicsMenu(int iPad, void *initData, UILayer *parentLayer); virtual ~UIScene_SettingsGraphicsMenu(); diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp index 1904685f..8fce4cb0 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SettingsOptionsMenu.cpp @@ -69,8 +69,9 @@ void UIScene_SettingsOptionsMenu::setupVoiceChatMenu() m_bMashUpWorldsUnhideOption = false; const bool proximityEnabled = vcm.isProximityEnabled(); + const bool voiceActivationEnabled = vcm.getVoiceInputMode() == VoiceChatManager::VOICE_INPUT_VOICE_ACTIVATION; m_checkboxViewBob.init(UIString(L"Proximity"), eControl_ViewBob, proximityEnabled); - m_checkboxShowHints.init(UIString(L"Voice Activate"), eControl_ShowHints, !proximityEnabled); + m_checkboxShowHints.init(UIString(L"Voice Activation"), eControl_ShowHints, voiceActivationEnabled); removeControl(&m_checkboxShowTooltips, true); removeControl(&m_checkboxInGameGamertags, true); @@ -247,6 +248,8 @@ void UIScene_SettingsOptionsMenu::setVoiceDifficultyLabel() void UIScene_SettingsOptionsMenu::enforceVoiceModeSwitch(int preferredControl) { + (void)preferredControl; + if (!m_bVoiceChatMode) { return; @@ -255,18 +258,9 @@ void UIScene_SettingsOptionsMenu::enforceVoiceModeSwitch(int preferredControl) bool proximity = m_checkboxViewBob.IsChecked(); bool voiceActivate = m_checkboxShowHints.IsChecked(); - if (proximity == voiceActivate) + if (voiceActivate && !proximity) { - if (preferredControl == eControl_ShowHints) - { - proximity = false; - voiceActivate = true; - } - else - { - proximity = true; - voiceActivate = false; - } + proximity = true; } m_checkboxViewBob.setChecked(proximity); @@ -403,20 +397,12 @@ void UIScene_SettingsOptionsMenu::handlePress(F64 controlId, F64 childId) case eControl_ViewBob: if (m_bVoiceChatMode) { - m_checkboxViewBob.setChecked(true); - m_checkboxShowHints.setChecked(false); - enforceVoiceModeSwitch(eControl_ViewBob); - setGameSettings(); break; } break; case eControl_ShowHints: if (m_bVoiceChatMode) { - m_checkboxViewBob.setChecked(false); - m_checkboxShowHints.setChecked(true); - enforceVoiceModeSwitch(eControl_ShowHints); - setGameSettings(); break; } break; @@ -446,13 +432,11 @@ void UIScene_SettingsOptionsMenu::handleCheckboxToggled(F64 controlId, bool sele { case eControl_ViewBob: m_checkboxViewBob.setChecked(selected); - m_checkboxShowHints.setChecked(!selected); enforceVoiceModeSwitch(eControl_ViewBob); setGameSettings(); break; case eControl_ShowHints: m_checkboxShowHints.setChecked(selected); - m_checkboxViewBob.setChecked(!selected); enforceVoiceModeSwitch(eControl_ShowHints); setGameSettings(); break; @@ -534,7 +518,7 @@ void UIScene_SettingsOptionsMenu::setGameSettings() VoiceChatManager &vcm = VoiceChatManager::getInstance(); const bool proximityEnabled = m_checkboxViewBob.IsChecked(); vcm.setProximityEnabled(proximityEnabled); - vcm.setVoiceInputMode(!proximityEnabled + vcm.setVoiceInputMode(m_checkboxShowHints.IsChecked() ? VoiceChatManager::VOICE_INPUT_VOICE_ACTIVATION : VoiceChatManager::VOICE_INPUT_PUSH_TO_TALK); return; diff --git a/Minecraft.Client/VoiceChatManager.cpp b/Minecraft.Client/VoiceChatManager.cpp index 9a03b6c0..c129616f 100644 --- a/Minecraft.Client/VoiceChatManager.cpp +++ b/Minecraft.Client/VoiceChatManager.cpp @@ -8,12 +8,25 @@ #include "..\Minecraft.World\Connection.h" #include "..\Minecraft.World\System.h" #include +#include // Include miniaudio #include "Common\Audio\miniaudio.h" namespace { + static const int MIN_VOICE_ACTIVATION_GAIN_PERCENT = 25; + static const int MAX_VOICE_ACTIVATION_GAIN_PERCENT = 400; + static const int VOICE_ACTIVATION_OPEN_PEAK = 900; + static const int VOICE_ACTIVATION_OPEN_RMS = 220; + static const int VOICE_ACTIVATION_CLOSE_PEAK = 600; + static const int VOICE_ACTIVATION_CLOSE_RMS = 160; + static const int VOICE_ACTIVATION_HOLD_FRAMES = 10; + static const int PUSH_TO_TALK_GATE_PEAK = 450; + static const int PUSH_TO_TALK_GATE_RMS = 120; + static const int PUSH_TO_TALK_HOLD_FRAMES = 4; + static const float CAPTURE_HIGHPASS_ALPHA = 0.995f; + static void RefreshDeviceLists( std::vector &playbackDevices, std::vector &captureDevices, @@ -44,6 +57,48 @@ namespace if (selectedIndex >= static_cast(count)) return nullptr; return &pInfos[selectedIndex].id; } + + static int SampleMagnitude(int sample) + { + return sample >= 0 ? sample : -sample; + } + + static short ClampSampleToShort(int sample) + { + if (sample > 32767) return 32767; + if (sample < -32768) return -32768; + return static_cast(sample); + } + + struct VoiceFrameMetrics + { + int peak; + int rms; + }; + + static VoiceFrameMetrics MeasureVoiceFrame(const short *samples, int sampleCount, int gainPercent) + { + VoiceFrameMetrics metrics = { 0, 0 }; + if (samples == nullptr || sampleCount <= 0) + { + return metrics; + } + + long long energy = 0; + for (int i = 0; i < sampleCount; ++i) + { + int sample = (static_cast(samples[i]) * gainPercent) / 100; + const int magnitude = SampleMagnitude(sample); + if (magnitude > metrics.peak) + { + metrics.peak = magnitude; + } + energy += static_cast(magnitude) * static_cast(magnitude); + } + + metrics.rms = static_cast(std::sqrt(static_cast(energy) / static_cast(sampleCount))); + return metrics; + } } VoiceChatManager &VoiceChatManager::getInstance() @@ -63,12 +118,16 @@ VoiceChatManager::VoiceChatManager() , m_isPushToTalkActive(false) , m_voiceInputMode(VOICE_INPUT_PUSH_TO_TALK) , m_proximityEnabled(true) - , m_voiceThreshold(300) , m_selectedCaptureDevice(-1) , m_selectedPlaybackDevice(-1) , m_localSequence(0) , m_micVolumePercent(100) , m_voiceChatVolumePercent(100) + , m_voiceActivationGainPercent(100) + , m_voiceActivationHoldFrames(0) + , m_pushToTalkHoldFrames(0) + , m_captureFilterLastInput(0.0f) + , m_captureFilterLastOutput(0.0f) { memset(m_captureBuffer, 0, sizeof(m_captureBuffer)); InitializeCriticalSection(&m_captureLock); @@ -165,6 +224,14 @@ void VoiceChatManager::init() { if (m_initialized) return; + m_captureWritePos = 0; + m_captureReadPos = 0; + m_voiceActivationHoldFrames = 0; + m_pushToTalkHoldFrames = 0; + m_captureFilterLastInput = 0.0f; + m_captureFilterLastOutput = 0.0f; + memset(m_captureBuffer, 0, sizeof(m_captureBuffer)); + m_audioContext = new ma_context(); if (ma_context_init(nullptr, 0, nullptr, m_audioContext) != MA_SUCCESS) { @@ -288,6 +355,10 @@ void VoiceChatManager::shutdown() } m_captureDevices.clear(); m_playbackDevices.clear(); + m_voiceActivationHoldFrames = 0; + m_pushToTalkHoldFrames = 0; + m_captureFilterLastInput = 0.0f; + m_captureFilterLastOutput = 0.0f; m_initialized = false; app.DebugPrintf("VoiceChat: Shutdown\n"); @@ -295,7 +366,14 @@ void VoiceChatManager::shutdown() void VoiceChatManager::setVoiceInputMode(VoiceInputMode mode) { + if (m_voiceInputMode == mode) + { + return; + } + m_voiceInputMode = mode; + m_voiceActivationHoldFrames = 0; + m_pushToTalkHoldFrames = 0; app.DebugPrintf("VoiceChat: Input mode set to %s\n", m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK ? "PTT" : "VoiceActivation"); } @@ -317,6 +395,17 @@ void VoiceChatManager::toggleProximityEnabled() app.DebugPrintf("VoiceChat: Proximity %s\n", m_proximityEnabled ? "enabled" : "disabled"); } +void VoiceChatManager::setProximityEnabled(bool enabled) +{ + if (m_proximityEnabled == enabled) + { + return; + } + + m_proximityEnabled = enabled; + app.DebugPrintf("VoiceChat: Proximity %s\n", m_proximityEnabled ? "enabled" : "disabled"); +} + bool VoiceChatManager::cycleCaptureDevice(int dir) { if (!m_initialized) init(); @@ -456,26 +545,64 @@ void VoiceChatManager::tick(Minecraft *minecraft) { int sample = m_captureBuffer[m_captureReadPos]; sample = (sample * m_micVolumePercent) / 100; - if (sample > 32767) sample = 32767; - if (sample < -32768) sample = -32768; - frameBuffer[i] = (short)sample; + + // Strip DC bias and low-frequency rumble so we do not transmit a bed of hiss/hum when the mic is idle. + const float filteredSample = + static_cast(sample) - m_captureFilterLastInput + (CAPTURE_HIGHPASS_ALPHA * m_captureFilterLastOutput); + m_captureFilterLastInput = static_cast(sample); + m_captureFilterLastOutput = filteredSample; + + frameBuffer[i] = ClampSampleToShort(static_cast(filteredSample)); m_captureReadPos = (m_captureReadPos + 1) % CAPTURE_BUFFER_SIZE; } + const VoiceFrameMetrics pushToTalkMetrics = MeasureVoiceFrame(frameBuffer, FRAMES_PER_PACKET, 100); + bool pushToTalkSpeechDetected = + pushToTalkMetrics.peak >= PUSH_TO_TALK_GATE_PEAK || + pushToTalkMetrics.rms >= PUSH_TO_TALK_GATE_RMS; + + if (m_isPushToTalkActive) + { + if (pushToTalkSpeechDetected) + { + m_pushToTalkHoldFrames = PUSH_TO_TALK_HOLD_FRAMES; + } + else if (m_pushToTalkHoldFrames > 0) + { + --m_pushToTalkHoldFrames; + } + } + else + { + m_pushToTalkHoldFrames = 0; + } + + const VoiceFrameMetrics voiceActivationMetrics = + MeasureVoiceFrame(frameBuffer, FRAMES_PER_PACKET, m_voiceActivationGainPercent); + const bool useCloseThresholds = m_voiceActivationHoldFrames > 0; + const int requiredPeak = useCloseThresholds ? VOICE_ACTIVATION_CLOSE_PEAK : VOICE_ACTIVATION_OPEN_PEAK; + const int requiredRms = useCloseThresholds ? VOICE_ACTIVATION_CLOSE_RMS : VOICE_ACTIVATION_OPEN_RMS; + const bool voiceActivationDetected = + voiceActivationMetrics.peak >= requiredPeak || + voiceActivationMetrics.rms >= requiredRms; + + if (voiceActivationDetected) + { + m_voiceActivationHoldFrames = VOICE_ACTIVATION_HOLD_FRAMES; + } + else if (m_voiceActivationHoldFrames > 0) + { + --m_voiceActivationHoldFrames; + } + bool shouldSend = false; if (m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK) { - shouldSend = m_isPushToTalkActive; + shouldSend = m_isPushToTalkActive && (pushToTalkSpeechDetected || m_pushToTalkHoldFrames > 0); } else { - int peak = 0; - for (int i = 0; i < FRAMES_PER_PACKET; ++i) - { - int sample = abs((int)frameBuffer[i]); - if (sample > peak) peak = sample; - } - shouldSend = peak >= m_voiceThreshold; + shouldSend = m_voiceActivationHoldFrames > 0; } if (shouldSend) @@ -630,3 +757,10 @@ void VoiceChatManager::setVoiceChatVolumePercent(int percent) if (percent > 200) percent = 200; m_voiceChatVolumePercent = percent; } + +void VoiceChatManager::setVoiceActivationGainPercent(int percent) +{ + if (percent < MIN_VOICE_ACTIVATION_GAIN_PERCENT) percent = MIN_VOICE_ACTIVATION_GAIN_PERCENT; + if (percent > MAX_VOICE_ACTIVATION_GAIN_PERCENT) percent = MAX_VOICE_ACTIVATION_GAIN_PERCENT; + m_voiceActivationGainPercent = percent; +} diff --git a/Minecraft.Client/VoiceChatManager.h b/Minecraft.Client/VoiceChatManager.h index 56d2ba27..4157f0ff 100644 --- a/Minecraft.Client/VoiceChatManager.h +++ b/Minecraft.Client/VoiceChatManager.h @@ -34,7 +34,14 @@ public: void setListenerPosition(double x, double y, double z); // Push-to-talk control - void setPushToTalk(bool active) { m_isPushToTalkActive = active; } + void setPushToTalk(bool active) + { + m_isPushToTalkActive = active; + if (!active) + { + m_pushToTalkHoldFrames = 0; + } + } bool isPushToTalkActive() const { return m_isPushToTalkActive; } // Voice input mode controls @@ -43,7 +50,7 @@ public: void toggleVoiceInputMode(); // Proximity chat controls - void setProximityEnabled(bool enabled) { m_proximityEnabled = enabled; } + void setProximityEnabled(bool enabled); bool isProximityEnabled() const { return m_proximityEnabled; } void toggleProximityEnabled(); @@ -64,6 +71,8 @@ public: int getMicVolumePercent() const { return m_micVolumePercent; } void setVoiceChatVolumePercent(int percent); int getVoiceChatVolumePercent() const { return m_voiceChatVolumePercent; } + void setVoiceActivationGainPercent(int percent); + int getVoiceActivationGainPercent() const { return m_voiceActivationGainPercent; } bool isInitialized() const { return m_initialized; } @@ -92,7 +101,6 @@ private: bool m_isPushToTalkActive; VoiceInputMode m_voiceInputMode; bool m_proximityEnabled; - int m_voiceThreshold; std::vector m_captureDevices; std::vector m_playbackDevices; int m_selectedCaptureDevice; @@ -100,6 +108,11 @@ private: unsigned short m_localSequence; int m_micVolumePercent; int m_voiceChatVolumePercent; + int m_voiceActivationGainPercent; + int m_voiceActivationHoldFrames; + int m_pushToTalkHoldFrames; + float m_captureFilterLastInput; + float m_captureFilterLastOutput; // Capture buffer (ring buffer for PCM data captured from mic) static const int CAPTURE_BUFFER_SIZE = 96000; // 2 seconds at 48kHz mono