added voice chat menu, added udp transport, audio cleanup

This commit is contained in:
haxi0
2026-03-10 23:27:52 +03:00
parent 85b0f070b9
commit f4116519fe
25 changed files with 1395 additions and 234 deletions
+329 -31
View File
@@ -7,10 +7,45 @@
#include "..\Minecraft.World\net.minecraft.world.entity.player.h"
#include "..\Minecraft.World\Connection.h"
#include "..\Minecraft.World\System.h"
#include <cstring>
// Include miniaudio
#include "Common\Audio\miniaudio.h"
namespace
{
static void RefreshDeviceLists(
std::vector<std::wstring> &playbackDevices,
std::vector<std::wstring> &captureDevices,
ma_device_info *pPlaybackInfos,
ma_uint32 playbackCount,
ma_device_info *pCaptureInfos,
ma_uint32 captureCount)
{
playbackDevices.clear();
captureDevices.clear();
for (ma_uint32 i = 0; i < playbackCount; ++i)
{
const char *name = pPlaybackInfos[i].name;
playbackDevices.emplace_back(name, name + strlen(name));
}
for (ma_uint32 i = 0; i < captureCount; ++i)
{
const char *name = pCaptureInfos[i].name;
captureDevices.emplace_back(name, name + strlen(name));
}
}
static const ma_device_id *GetSelectedDeviceId(int selectedIndex, ma_device_info *pInfos, ma_uint32 count)
{
if (selectedIndex < 0) return nullptr; // default device
if (selectedIndex >= static_cast<int>(count)) return nullptr;
return &pInfos[selectedIndex].id;
}
}
VoiceChatManager &VoiceChatManager::getInstance()
{
static VoiceChatManager instance;
@@ -19,12 +54,21 @@ VoiceChatManager &VoiceChatManager::getInstance()
VoiceChatManager::VoiceChatManager()
: m_initialized(false)
, m_audioContext(nullptr)
, m_captureDevice(nullptr)
, m_playbackDevice(nullptr)
, m_captureWritePos(0)
, m_captureReadPos(0)
, m_listenerX(0), m_listenerY(0), m_listenerZ(0)
, 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)
{
memset(m_captureBuffer, 0, sizeof(m_captureBuffer));
InitializeCriticalSection(&m_captureLock);
@@ -70,25 +114,31 @@ void VoiceChatManager::onPlaybackAudio(ma_device *pDevice, void *pOutput, const
{
RemoteVoiceStream &stream = pair.second;
// Calculate distance attenuation
double dx = stream.x - mgr->m_listenerX;
double dy = stream.y - mgr->m_listenerY;
double dz = stream.z - mgr->m_listenerZ;
double dist = sqrt(dx * dx + dy * dy + dz * dz);
float volume = 1.0f;
if (dist >= MAX_VOICE_DISTANCE)
if (mgr->m_proximityEnabled)
{
volume = 0.0f;
}
else if (dist > 1.0)
{
// Linear falloff with exponential tail
float linearFactor = 1.0f - (float)(dist / MAX_VOICE_DISTANCE);
float expFactor = (float)exp(-dist * 0.08);
volume = linearFactor * expFactor;
// Calculate distance attenuation
double dx = stream.x - mgr->m_listenerX;
double dy = stream.y - mgr->m_listenerY;
double dz = stream.z - mgr->m_listenerZ;
double dist = sqrt(dx * dx + dy * dy + dz * dz);
if (dist >= MAX_VOICE_DISTANCE)
{
volume = 0.0f;
}
else if (dist > 1.0)
{
// Linear falloff with exponential tail
float linearFactor = 1.0f - (float)(dist / MAX_VOICE_DISTANCE);
float expFactor = (float)exp(-dist * 0.08);
volume = linearFactor * expFactor;
}
}
if (volume <= 0.001f) continue;
volume *= (float)mgr->m_voiceChatVolumePercent / 100.0f;
if (volume <= 0.001f) continue;
// Mix this stream's audio into the output
@@ -115,30 +165,54 @@ void VoiceChatManager::init()
{
if (m_initialized) return;
m_audioContext = new ma_context();
if (ma_context_init(nullptr, 0, nullptr, m_audioContext) != MA_SUCCESS)
{
app.DebugPrintf("VoiceChat: Failed to initialize audio context\n");
delete m_audioContext;
m_audioContext = nullptr;
return;
}
ma_device_info *pPlaybackInfos = nullptr;
ma_uint32 playbackCount = 0;
ma_device_info *pCaptureInfos = nullptr;
ma_uint32 captureCount = 0;
if (ma_context_get_devices(m_audioContext, &pPlaybackInfos, &playbackCount, &pCaptureInfos, &captureCount) != MA_SUCCESS)
{
app.DebugPrintf("VoiceChat: Failed to enumerate audio devices\n");
ma_context_uninit(m_audioContext);
delete m_audioContext;
m_audioContext = nullptr;
return;
}
RefreshDeviceLists(m_playbackDevices, m_captureDevices, pPlaybackInfos, playbackCount, pCaptureInfos, captureCount);
if (m_selectedCaptureDevice >= static_cast<int>(m_captureDevices.size())) m_selectedCaptureDevice = -1;
if (m_selectedPlaybackDevice >= static_cast<int>(m_playbackDevices.size())) m_selectedPlaybackDevice = -1;
// Initialize capture device (microphone)
m_captureDevice = new ma_device();
ma_device_config captureConfig = ma_device_config_init(ma_device_type_capture);
captureConfig.capture.format = ma_format_s16;
captureConfig.capture.channels = CHANNELS;
captureConfig.capture.pDeviceID = GetSelectedDeviceId(m_selectedCaptureDevice, pCaptureInfos, captureCount);
captureConfig.sampleRate = SAMPLE_RATE;
captureConfig.dataCallback = onCaptureAudio;
captureConfig.pUserData = this;
if (ma_device_init(nullptr, &captureConfig, m_captureDevice) != MA_SUCCESS)
if (ma_device_init(m_audioContext, &captureConfig, m_captureDevice) != MA_SUCCESS)
{
app.DebugPrintf("VoiceChat: Failed to initialize capture device\n");
delete m_captureDevice;
m_captureDevice = nullptr;
return;
}
if (ma_device_start(m_captureDevice) != MA_SUCCESS)
else if (ma_device_start(m_captureDevice) != MA_SUCCESS)
{
app.DebugPrintf("VoiceChat: Failed to start capture device\n");
ma_device_uninit(m_captureDevice);
delete m_captureDevice;
m_captureDevice = nullptr;
return;
}
// Initialize playback device (speakers)
@@ -146,11 +220,12 @@ void VoiceChatManager::init()
ma_device_config playbackConfig = ma_device_config_init(ma_device_type_playback);
playbackConfig.playback.format = ma_format_s16;
playbackConfig.playback.channels = CHANNELS;
playbackConfig.playback.pDeviceID = GetSelectedDeviceId(m_selectedPlaybackDevice, pPlaybackInfos, playbackCount);
playbackConfig.sampleRate = SAMPLE_RATE;
playbackConfig.dataCallback = onPlaybackAudio;
playbackConfig.pUserData = this;
if (ma_device_init(nullptr, &playbackConfig, m_playbackDevice) != MA_SUCCESS)
if (ma_device_init(m_audioContext, &playbackConfig, m_playbackDevice) != MA_SUCCESS)
{
app.DebugPrintf("VoiceChat: Failed to initialize playback device\n");
delete m_playbackDevice;
@@ -168,9 +243,19 @@ void VoiceChatManager::init()
}
}
m_initialized = true;
app.DebugPrintf("VoiceChat: Initialized (capture=%s, playback=%s)\n",
m_captureDevice ? "OK" : "FAIL", m_playbackDevice ? "OK" : "FAIL");
m_initialized = (m_captureDevice != nullptr || m_playbackDevice != nullptr);
if (!m_initialized)
{
ma_context_uninit(m_audioContext);
delete m_audioContext;
m_audioContext = nullptr;
}
app.DebugPrintf("VoiceChat: Initialized (capture=%s, playback=%s, mode=%s, proximity=%s)\n",
m_captureDevice ? "OK" : "FAIL",
m_playbackDevice ? "OK" : "FAIL",
m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK ? "PTT" : "VAD",
m_proximityEnabled ? "ON" : "OFF");
}
void VoiceChatManager::shutdown()
@@ -195,10 +280,157 @@ void VoiceChatManager::shutdown()
m_remoteStreams.clear();
LeaveCriticalSection(&m_playbackLock);
if (m_audioContext)
{
ma_context_uninit(m_audioContext);
delete m_audioContext;
m_audioContext = nullptr;
}
m_captureDevices.clear();
m_playbackDevices.clear();
m_initialized = false;
app.DebugPrintf("VoiceChat: Shutdown\n");
}
void VoiceChatManager::setVoiceInputMode(VoiceInputMode mode)
{
m_voiceInputMode = mode;
app.DebugPrintf("VoiceChat: Input mode set to %s\n", m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK ? "PTT" : "VoiceActivation");
}
void VoiceChatManager::toggleVoiceInputMode()
{
if (m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK)
{
setVoiceInputMode(VOICE_INPUT_VOICE_ACTIVATION);
}
else
{
setVoiceInputMode(VOICE_INPUT_PUSH_TO_TALK);
}
}
void VoiceChatManager::toggleProximityEnabled()
{
m_proximityEnabled = !m_proximityEnabled;
app.DebugPrintf("VoiceChat: Proximity %s\n", m_proximityEnabled ? "enabled" : "disabled");
}
bool VoiceChatManager::cycleCaptureDevice(int dir)
{
if (!m_initialized) init();
if (m_captureDevices.empty()) return false;
const int totalSlots = static_cast<int>(m_captureDevices.size()) + 1; // default + explicit devices
int slot = m_selectedCaptureDevice + 1; // -1 -> 0
slot = (slot + dir) % totalSlots;
if (slot < 0) slot += totalSlots;
m_selectedCaptureDevice = slot - 1;
shutdown();
init();
app.DebugPrintf("VoiceChat: Capture device -> %ls\n", getSelectedCaptureDeviceName().c_str());
return m_initialized;
}
bool VoiceChatManager::cyclePlaybackDevice(int dir)
{
if (!m_initialized) init();
if (m_playbackDevices.empty()) return false;
const int totalSlots = static_cast<int>(m_playbackDevices.size()) + 1; // default + explicit devices
int slot = m_selectedPlaybackDevice + 1; // -1 -> 0
slot = (slot + dir) % totalSlots;
if (slot < 0) slot += totalSlots;
m_selectedPlaybackDevice = slot - 1;
shutdown();
init();
app.DebugPrintf("VoiceChat: Playback device -> %ls\n", getSelectedPlaybackDeviceName().c_str());
return m_initialized;
}
wstring VoiceChatManager::getSelectedCaptureDeviceName() const
{
if (m_selectedCaptureDevice < 0 || m_selectedCaptureDevice >= static_cast<int>(m_captureDevices.size()))
{
return L"Default";
}
return m_captureDevices[m_selectedCaptureDevice];
}
wstring VoiceChatManager::getSelectedPlaybackDeviceName() const
{
if (m_selectedPlaybackDevice < 0 || m_selectedPlaybackDevice >= static_cast<int>(m_playbackDevices.size()))
{
return L"Default";
}
return m_playbackDevices[m_selectedPlaybackDevice];
}
void VoiceChatManager::getCaptureDeviceNames(vector<wstring> &outNames, bool includeDefault) const
{
outNames.clear();
if (includeDefault) outNames.push_back(L"Default");
for (const auto &name : m_captureDevices)
{
outNames.push_back(name);
}
}
void VoiceChatManager::getPlaybackDeviceNames(vector<wstring> &outNames, bool includeDefault) const
{
outNames.clear();
if (includeDefault) outNames.push_back(L"Default");
for (const auto &name : m_playbackDevices)
{
outNames.push_back(name);
}
}
int VoiceChatManager::getSelectedCaptureMenuIndex() const
{
return m_selectedCaptureDevice + 1;
}
int VoiceChatManager::getSelectedPlaybackMenuIndex() const
{
return m_selectedPlaybackDevice + 1;
}
bool VoiceChatManager::selectCaptureMenuIndex(int menuIndex)
{
if (!m_initialized) init();
const int maxIndex = static_cast<int>(m_captureDevices.size());
if (menuIndex < 0 || menuIndex > maxIndex) return false;
const int nextSelected = menuIndex - 1;
if (nextSelected == m_selectedCaptureDevice) return true;
m_selectedCaptureDevice = nextSelected;
shutdown();
init();
return m_initialized;
}
bool VoiceChatManager::selectPlaybackMenuIndex(int menuIndex)
{
if (!m_initialized) init();
const int maxIndex = static_cast<int>(m_playbackDevices.size());
if (menuIndex < 0 || menuIndex > maxIndex) return false;
const int nextSelected = menuIndex - 1;
if (nextSelected == m_selectedPlaybackDevice) return true;
m_selectedPlaybackDevice = nextSelected;
shutdown();
init();
return m_initialized;
}
void VoiceChatManager::tick(Minecraft *minecraft)
{
if (!m_initialized || m_captureDevice == nullptr) return;
@@ -216,18 +448,37 @@ void VoiceChatManager::tick(Minecraft *minecraft)
int available = (m_captureWritePos - m_captureReadPos + CAPTURE_BUFFER_SIZE) % CAPTURE_BUFFER_SIZE;
// Send in chunks of FRAMES_PER_PACKET (320 samples = 20ms at 16kHz)
// Send in short frames to keep UDP payloads under common MTU limits.
while (available >= FRAMES_PER_PACKET)
{
short frameBuffer[FRAMES_PER_PACKET];
for (int i = 0; i < FRAMES_PER_PACKET; i++)
{
frameBuffer[i] = m_captureBuffer[m_captureReadPos];
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;
m_captureReadPos = (m_captureReadPos + 1) % CAPTURE_BUFFER_SIZE;
}
// Send audio only if Push-To-Talk is active
if (m_isPushToTalkActive)
bool shouldSend = false;
if (m_voiceInputMode == VOICE_INPUT_PUSH_TO_TALK)
{
shouldSend = m_isPushToTalkActive;
}
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;
}
if (shouldSend)
{
// Mark our local player as speaking for the indicator
markSpeaking(minecraft->player->entityId);
@@ -238,9 +489,10 @@ void VoiceChatManager::tick(Minecraft *minecraft)
memcpy(audioData.data, frameBuffer, dataLen);
shared_ptr<Packet> packet = std::make_shared<VoiceChatPacket>(
minecraft->player->entityId, audioData, dataLen);
minecraft->player->entityId, m_localSequence++, audioData, dataLen);
conn->send(packet);
// Queue to slow path so it is emitted as standalone UDP transport packets.
conn->queueSend(packet);
}
available = (m_captureWritePos - m_captureReadPos + CAPTURE_BUFFER_SIZE) % CAPTURE_BUFFER_SIZE;
@@ -268,7 +520,7 @@ void VoiceChatManager::tick(Minecraft *minecraft)
tickSpeakingState();
}
void VoiceChatManager::receiveVoiceData(int playerId, double x, double y, double z, const unsigned char *data, int dataLength)
void VoiceChatManager::receiveVoiceData(int playerId, unsigned short sequence, double x, double y, double z, const unsigned char *data, int dataLength)
{
if (!m_initialized || m_playbackDevice == nullptr) return;
if (data == nullptr || dataLength <= 0) return;
@@ -284,6 +536,38 @@ void VoiceChatManager::receiveVoiceData(int playerId, double x, double y, double
stream.z = z;
stream.lastReceiveTime = System::currentTimeMillis();
// Reject very old/out-of-order packets aggressively, but tolerate wrap-around.
if (stream.hasSequence)
{
unsigned short delta = static_cast<unsigned short>(sequence - stream.lastSequence);
if (delta == 0)
{
LeaveCriticalSection(&m_playbackLock);
return; // duplicate
}
if (delta > 32768)
{
LeaveCriticalSection(&m_playbackLock);
return; // stale packet
}
// Packet loss concealment: for small gaps, insert silence frames.
const int missingPackets = static_cast<int>(delta) - 1;
if (missingPackets > 0)
{
const int concealPackets = (missingPackets > 3) ? 3 : missingPackets;
const int concealSamples = concealPackets * FRAMES_PER_PACKET;
for (int i = 0; i < concealSamples; ++i)
{
stream.buffer[stream.writePos] = 0;
stream.writePos = (stream.writePos + 1) % RemoteVoiceStream::BUFFER_SIZE;
}
}
}
stream.lastSequence = sequence;
stream.hasSequence = true;
for (int i = 0; i < sampleCount; i++)
{
stream.buffer[stream.writePos] = samples[i];
@@ -332,3 +616,17 @@ void VoiceChatManager::tickSpeakingState()
}
LeaveCriticalSection(&m_speakingLock);
}
void VoiceChatManager::setMicVolumePercent(int percent)
{
if (percent < 0) percent = 0;
if (percent > 200) percent = 200;
m_micVolumePercent = percent;
}
void VoiceChatManager::setVoiceChatVolumePercent(int percent)
{
if (percent < 0) percent = 0;
if (percent > 200) percent = 200;
m_voiceChatVolumePercent = percent;
}