|
| 1 | +// VoiceCapture.h |
| 2 | +// |
| 3 | +// WASAPI-based microphone capture for lobby voice chat. Runs its own |
| 4 | +// worker thread that pulls packets from the OS capture endpoint, |
| 5 | +// resamples/converts to 48 kHz mono PCM16 if needed, and hands |
| 6 | +// completed 20 ms frames to a caller-supplied callback. The callback |
| 7 | +// is invoked on the capture worker thread - callers must be |
| 8 | +// thread-safe or marshal work back to the main thread themselves. |
| 9 | +// |
| 10 | +// Lifecycle: |
| 11 | +// 1. Construct (cheap, does nothing). |
| 12 | +// 2. Start() - opens the default capture endpoint, starts the |
| 13 | +// worker thread, begins delivering frames. Returns false if no |
| 14 | +// microphone is available or WASAPI initialisation fails. |
| 15 | +// 3. SetTransmitting(true/false) - gates whether the worker actually |
| 16 | +// calls the frame callback. When false the mic is still open but |
| 17 | +// frames are discarded. This models push-to-talk without |
| 18 | +// constantly stopping/restarting the capture stream. |
| 19 | +// 4. Stop() - joins the worker and releases the endpoint. |
| 20 | +// 5. Destruct. |
| 21 | +// |
| 22 | +// The capture format is requested as 48 kHz mono PCM16 shared-mode. If |
| 23 | +// the endpoint rejects that, we fall back to the endpoint's native |
| 24 | +// mix format and convert on the fly using Windows' automatic format |
| 25 | +// conversion inside IAudioClient (AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
| 26 | +// plus AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY). |
| 27 | + |
| 28 | +#pragma once |
| 29 | + |
| 30 | +#ifdef ENABLE_VOICE_CHAT |
| 31 | + |
| 32 | +#include <cstdint> |
| 33 | +#include <functional> |
| 34 | +#include <atomic> |
| 35 | +#include <thread> |
| 36 | +#include <string> |
| 37 | +#include <vector> |
| 38 | + |
| 39 | +namespace Voice |
| 40 | +{ |
| 41 | + |
| 42 | +// Callback signature: receives one 20 ms frame of 48 kHz mono PCM16 |
| 43 | +// (960 int16 samples). The pointer is valid only for the duration of |
| 44 | +// the call; copy if you need to keep it. |
| 45 | +using CaptureFrameCallback = std::function<void(const int16_t* pcm, int sampleCount)>; |
| 46 | + |
| 47 | +// Description of a capture endpoint as returned by |
| 48 | +// VoiceCapture::EnumerateDevices. The id is the opaque WASAPI endpoint |
| 49 | +// id string (suitable for IMMDeviceEnumerator::GetDevice); the |
| 50 | +// friendlyName is for presenting to the user. |
| 51 | +struct CaptureDeviceInfo |
| 52 | +{ |
| 53 | + std::wstring id; |
| 54 | + std::wstring friendlyName; |
| 55 | + bool isDefaultCommunications = false; |
| 56 | + bool isDefaultConsole = false; |
| 57 | +}; |
| 58 | + |
| 59 | +class VoiceCapture |
| 60 | +{ |
| 61 | +public: |
| 62 | + VoiceCapture(); |
| 63 | + ~VoiceCapture(); |
| 64 | + |
| 65 | + VoiceCapture(const VoiceCapture&) = delete; |
| 66 | + VoiceCapture& operator=(const VoiceCapture&) = delete; |
| 67 | + |
| 68 | + // Opens the default capture endpoint and starts the worker thread. |
| 69 | + // Returns false on any failure (no mic, no permission, WASAPI |
| 70 | + // unavailable); the object is safe to destruct in that state. |
| 71 | + bool Start(CaptureFrameCallback onFrame); |
| 72 | + |
| 73 | + // Stops the worker thread and releases the WASAPI objects. Safe |
| 74 | + // to call multiple times. |
| 75 | + void Stop(); |
| 76 | + |
| 77 | + // When false, captured audio is dropped on the floor instead of |
| 78 | + // being passed to the callback. This is how push-to-talk is |
| 79 | + // implemented: the mic stream keeps running (avoids click/pop on |
| 80 | + // enable) but frames only reach the network path while the PTT |
| 81 | + // key is held. |
| 82 | + void SetTransmitting(bool transmitting); |
| 83 | + |
| 84 | + bool IsRunning() const { return m_workerRunning.load(); } |
| 85 | + |
| 86 | + // Peak level of the most recent transmitted frame, 0.0 - 1.0. |
| 87 | + // Useful for a "mic level" indicator in the UI. |
| 88 | + float GetCurrentLevel() const { return m_currentLevel.load(); } |
| 89 | + |
| 90 | + // ---------- Device selection ----------------------------------- |
| 91 | + // Enumerate all active capture endpoints. Safe to call before or |
| 92 | + // after Start() - uses its own temporary device enumerator so it |
| 93 | + // doesn't touch the running capture stream. |
| 94 | + static std::vector<CaptureDeviceInfo> EnumerateDevices(); |
| 95 | + |
| 96 | + // Select a capture endpoint by its WASAPI id string. Pass an empty |
| 97 | + // string to go back to the Windows default communications endpoint. |
| 98 | + // Only takes effect on the next Start(): call Stop() then Start() |
| 99 | + // again to apply at runtime. |
| 100 | + void SetDeviceID(const std::wstring& id) { m_requestedDeviceID = id; } |
| 101 | + const std::wstring& GetDeviceID() const { return m_requestedDeviceID; } |
| 102 | + |
| 103 | + // ---------- Mic gain ------------------------------------------- |
| 104 | + // Linear multiplier applied to captured samples before Opus encode. |
| 105 | + // 1.0 = unity (no change), 2.0 = +6 dB, 0.0 = mute. |
| 106 | + // Clamped to [0.0, 4.0] internally. Thread-safe. |
| 107 | + void SetMicGain(float gain); |
| 108 | + float GetMicGain() const { return m_micGain.load(); } |
| 109 | + |
| 110 | +private: |
| 111 | + // Runs on the worker thread. |
| 112 | + void WorkerLoop(); |
| 113 | + |
| 114 | + // Opaque forward declaration so the WASAPI headers don't leak |
| 115 | + // through to every TU that includes this file. |
| 116 | + struct Impl; |
| 117 | + Impl* m_impl; |
| 118 | + |
| 119 | + CaptureFrameCallback m_onFrame; |
| 120 | + std::atomic<bool> m_workerRunning; |
| 121 | + std::atomic<bool> m_workerShouldExit; |
| 122 | + std::atomic<bool> m_transmitting; |
| 123 | + std::atomic<float> m_currentLevel; |
| 124 | + std::atomic<float> m_micGain{1.0f}; |
| 125 | + std::wstring m_requestedDeviceID; // empty => default comms |
| 126 | + std::thread m_workerThread; |
| 127 | +}; |
| 128 | + |
| 129 | +} // namespace Voice |
| 130 | + |
| 131 | +#endif // ENABLE_VOICE_CHAT |
0 commit comments