Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,540 changes: 1,289 additions & 1,251 deletions GeneralsMD/Code/GameEngine/CMakeLists.txt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#pragma once
#include "libcurl/curl.h"
#include <string>
#include <vector>
#include <cstdint>

enum EHTTPVersion
{
Expand Down Expand Up @@ -61,6 +64,69 @@ class GenOnlineSettings

bool Debug_VerboseLogging() const { return m_bVerbose; }

// -------- Lobby voice chat settings --------
bool Voice_GetEnabled() const { return m_Voice_Enabled; }
const std::wstring& Voice_GetCaptureDeviceID() const { return m_Voice_CaptureDeviceID; }
float Voice_GetMicGain() const { return m_Voice_MicGain; }
float Voice_GetGlobalVolume() const { return m_Voice_GlobalVolume; }

// -------- Persistent per-client voice ignore list --------
// Client-local only. Never transmitted, never uploaded. When the local
// user mutes a peer via /voice mute, that peer's NGMP userID gets
// appended here and the list survives game restarts, so a troll that
// constantly leaves and re-joins the same lobby cannot bypass the mute
// just by reconnecting.
//
// Muting affects ONLY the muter's own playback: VoicePlayback drops the
// decoded audio locally. Nobody else's client knows or cares.
const std::vector<int64_t>& Voice_GetMutedPeers() const { return m_Voice_MutedPeers; }

void Save_Voice_Enabled(bool enabled)
{
m_Voice_Enabled = enabled;
Save();
}
void Save_Voice_CaptureDeviceID(const std::wstring& deviceID)
{
m_Voice_CaptureDeviceID = deviceID;
Save();
}
void Save_Voice_MicGain(float gain)
{
if (gain < 0.0f) gain = 0.0f;
if (gain > 4.0f) gain = 4.0f;
m_Voice_MicGain = gain;
Save();
}
void Save_Voice_GlobalVolume(float volume)
{
if (volume < 0.0f) volume = 0.0f;
if (volume > 2.0f) volume = 2.0f;
m_Voice_GlobalVolume = volume;
Save();
}

// Replace the full persistent mute list. Caller is expected to dedupe
// and drop invalid IDs (<=0) beforehand; we still re-validate here so
// a corrupt caller cannot poison the file.
void Save_Voice_MutedPeers(const std::vector<int64_t>& mutedPeers)
{
m_Voice_MutedPeers.clear();
m_Voice_MutedPeers.reserve(mutedPeers.size());
for (int64_t id : mutedPeers)
{
if (id <= 0) continue;
// dedupe - small N, linear scan is cheaper than a set
bool dup = false;
for (int64_t existing : m_Voice_MutedPeers)
{
if (existing == id) { dup = true; break; }
}
if (!dup) m_Voice_MutedPeers.push_back(id);
}
Save();
}

int GetChatLifeSeconds() const { return std::max<int>(m_Chat_LifeSeconds, 10); }

void Initialize()
Expand Down Expand Up @@ -138,4 +204,17 @@ class GenOnlineSettings

EHTTPVersion m_Network_HTTPVersion = EHTTPVersion::HTTP_VERSION_AUTO;
bool m_Network_UseAlternativeEndpoint = false;

// -------- Lobby voice chat settings --------
// Empty string = use the system default communications device.
bool m_Voice_Enabled = true;
std::wstring m_Voice_CaptureDeviceID;
float m_Voice_MicGain = 1.0f;
float m_Voice_GlobalVolume = 1.0f;

// Persistent per-client ignore list. See comment on Voice_GetMutedPeers.
// Stored in the settings JSON as an array of DECIMAL STRINGS (not raw
// numbers) because NGMP user IDs can exceed the ~2^53 safe integer
// precision of nlohmann::json's default number handling.
std::vector<int64_t> m_Voice_MutedPeers;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// NGMPVoiceBridge.h
//
// Glue between the VoiceManager (platform-agnostic audio + codec) and
// the NGMP peer mesh. Lives here so Voice/ stays free of NGMP/Steam
// includes and NGMP/ stays free of WASAPI/Opus includes.
//
// The bridge owns:
// - the global TheVoiceManager lifetime (create in Init, destroy in Shutdown)
// - the PacketSink that serialises a voice frame onto the wire with the
// VOICE_MAGIC_NUMBER prefix and broadcasts it to every peer in the mesh
// - the reverse path: given a SteamNetworkingMessage payload whose
// TransportMessageHeader magic == VOICE_MAGIC_NUMBER, strip the header
// and hand the remaining bytes to TheVoiceManager->OnVoicePacket
// - lobby -> in-game state transitions driven by the lobby lifecycle
// - the per-frame PTT poll (Left Alt via GetAsyncKeyState)
//
// Called from:
// - GameEngine::init -> NGMPVoice_Init
// - GameEngine::~GameEngine -> NGMPVoice_Shutdown
// - GameEngine::update -> NGMPVoice_Update
// - OnlineServices_LobbyInterface (Join/Create success) -> NGMPVoice_OnEnteredLobby
// - OnlineServices_LobbyInterface::LeaveCurrentLobby -> NGMPVoice_OnLeftLobby
// - NextGenTransport::doRecv (on magic == VOICE_MAGIC_NUMBER) ->
// NGMPVoice_DispatchIncoming

#pragma once

#include <cstdint>

class NGMPVoiceBridge
{
public:
// Called once, very early in GameEngine::init. Safe to call without
// ENABLE_VOICE_CHAT (becomes a no-op).
static void Init();

// Called once, in GameEngine::~GameEngine. No-op if Init wasn't called.
static void Shutdown();

// Called every frame from GameEngine::update. Polls the PTT key and
// performs the lobby -> in-game mode transition.
static void Update();

// Called by the NGMP lobby interface after it successfully joins or
// creates a lobby (i.e. after m_pLobbyMesh has been constructed). The
// bridge then asks the VoiceManager to open the mic/speakers and
// installs the broadcast sink.
static void OnEnteredLobby(int64_t myUserID);

// Called when leaving the current lobby, before the mesh is deleted.
// Closes audio devices and clears the sink pointer.
static void OnLeftLobby();

// Dispatch a raw incoming network message that has already been
// identified as a voice packet by its header magic. `payload` must
// point to the bytes that follow the 6-byte TransportMessageHeader,
// and `payloadLen` must be the number of such bytes.
static void DispatchIncoming(int64_t senderUserID,
const unsigned char* payload,
int payloadLen);

// True if the bridge currently has an active voice session (lobby
// or in-game). Used to gate UI widgets.
static bool IsActive();
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// VoiceCapture.h
//
// WASAPI-based microphone capture for lobby voice chat. Runs its own
// worker thread that pulls packets from the OS capture endpoint,
// resamples/converts to 48 kHz mono PCM16 if needed, and hands
// completed 20 ms frames to a caller-supplied callback. The callback
// is invoked on the capture worker thread - callers must be
// thread-safe or marshal work back to the main thread themselves.
//
// Lifecycle:
// 1. Construct (cheap, does nothing).
// 2. Start() - opens the default capture endpoint, starts the
// worker thread, begins delivering frames. Returns false if no
// microphone is available or WASAPI initialisation fails.
// 3. SetTransmitting(true/false) - gates whether the worker actually
// calls the frame callback. When false the mic is still open but
// frames are discarded. This models push-to-talk without
// constantly stopping/restarting the capture stream.
// 4. Stop() - joins the worker and releases the endpoint.
// 5. Destruct.
//
// The capture format is requested as 48 kHz mono PCM16 shared-mode. If
// the endpoint rejects that, we fall back to the endpoint's native
// mix format and convert on the fly using Windows' automatic format
// conversion inside IAudioClient (AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM
// plus AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY).

#pragma once

#ifdef ENABLE_VOICE_CHAT

#include <cstdint>
#include <functional>
#include <atomic>
#include <thread>
#include <string>
#include <vector>

namespace Voice
{

// Callback signature: receives one 20 ms frame of 48 kHz mono PCM16
// (960 int16 samples). The pointer is valid only for the duration of
// the call; copy if you need to keep it.
using CaptureFrameCallback = std::function<void(const int16_t* pcm, int sampleCount)>;

// Description of a capture endpoint as returned by
// VoiceCapture::EnumerateDevices. The id is the opaque WASAPI endpoint
// id string (suitable for IMMDeviceEnumerator::GetDevice); the
// friendlyName is for presenting to the user.
struct CaptureDeviceInfo
{
std::wstring id;
std::wstring friendlyName;
bool isDefaultCommunications = false;
bool isDefaultConsole = false;
};

class VoiceCapture
{
public:
VoiceCapture();
~VoiceCapture();

VoiceCapture(const VoiceCapture&) = delete;
VoiceCapture& operator=(const VoiceCapture&) = delete;

// Opens the default capture endpoint and starts the worker thread.
// Returns false on any failure (no mic, no permission, WASAPI
// unavailable); the object is safe to destruct in that state.
bool Start(CaptureFrameCallback onFrame);

// Stops the worker thread and releases the WASAPI objects. Safe
// to call multiple times.
void Stop();

// When false, captured audio is dropped on the floor instead of
// being passed to the callback. This is how push-to-talk is
// implemented: the mic stream keeps running (avoids click/pop on
// enable) but frames only reach the network path while the PTT
// key is held.
void SetTransmitting(bool transmitting);

bool IsRunning() const { return m_workerRunning.load(); }

// Peak level of the most recent transmitted frame, 0.0 - 1.0.
// Useful for a "mic level" indicator in the UI.
float GetCurrentLevel() const { return m_currentLevel.load(); }

// ---------- Device selection -----------------------------------
// Enumerate all active capture endpoints. Safe to call before or
// after Start() - uses its own temporary device enumerator so it
// doesn't touch the running capture stream.
static std::vector<CaptureDeviceInfo> EnumerateDevices();

// Select a capture endpoint by its WASAPI id string. Pass an empty
// string to go back to the Windows default communications endpoint.
// Only takes effect on the next Start(): call Stop() then Start()
// again to apply at runtime.
void SetDeviceID(const std::wstring& id) { m_requestedDeviceID = id; }
const std::wstring& GetDeviceID() const { return m_requestedDeviceID; }

// ---------- Mic gain -------------------------------------------
// Linear multiplier applied to captured samples before Opus encode.
// 1.0 = unity (no change), 2.0 = +6 dB, 0.0 = mute.
// Clamped to [0.0, 4.0] internally. Thread-safe.
void SetMicGain(float gain);
float GetMicGain() const { return m_micGain.load(); }

private:
// Runs on the worker thread.
void WorkerLoop();

// Opaque forward declaration so the WASAPI headers don't leak
// through to every TU that includes this file.
struct Impl;
Impl* m_impl;

CaptureFrameCallback m_onFrame;
std::atomic<bool> m_workerRunning;
std::atomic<bool> m_workerShouldExit;
std::atomic<bool> m_transmitting;
std::atomic<float> m_currentLevel;
std::atomic<float> m_micGain{1.0f};
std::wstring m_requestedDeviceID; // empty => default comms
std::thread m_workerThread;
};

} // namespace Voice

#endif // ENABLE_VOICE_CHAT
Loading
Loading