diff --git a/Minecraft.Client/ClientConnection.cpp b/Minecraft.Client/ClientConnection.cpp index ba40b43e..916b0604 100644 --- a/Minecraft.Client/ClientConnection.cpp +++ b/Minecraft.Client/ClientConnection.cpp @@ -54,6 +54,7 @@ #include "PS3/Network/SonyVoiceChat.h" #endif #include "DLCTexturePack.h" +#include "VoiceChatManager.h" #ifdef _WINDOWS64 #include "Xbox\Network\NetworkPlayerXbox.h" @@ -4069,3 +4070,38 @@ ClientConnection::DeferredEntityLinkPacket::DeferredEntityLinkPacket(shared_ptr< m_recievedTick = GetTickCount(); m_packet = packet; } + +void ClientConnection::handleVoiceChat(VoiceChatPacket *packet) +{ + if (done) return; + if (packet == nullptr || packet->dataLength <= 0) return; + if (minecraft == nullptr || minecraft->level == nullptr) return; + + // Don't play back our own voice + for (int i = 0; i < XUSER_MAX_COUNT; i++) + { + if (minecraft->localplayers[i] != nullptr && minecraft->localplayers[i]->entityId == packet->senderPlayerId) + { + return; + } + } + + // Mark the sender as speaking in VoiceChatManager (centralized tracking) + VoiceChatManager::getInstance().markSpeaking(packet->senderPlayerId); + + // Find the sender entity position for 3D audio + shared_ptr senderEntity = minecraft->level->getEntity(packet->senderPlayerId); + double sx = 0, sy = 0, sz = 0; + if (senderEntity != nullptr) + { + sx = senderEntity->x; + sy = senderEntity->y; + sz = senderEntity->z; + } + + // Route audio to VoiceChatManager for 3D playback + VoiceChatManager::getInstance().receiveVoiceData( + packet->senderPlayerId, sx, sy, sz, + (const unsigned char *)packet->audioData.data, + packet->dataLength); +} diff --git a/Minecraft.Client/ClientConnection.h b/Minecraft.Client/ClientConnection.h index f13b93e7..1fa0de48 100644 --- a/Minecraft.Client/ClientConnection.h +++ b/Minecraft.Client/ClientConnection.h @@ -124,6 +124,7 @@ public: virtual void handlePlayerAbilities(shared_ptr playerAbilitiesPacket); virtual void handleSoundEvent(shared_ptr packet); virtual void handleCustomPayload(shared_ptr customPayloadPacket); + virtual void handleVoiceChat(VoiceChatPacket *packet); virtual Connection *getConnection(); // 4J Added diff --git a/Minecraft.Client/Common/res/1_2_2/gui/speaker.png b/Minecraft.Client/Common/res/1_2_2/gui/speaker.png new file mode 100644 index 00000000..d8767ef1 --- /dev/null +++ b/Minecraft.Client/Common/res/1_2_2/gui/speaker.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7483dd513d930b61674b56f5ae0a549fd7c895065a314e008b3a590a2c5321a3 +size 330 diff --git a/Minecraft.Client/Common/res/gui/speaker.png b/Minecraft.Client/Common/res/gui/speaker.png new file mode 100644 index 00000000..d8767ef1 --- /dev/null +++ b/Minecraft.Client/Common/res/gui/speaker.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7483dd513d930b61674b56f5ae0a549fd7c895065a314e008b3a590a2c5321a3 +size 330 diff --git a/Minecraft.Client/Minecraft.Client.vcxproj b/Minecraft.Client/Minecraft.Client.vcxproj index d97cbc38..47a764fe 100644 --- a/Minecraft.Client/Minecraft.Client.vcxproj +++ b/Minecraft.Client/Minecraft.Client.vcxproj @@ -1,4 +1,4 @@ - + @@ -21019,6 +21019,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU + @@ -38270,6 +38271,7 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU + true @@ -48738,4 +48740,4 @@ xcopy /q /y /i /s /e $(ProjectDir)Durango\CU $(LayoutDir)Image\Loose\CU - \ No newline at end of file + diff --git a/Minecraft.Client/Minecraft.cpp b/Minecraft.Client/Minecraft.cpp index aa8fa1fa..1c5e5b08 100644 --- a/Minecraft.Client/Minecraft.cpp +++ b/Minecraft.Client/Minecraft.cpp @@ -72,6 +72,7 @@ #include "Common\UI\IUIScene_CreativeMenu.h" #include "Common\UI\UIFontData.h" #include "DLCTexturePack.h" +#include "VoiceChatManager.h" #ifdef __ORBIS__ #include "Orbis\Network\PsPlusUpsellWrapper_Orbis.h" @@ -612,6 +613,7 @@ void Minecraft::destroy() // } catch (Throwable e) { // } + VoiceChatManager::getInstance().shutdown(); soundEngine->destroy(); //} finally { Display::destroy(); @@ -1939,6 +1941,13 @@ void Minecraft::run_middle() soundEngine->tick((shared_ptr *)localplayers, timer->a); PIXEndNamedEvent(); + // Voice chat tick + { + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + if (!vcm.isInitialized()) vcm.init(); + vcm.tick(this); + } + PIXBeginNamedEvent(0,"Light update"); glEnable(GL_TEXTURE_2D); @@ -2296,6 +2305,23 @@ void Minecraft::tick(bool bFirst, bool bUpdateTextures) //4J-PB - only tick this player's stats stats[iPad]->tick(iPad); + // Voice chat: lazy-init and tick + VoiceChatManager &vcm = VoiceChatManager::getInstance(); + if (!vcm.isInitialized()) + { + vcm.init(); + } + + // Push-To-Talk: Only capture if 'V' key or DPAD_UP is held + bool pttPressed = false; +#ifdef _WINDOWS64 + if (g_KBMInput.IsKeyDown('V')) pttPressed = true; +#endif + if (InputManager.ButtonDown(iPad, MINECRAFT_ACTION_DPAD_UP)) pttPressed = true; + + vcm.setPushToTalk(pttPressed); + vcm.tick(this); + // Tick the opacity timer (to display the interface at default opacity for a certain time if the user has been navigating it) app.TickOpacityTimer(iPad); diff --git a/Minecraft.Client/PlayerConnection.cpp b/Minecraft.Client/PlayerConnection.cpp index d9915cf6..e62acabb 100644 --- a/Minecraft.Client/PlayerConnection.cpp +++ b/Minecraft.Client/PlayerConnection.cpp @@ -34,6 +34,7 @@ // 4J Added #include "..\Minecraft.World\net.minecraft.world.item.crafting.h" #include "Options.h" +#include "..\Minecraft.World\VoiceChatPacket.h" Random PlayerConnection::random; @@ -1801,3 +1802,17 @@ bool PlayerConnection::isGuest() return isGuest; } } + +void PlayerConnection::handleVoiceChat(VoiceChatPacket *packet) +{ + if (done) return; + if (packet == nullptr || packet->dataLength <= 0) return; + + // Stamp the sender's entity ID so clients know who is speaking + packet->senderPlayerId = player->entityId; + + // Broadcast to all connected players (including the sender's client, which will filter itself) + auto sharedPacket = std::make_shared( + packet->senderPlayerId, packet->audioData, packet->dataLength); + server->getPlayers()->broadcastAll(sharedPacket); +} diff --git a/Minecraft.Client/PlayerConnection.h b/Minecraft.Client/PlayerConnection.h index ff6093a3..5cf89651 100644 --- a/Minecraft.Client/PlayerConnection.h +++ b/Minecraft.Client/PlayerConnection.h @@ -107,6 +107,7 @@ public: virtual void handleServerSettingsChanged(shared_ptr packet); virtual void handleKickPlayer(shared_ptr packet); virtual void handleGameCommand(shared_ptr packet); + virtual void handleVoiceChat(VoiceChatPacket *packet); INetworkPlayer *getNetworkPlayer(); bool isLocal(); diff --git a/Minecraft.Client/PlayerRenderer.cpp b/Minecraft.Client/PlayerRenderer.cpp index a9b94544..61d52d55 100644 --- a/Minecraft.Client/PlayerRenderer.cpp +++ b/Minecraft.Client/PlayerRenderer.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include "PlayerRenderer.h" +#include "VoiceChatManager.h" #include "SkullTileRenderer.h" #include "HumanoidMobRenderer.h" #include "HumanoidModel.h" @@ -470,10 +471,46 @@ void PlayerRenderer::additionalRendering(shared_ptr _mob, float a) } } -void PlayerRenderer::renderNameTags(shared_ptr player, double x, double y, double z, wstring msg, float scale, double dist) +void PlayerRenderer::renderNameTags(shared_ptr player, double x, double y, double z, const wstring &msg, float scale, double dist) { + // Voice chat speaking indicator + if (player != nullptr && VoiceChatManager::getInstance().isEntitySpeaking(player->entityId)) + { + glPushMatrix(); + // Position above the head (slightly higher than the name tag) + glTranslatef(static_cast(x), static_cast(y) + player->bbHeight + 0.75f, static_cast(z)); + + // Billboard logic (face the camera) + glRotatef(-entityRenderDispatcher->playerRotY, 0, 1, 0); + glRotatef(entityRenderDispatcher->playerRotX, 1, 0, 0); + + // Scale the icon + float sIcon = 0.02f; + glScalef(-sIcon, -sIcon, sIcon); + + glDisable(GL_LIGHTING); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glColor4f(1, 1, 1, 1); + + ResourceLocation speakerLoc(TN_GUI_SPEAKER); + bindTexture(&speakerLoc); + + Tesselator* t = Tesselator::getInstance(); + t->begin(); + // 16x16 icon coordinates + t->vertexUV(-8, -8, 0, 0, 0); + t->vertexUV(-8, 8, 0, 0, 1); + t->vertexUV( 8, 8, 0, 1, 1); + t->vertexUV( 8, -8, 0, 1, 0); + t->end(); + + glPopMatrix(); + } + #if 0 if (dist < 10 * 10) + { Scoreboard *scoreboard = player->getScoreboard(); Objective *objective = scoreboard->getDisplayObjective(Scoreboard::DISPLAY_SLOT_BELOW_NAME); diff --git a/Minecraft.Client/PlayerRenderer.h b/Minecraft.Client/PlayerRenderer.h index 494ff795..775bac5d 100644 --- a/Minecraft.Client/PlayerRenderer.h +++ b/Minecraft.Client/PlayerRenderer.h @@ -34,7 +34,7 @@ public: protected: virtual void additionalRendering(shared_ptr _mob, float a); - void renderNameTags(shared_ptr player, double x, double y, double z, wstring msg, float scale, double dist); + void renderNameTags(shared_ptr player, double x, double y, double z, const wstring &msg, float scale, double dist); virtual void scale(shared_ptr _player, float a); public: diff --git a/Minecraft.Client/Textures.cpp b/Minecraft.Client/Textures.cpp index ba7c0282..162ace1d 100644 --- a/Minecraft.Client/Textures.cpp +++ b/Minecraft.Client/Textures.cpp @@ -246,6 +246,7 @@ const wchar_t *Textures::preLoaded[TN_COUNT] = L"/AH_0008", L"/AH_0009",*/ + L"gui/speaker", L"gui/items", L"terrain", }; diff --git a/Minecraft.Client/Textures.h b/Minecraft.Client/Textures.h index 1fca5610..b6904462 100644 --- a/Minecraft.Client/Textures.h +++ b/Minecraft.Client/Textures.h @@ -228,6 +228,7 @@ typedef enum _TEXTURE_NAME TN_AH_0008, TN_AH_0009,*/ + TN_GUI_SPEAKER, TN_GUI_ITEMS, TN_TERRAIN, diff --git a/Minecraft.Client/VoiceChatManager.cpp b/Minecraft.Client/VoiceChatManager.cpp new file mode 100644 index 00000000..ac0d2c58 --- /dev/null +++ b/Minecraft.Client/VoiceChatManager.cpp @@ -0,0 +1,334 @@ +#include "stdafx.h" +#include "VoiceChatManager.h" +#include "Minecraft.h" +#include "MultiPlayerLocalPlayer.h" +#include "ClientConnection.h" +#include "..\Minecraft.World\net.minecraft.network.packet.h" +#include "..\Minecraft.World\net.minecraft.world.entity.player.h" +#include "..\Minecraft.World\Connection.h" +#include "..\Minecraft.World\System.h" + +// Include miniaudio +#include "Common\Audio\miniaudio.h" + +VoiceChatManager &VoiceChatManager::getInstance() +{ + static VoiceChatManager instance; + return instance; +} + +VoiceChatManager::VoiceChatManager() + : m_initialized(false) + , m_captureDevice(nullptr) + , m_playbackDevice(nullptr) + , m_captureWritePos(0) + , m_captureReadPos(0) + , m_listenerX(0), m_listenerY(0), m_listenerZ(0) + , m_isPushToTalkActive(false) +{ + memset(m_captureBuffer, 0, sizeof(m_captureBuffer)); + InitializeCriticalSection(&m_captureLock); + InitializeCriticalSection(&m_playbackLock); + InitializeCriticalSection(&m_speakingLock); +} + +VoiceChatManager::~VoiceChatManager() +{ + shutdown(); + DeleteCriticalSection(&m_captureLock); + DeleteCriticalSection(&m_playbackLock); + DeleteCriticalSection(&m_speakingLock); +} + +void VoiceChatManager::onCaptureAudio(ma_device *pDevice, void *pOutput, const void *pInput, unsigned int frameCount) +{ + VoiceChatManager *mgr = (VoiceChatManager *)pDevice->pUserData; + const short *input = (const short *)pInput; + + if (input == nullptr || mgr == nullptr) return; + + EnterCriticalSection(&mgr->m_captureLock); + for (unsigned int i = 0; i < frameCount; i++) + { + mgr->m_captureBuffer[mgr->m_captureWritePos] = input[i]; + mgr->m_captureWritePos = (mgr->m_captureWritePos + 1) % CAPTURE_BUFFER_SIZE; + } + LeaveCriticalSection(&mgr->m_captureLock); +} + +void VoiceChatManager::onPlaybackAudio(ma_device *pDevice, void *pOutput, const void *pInput, unsigned int frameCount) +{ + VoiceChatManager *mgr = (VoiceChatManager *)pDevice->pUserData; + short *output = (short *)pOutput; + + if (output == nullptr || mgr == nullptr) return; + + memset(output, 0, frameCount * sizeof(short)); + + EnterCriticalSection(&mgr->m_playbackLock); + for (auto &pair : mgr->m_remoteStreams) + { + 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) + { + 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; + + // Mix this stream's audio into the output + for (unsigned int i = 0; i < frameCount; i++) + { + if (stream.readPos != stream.writePos) + { + int sample = (int)(stream.buffer[stream.readPos] * volume); + int mixed = output[i] + sample; + + // Clamp to prevent clipping + if (mixed > 32767) mixed = 32767; + if (mixed < -32768) mixed = -32768; + + output[i] = (short)mixed; + stream.readPos = (stream.readPos + 1) % RemoteVoiceStream::BUFFER_SIZE; + } + } + } + LeaveCriticalSection(&mgr->m_playbackLock); +} + +void VoiceChatManager::init() +{ + if (m_initialized) return; + + // 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.sampleRate = SAMPLE_RATE; + captureConfig.dataCallback = onCaptureAudio; + captureConfig.pUserData = this; + + if (ma_device_init(nullptr, &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) + { + 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) + m_playbackDevice = new ma_device(); + ma_device_config playbackConfig = ma_device_config_init(ma_device_type_playback); + playbackConfig.playback.format = ma_format_s16; + playbackConfig.playback.channels = CHANNELS; + playbackConfig.sampleRate = SAMPLE_RATE; + playbackConfig.dataCallback = onPlaybackAudio; + playbackConfig.pUserData = this; + + if (ma_device_init(nullptr, &playbackConfig, m_playbackDevice) != MA_SUCCESS) + { + app.DebugPrintf("VoiceChat: Failed to initialize playback device\n"); + delete m_playbackDevice; + m_playbackDevice = nullptr; + // Still keep capture running + } + else + { + if (ma_device_start(m_playbackDevice) != MA_SUCCESS) + { + app.DebugPrintf("VoiceChat: Failed to start playback device\n"); + ma_device_uninit(m_playbackDevice); + delete m_playbackDevice; + m_playbackDevice = nullptr; + } + } + + m_initialized = true; + app.DebugPrintf("VoiceChat: Initialized (capture=%s, playback=%s)\n", + m_captureDevice ? "OK" : "FAIL", m_playbackDevice ? "OK" : "FAIL"); +} + +void VoiceChatManager::shutdown() +{ + if (!m_initialized) return; + + if (m_captureDevice) + { + ma_device_uninit(m_captureDevice); + delete m_captureDevice; + m_captureDevice = nullptr; + } + + if (m_playbackDevice) + { + ma_device_uninit(m_playbackDevice); + delete m_playbackDevice; + m_playbackDevice = nullptr; + } + + EnterCriticalSection(&m_playbackLock); + m_remoteStreams.clear(); + LeaveCriticalSection(&m_playbackLock); + + m_initialized = false; + app.DebugPrintf("VoiceChat: Shutdown\n"); +} + +void VoiceChatManager::tick(Minecraft *minecraft) +{ + if (!m_initialized || m_captureDevice == nullptr) return; + if (minecraft == nullptr || minecraft->player == nullptr) return; + + int iPad = minecraft->player->GetXboxPad(); + ClientConnection *conn = minecraft->getConnection(iPad); + if (conn == nullptr) return; + + // Update listener position + setListenerPosition(minecraft->player->x, minecraft->player->y, minecraft->player->z); + + // Read captured audio and send as packets + EnterCriticalSection(&m_captureLock); + + int available = (m_captureWritePos - m_captureReadPos + CAPTURE_BUFFER_SIZE) % CAPTURE_BUFFER_SIZE; + + // Send in chunks of FRAMES_PER_PACKET (320 samples = 20ms at 16kHz) + 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]; + m_captureReadPos = (m_captureReadPos + 1) % CAPTURE_BUFFER_SIZE; + } + + // Send audio only if Push-To-Talk is active + if (m_isPushToTalkActive) + { + // Mark our local player as speaking for the indicator + markSpeaking(minecraft->player->entityId); + + // Send raw PCM as a voice chat packet + short dataLen = (short)(FRAMES_PER_PACKET * sizeof(short)); + byteArray audioData(dataLen); + memcpy(audioData.data, frameBuffer, dataLen); + + shared_ptr packet = std::make_shared( + minecraft->player->entityId, audioData, dataLen); + + conn->send(packet); + } + + available = (m_captureWritePos - m_captureReadPos + CAPTURE_BUFFER_SIZE) % CAPTURE_BUFFER_SIZE; + } + + LeaveCriticalSection(&m_captureLock); + + // Cleanup stale remote streams (no data for >2 seconds) + int64_t now = System::currentTimeMillis(); + EnterCriticalSection(&m_playbackLock); + for (auto it = m_remoteStreams.begin(); it != m_remoteStreams.end(); ) + { + if (now - it->second.lastReceiveTime > 2000) + { + it = m_remoteStreams.erase(it); + } + else + { + ++it; + } + } + LeaveCriticalSection(&m_playbackLock); + + // Tick speaking indicators + tickSpeakingState(); +} + +void VoiceChatManager::receiveVoiceData(int playerId, 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; + + int sampleCount = dataLength / sizeof(short); + const short *samples = (const short *)data; + + EnterCriticalSection(&m_playbackLock); + + RemoteVoiceStream &stream = m_remoteStreams[playerId]; + stream.x = x; + stream.y = y; + stream.z = z; + stream.lastReceiveTime = System::currentTimeMillis(); + + for (int i = 0; i < sampleCount; i++) + { + stream.buffer[stream.writePos] = samples[i]; + stream.writePos = (stream.writePos + 1) % RemoteVoiceStream::BUFFER_SIZE; + } + + LeaveCriticalSection(&m_playbackLock); +} + +void VoiceChatManager::setListenerPosition(double x, double y, double z) +{ + m_listenerX = x; + m_listenerY = y; + m_listenerZ = z; +} + +void VoiceChatManager::markSpeaking(int entityId) +{ + EnterCriticalSection(&m_speakingLock); + m_speakingEntities[entityId] = 30; // ~1.5 seconds at 20 TPS + LeaveCriticalSection(&m_speakingLock); +} + +bool VoiceChatManager::isEntitySpeaking(int entityId) +{ + EnterCriticalSection(&m_speakingLock); + bool speaking = m_speakingEntities.count(entityId) > 0 && m_speakingEntities[entityId] > 0; + LeaveCriticalSection(&m_speakingLock); + return speaking; +} + +void VoiceChatManager::tickSpeakingState() +{ + EnterCriticalSection(&m_speakingLock); + for (auto it = m_speakingEntities.begin(); it != m_speakingEntities.end(); ) + { + it->second--; + if (it->second <= 0) + { + it = m_speakingEntities.erase(it); + } + else + { + ++it; + } + } + LeaveCriticalSection(&m_speakingLock); +} diff --git a/Minecraft.Client/VoiceChatManager.h b/Minecraft.Client/VoiceChatManager.h new file mode 100644 index 00000000..bec6b8a1 --- /dev/null +++ b/Minecraft.Client/VoiceChatManager.h @@ -0,0 +1,98 @@ +#pragma once +using namespace std; + +class Connection; +class Minecraft; + +// Forward-declare miniaudio types to avoid including the massive header here +struct ma_device; + +class VoiceChatManager +{ +public: + static VoiceChatManager &getInstance(); + + void init(); + void shutdown(); + + // Called each game tick to send buffered captured audio + void tick(Minecraft *minecraft); + + // Called when a voice packet arrives from the network + void receiveVoiceData(int playerId, double x, double y, double z, const unsigned char *data, int dataLength); + + // Update the local listener's position for 3D attenuation + void setListenerPosition(double x, double y, double z); + + // Push-to-talk control + void setPushToTalk(bool active) { m_isPushToTalkActive = active; } + bool isPushToTalkActive() const { return m_isPushToTalkActive; } + + bool isInitialized() const { return m_initialized; } + + // Speaking state tracking for rendering indicators + void markSpeaking(int entityId); + bool isEntitySpeaking(int entityId); + void tickSpeakingState(); + +private: + VoiceChatManager(); + ~VoiceChatManager(); + VoiceChatManager(const VoiceChatManager &) = delete; + VoiceChatManager &operator=(const VoiceChatManager &) = delete; + + // Miniaudio capture callback (static, called from audio thread) + static void onCaptureAudio(ma_device *pDevice, void *pOutput, const void *pInput, unsigned int frameCount); + + // Miniaudio playback callback (static, called from audio thread) + static void onPlaybackAudio(ma_device *pDevice, void *pOutput, const void *pInput, unsigned int frameCount); + + bool m_initialized; + + ma_device *m_captureDevice; + ma_device *m_playbackDevice; + bool m_isPushToTalkActive; + + // Capture buffer (ring buffer for PCM data captured from mic) + static const int CAPTURE_BUFFER_SIZE = 96000; // 2 seconds at 48kHz mono + short m_captureBuffer[CAPTURE_BUFFER_SIZE]; + int m_captureWritePos; + int m_captureReadPos; + CRITICAL_SECTION m_captureLock; + + // Voice activity detection threshold + static const int VOICE_THRESHOLD = 500; + + // Per-player playback buffers + struct RemoteVoiceStream + { + static const int BUFFER_SIZE = 96000; // 2 seconds at 48kHz mono + short buffer[BUFFER_SIZE]; + int writePos; + int readPos; + double x, y, z; // Last known position of this speaker + int64_t lastReceiveTime; // For cleanup of stale streams + + RemoteVoiceStream() : writePos(0), readPos(0), x(0), y(0), z(0), lastReceiveTime(0) + { + memset(buffer, 0, sizeof(buffer)); + } + }; + + unordered_map m_remoteStreams; + CRITICAL_SECTION m_playbackLock; + + // Listener position + double m_listenerX, m_listenerY, m_listenerZ; + + // Constants + static const int SAMPLE_RATE = 48000; + static const int CHANNELS = 1; + static const int FRAME_SIZE_MS = 20; + static const int FRAMES_PER_PACKET = SAMPLE_RATE * FRAME_SIZE_MS / 1000; // 320 samples + static const int MAX_VOICE_DISTANCE = 32; // blocks + + // Speaking indicator state: entityId -> remaining ticks + unordered_map m_speakingEntities; + CRITICAL_SECTION m_speakingLock; +}; diff --git a/Minecraft.Client/Windows64Media/Media/gui/speaker.png b/Minecraft.Client/Windows64Media/Media/gui/speaker.png new file mode 100644 index 00000000..d8767ef1 --- /dev/null +++ b/Minecraft.Client/Windows64Media/Media/gui/speaker.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7483dd513d930b61674b56f5ae0a549fd7c895065a314e008b3a590a2c5321a3 +size 330 diff --git a/Minecraft.World/Minecraft.World.vcxproj b/Minecraft.World/Minecraft.World.vcxproj index b6af9153..ddf7aba9 100644 --- a/Minecraft.World/Minecraft.World.vcxproj +++ b/Minecraft.World/Minecraft.World.vcxproj @@ -1,4 +1,4 @@ - + @@ -2942,6 +2942,7 @@ + @@ -3593,6 +3594,7 @@ + diff --git a/Minecraft.World/Packet.cpp b/Minecraft.World/Packet.cpp index 05bf932d..9851feb5 100644 --- a/Minecraft.World/Packet.cpp +++ b/Minecraft.World/Packet.cpp @@ -145,6 +145,7 @@ void Packet::staticCtor() map(209, true, false, true, false, typeid(SetPlayerTeamPacket), SetPlayerTeamPacket::create); map(250, true, true, true, false, typeid(CustomPayloadPacket), CustomPayloadPacket::create); + map(251, true, true, true, false, typeid(VoiceChatPacket), VoiceChatPacket::create); // 4J Stu - These added 1.3.2, but don't think we need them //map(252, true, true, SharedKeyPacket.class); //map(253, true, false, ServerAuthDataPacket.class); diff --git a/Minecraft.World/PacketListener.cpp b/Minecraft.World/PacketListener.cpp index 09fbb052..bf914560 100644 --- a/Minecraft.World/PacketListener.cpp +++ b/Minecraft.World/PacketListener.cpp @@ -491,3 +491,7 @@ void PacketListener::handleGameCommand(shared_ptr packet) { onUnhandledPacket( (shared_ptr ) packet); } + +void PacketListener::handleVoiceChat(VoiceChatPacket *packet) +{ +} diff --git a/Minecraft.World/PacketListener.h b/Minecraft.World/PacketListener.h index f63b6bc7..40307bbd 100644 --- a/Minecraft.World/PacketListener.h +++ b/Minecraft.World/PacketListener.h @@ -111,6 +111,7 @@ class KickPlayerPacket; class AdditionalModelPartsPacket; class XZPacket; class GameCommandPacket; +class VoiceChatPacket; class PacketListener { @@ -227,4 +228,5 @@ public: virtual void handleKickPlayer(shared_ptr packet); virtual void handleXZ(shared_ptr packet); virtual void handleGameCommand(shared_ptr packet); + virtual void handleVoiceChat(VoiceChatPacket *packet); }; diff --git a/Minecraft.World/Player.cpp b/Minecraft.World/Player.cpp index 00c7148e..a0626980 100644 --- a/Minecraft.World/Player.cpp +++ b/Minecraft.World/Player.cpp @@ -84,6 +84,8 @@ void Player::_init() jumpTriggerTime = 0; takeXpDelay = 0; + isSpeaking = false; + speakingTicks = 0; experienceLevel = totalExperience = 0; experienceProgress = 0.0f; @@ -328,6 +330,16 @@ void Player::tick() clearFire(); } + // Voice chat speaking indicator timeout + if (speakingTicks > 0) + { + speakingTicks--; + if (speakingTicks <= 0) + { + isSpeaking = false; + } + } + xCloakO = xCloak; yCloakO = yCloak; zCloakO = zCloak; diff --git a/Minecraft.World/Player.h b/Minecraft.World/Player.h index 2e223a1e..dc2cd626 100644 --- a/Minecraft.World/Player.h +++ b/Minecraft.World/Player.h @@ -78,6 +78,10 @@ public: wstring name; int takeXpDelay; + // Voice chat speaking indicator + bool isSpeaking; + int speakingTicks; + // 4J-PB - track custom skin wstring customTextureUrl; wstring customTextureUrl2; diff --git a/Minecraft.World/VoiceChatPacket.cpp b/Minecraft.World/VoiceChatPacket.cpp new file mode 100644 index 00000000..2599e329 --- /dev/null +++ b/Minecraft.World/VoiceChatPacket.cpp @@ -0,0 +1,54 @@ +#include "stdafx.h" +#include "InputOutputStream.h" +#include "PacketListener.h" +#include "VoiceChatPacket.h" + +VoiceChatPacket::VoiceChatPacket() + : senderPlayerId(-1), dataLength(0), audioData() +{ +} + +VoiceChatPacket::VoiceChatPacket(int senderPlayerId, byteArray audioData, short dataLength) + : senderPlayerId(senderPlayerId), audioData(audioData), dataLength(dataLength) +{ +} + +VoiceChatPacket::~VoiceChatPacket() +{ +} + +void VoiceChatPacket::read(DataInputStream *dis) +{ + senderPlayerId = dis->readInt(); + dataLength = dis->readShort(); + if (dataLength > 0 && dataLength <= 8192) + { + audioData = byteArray(dataLength); + dis->readFully(audioData); + } + else + { + audioData = byteArray(); + dataLength = 0; + } +} + +void VoiceChatPacket::write(DataOutputStream *dos) +{ + dos->writeInt(senderPlayerId); + dos->writeShort(dataLength); + if (dataLength > 0) + { + dos->write(audioData); + } +} + +void VoiceChatPacket::handle(PacketListener *listener) +{ + listener->handleVoiceChat(this); +} + +int VoiceChatPacket::getEstimatedSize() +{ + return sizeof(int) + sizeof(short) + dataLength; +} diff --git a/Minecraft.World/VoiceChatPacket.h b/Minecraft.World/VoiceChatPacket.h new file mode 100644 index 00000000..7a137ba2 --- /dev/null +++ b/Minecraft.World/VoiceChatPacket.h @@ -0,0 +1,24 @@ +#pragma once +using namespace std; + +#include "Packet.h" + +class VoiceChatPacket : public Packet, public enable_shared_from_this +{ +public: + int senderPlayerId; + short dataLength; + byteArray audioData; + + VoiceChatPacket(); + VoiceChatPacket(int senderPlayerId, byteArray audioData, short dataLength); + ~VoiceChatPacket(); + + virtual void read(DataInputStream *dis); + virtual void write(DataOutputStream *dos); + virtual void handle(PacketListener *listener); + virtual int getEstimatedSize(); + + static shared_ptr create() { return std::make_shared(); } + virtual int getId() { return 251; } +}; diff --git a/Minecraft.World/net.minecraft.network.packet.h b/Minecraft.World/net.minecraft.network.packet.h index c1d52d14..05758f4c 100644 --- a/Minecraft.World/net.minecraft.network.packet.h +++ b/Minecraft.World/net.minecraft.network.packet.h @@ -108,4 +108,5 @@ #include "KickPlayerPacket.h" #include "XZPacket.h" #include "GameCommandPacket.h" +#include "VoiceChatPacket.h" diff --git a/cmake/ClientSources.cmake b/cmake/ClientSources.cmake index 6467a243..db84be5c 100644 --- a/cmake/ClientSources.cmake +++ b/cmake/ClientSources.cmake @@ -471,6 +471,7 @@ set(MINECRAFT_CLIENT_SOURCES "VillagerModel.cpp" "VillagerRenderer.cpp" "VillagerZombieModel.cpp" + "VoiceChatManager.cpp" "WaterDropParticle.cpp" "Windows64/Iggy/gdraw/gdraw_d3d11.cpp" "Windows64/KeyboardMouseInput.cpp" diff --git a/cmake/WorldSources.cmake b/cmake/WorldSources.cmake index cd6c1b81..3e30d77a 100644 --- a/cmake/WorldSources.cmake +++ b/cmake/WorldSources.cmake @@ -768,6 +768,7 @@ set(MINECRAFT_WORLD_SOURCES "VineTile.cpp" "VinesFeature.cpp" "VoronoiZoom.cpp" + "VoiceChatPacket.cpp" "WallTile.cpp" "WaterAnimal.cpp" "WaterColor.cpp"