Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
VoiceChatAudioRuntime.cpp
Go to the documentation of this file.
2
4
5#include <SDL3/SDL.h>
6
7#include <algorithm>
8#include <cmath>
9#include <limits>
10
11namespace monopoly::voicechat
12{
13 namespace
14 {
15 constexpr std::uint16_t PcmFormatTag = 0x0001u;
16 constexpr int VoiceRate = 11025;
17 constexpr std::size_t CaptureReadLimit = 50000u;
18 constexpr std::size_t PostSilenceStorageLimit = 11025u - 8u;
19 constexpr std::size_t SilenceSubsampleStep = 16u;
20
21 [[nodiscard]] std::string sdlFailure(const char* operation)
22 {
23 return std::string(operation) + ": " + SDL_GetError();
24 }
25
26 [[nodiscard]] SDL_AudioFormat pcmFormat(std::uint16_t bits) noexcept
27 {
28 return bits == 16u ? SDL_AUDIO_S16LE : SDL_AUDIO_U8;
29 }
30 }
31
33 {
36 std::uint32_t maxFeedingSize{};
37 std::unique_ptr<gsm610::Decoder> gsmDecoder;
38
40 {
41 if (stream != nullptr)
43 }
44 };
45
47 : audio_(audio)
48 {
49 }
50
56
57 std::expected<void, std::string> AudioRuntime::startCapture(
58 Settings settings)
59 {
61 const auto ready = audio_.ensureReady();
62 if (!ready)
63 return std::unexpected(ready.error());
64
65 settings.silenceThreshold = std::clamp(
66 settings.silenceThreshold, 0.0F, 1.0F);
67 settings.volume = std::min(settings.volume, 100u);
68 settings.dimensions = std::min(settings.dimensions, 3u);
69 settings_ = settings;
70
71 if (settings_.codec == Codec::Gsm610)
72 {
73 captureEncoder_ = std::make_unique<gsm610::Encoder>();
74 if (!captureEncoder_->available())
75 {
76 captureEncoder_.reset();
77 return std::unexpected("libgsm WAV49 encoder is unavailable");
78 }
79 }
80 else
81 captureEncoder_.reset();
82
83 // SDL3 converts the device-native capture stream to the app-side
84 // format requested here, replacing the old DirectSound conversion.
85 SDL_AudioSpec spec{};
86 spec.format = SDL_AUDIO_U8;
87 spec.channels = 1;
88 spec.freq = VoiceRate;
89 captureStream_ = SDL_OpenAudioDeviceStream(
90 SDL_AUDIO_DEVICE_DEFAULT_RECORDING, &spec, nullptr, nullptr);
91 if (captureStream_ == nullptr)
92 {
93 captureEncoder_.reset();
94 return std::unexpected(
95 sdlFailure("SDL_OpenAudioDeviceStream(recording)"));
96 }
97
98 if (!SDL_ResumeAudioStreamDevice(captureStream_))
99 {
100 const auto error =
101 sdlFailure("SDL_ResumeAudioStreamDevice(recording)");
102 SDL_DestroyAudioStream(captureStream_);
103 captureStream_ = nullptr;
104 captureEncoder_.reset();
105 return std::unexpected(error);
106 }
107
108 postSilenceState_ = PostSilenceState::Silence;
109 firstNoiseTick_ = 0;
110 lastNoiseTick_ = 0;
111 postSilenceStorage_.clear();
112 const auto started = sendStartPacket();
113 if (!started)
114 {
115 SDL_DestroyAudioStream(captureStream_);
116 captureStream_ = nullptr;
117 captureEncoder_.reset();
118 return started;
119 }
120 return {};
121 }
122
124 {
125 if (captureStream_ != nullptr)
126 {
127 std::vector<std::uint8_t> packetBytes;
130 SDL_DestroyAudioStream(captureStream_);
131 captureStream_ = nullptr;
132 }
133 captureEncoder_.reset();
134 postSilenceStorage_.clear();
135 postSilenceState_ = PostSilenceState::Silence;
136 firstNoiseTick_ = 0;
137 lastNoiseTick_ = 0;
138 }
139
141 {
142 return captureStream_ != nullptr;
143 }
144
145 std::expected<void, std::string> AudioRuntime::sendStartPacket()
146 {
148 std::span<const std::uint8_t> extra{};
149 std::array<std::uint8_t, 2> gsmExtra{};
150
151 if (settings_.codec == Codec::Gsm610)
152 {
153 const auto wire = gsm610::retailWireFormat();
154 format = wire.format;
155 gsmExtra = wire.extra;
156 extra = gsmExtra;
157 }
158 else
159 {
160 format.formatTag = PcmFormatTag;
161 format.channels = 1;
162 format.samplesPerSecond = VoiceRate;
163 format.averageBytesPerSecond = VoiceRate;
164 format.blockAlign = 1;
165 format.bitsPerSample = 8;
166 }
167
168 std::vector<std::uint8_t> packetBytes;
169 if (!packet::makeStartPacket(format, extra, settings_.dimensions,
170 settings_.volume, packetBytes))
171 return std::unexpected(
172 "voice CHAT packet exceeds the retail 64K limit");
173 if (!sendPacket(packetBytes, true))
174 return std::unexpected(
175 "unable to queue the initial voice CHAT packet");
176 return {};
177 }
178
179 std::expected<void, std::string> AudioRuntime::sendData(
180 std::span<const std::uint8_t> bytes, bool afterSilence)
181 {
182 if (bytes.empty())
183 return {};
184 std::vector<std::uint8_t> packetBytes;
186 return std::unexpected(
187 "voice data packet exceeds the retail 64K limit");
188
189 // Retail deliberately drops non-critical voice payloads when the
190 // outgoing queue is busy. That is not a capture failure.
191 (void)sendPacket(packetBytes, false);
192 return {};
193 }
194
195 bool AudioRuntime::containsNoise(
196 std::span<const std::uint8_t> bytes) const noexcept
197 {
198 if (bytes.empty())
199 return false;
200 std::uint64_t total{};
201 std::size_t count{};
202 for (std::size_t offset = 0; offset < bytes.size();
204 {
205 const int sample = static_cast<int>(bytes[offset]) - 128;
206 total += static_cast<std::uint64_t>(std::abs(sample));
207 ++count;
208 }
209 if (count == 0)
210 return false;
211 const float average = static_cast<float>(total) /
212 static_cast<float>(count) / 128.0F;
213 return average > settings_.silenceThreshold;
214 }
215
216 void AudioRuntime::appendHerald(std::vector<std::uint8_t>& bytes) const
217 {
218 const std::size_t samples = static_cast<std::size_t>(
219 settings_.heraldDelayTicks) * VoiceRate / 60u;
220 if (samples == 0 || bytes.size() + samples > CaptureReadLimit)
221 return;
222
223 std::vector<std::uint8_t> herald(samples, 128u);
224 if (settings_.heraldSoundType == 0 && samples >= 3u)
225 {
226 const std::size_t third = samples / 3u;
227 for (std::size_t index = 0; index < samples; ++index)
228 {
229 const std::size_t local =
230 index % std::max<std::size_t>(third, 1u);
231 const bool middle = index >= third && index < third * 2u;
232 const std::size_t period = middle ? 16u : 32u;
233 herald[index] =
234 (local & (period - 1u)) < period / 2u ? 148u : 108u;
235 }
236 }
237 else if (settings_.heraldSoundType == 1)
238 {
239 for (std::size_t index = 0; index < samples; ++index)
240 {
241 const auto reverse =
242 static_cast<std::uint32_t>(samples - index);
243 const auto phase = static_cast<std::uint8_t>(
244 reverse + reverse * reverse / 8192u);
245 herald[index] = (phase & 31u) < 16u ? 148u : 108u;
246 }
247 }
248 bytes.insert(bytes.begin(), herald.begin(), herald.end());
249 }
250
251 std::expected<void, std::string> AudioRuntime::pumpCapture(
252 std::uint64_t tick)
253 {
254 if (captureStream_ == nullptr)
255 return {};
256
257 const int available = SDL_GetAudioStreamAvailable(captureStream_);
258 if (available < 0)
259 return std::unexpected(
260 sdlFailure("SDL_GetAudioStreamAvailable(recording)"));
261 constexpr int minimum = VoiceRate / 4;
262 if (available < minimum)
263 return {};
264
265 const int requested = std::min<int>(
266 available, static_cast<int>(CaptureReadLimit));
267 std::vector<std::uint8_t> samples(
268 static_cast<std::size_t>(requested));
270 captureStream_, samples.data(), requested);
271 if (received < 0)
272 return std::unexpected(
273 sdlFailure("SDL_GetAudioStreamData(recording)"));
274 if (received == 0)
275 return {};
276 samples.resize(static_cast<std::size_t>(received));
277
278 const bool noisy = containsNoise(samples);
279 if (noisy)
280 lastNoiseTick_ = tick;
281 else if (tick > lastNoiseTick_ + settings_.maxSilentTicks)
282 {
283 postSilenceState_ = PostSilenceState::Silence;
284 postSilenceStorage_.clear();
285 return {};
286 }
287
288 if (postSilenceState_ == PostSilenceState::Silence)
289 {
290 if (!noisy)
291 return {};
292 postSilenceState_ = PostSilenceState::Collecting;
293 firstNoiseTick_ = lastNoiseTick_;
294 appendHerald(samples);
295 }
296
297 std::vector<std::uint8_t> wireData;
298 if (settings_.codec == Codec::Gsm610)
299 {
300 if (!captureEncoder_)
301 return std::unexpected("GSM 6.10 capture encoder was lost");
302 auto encoded = captureEncoder_->encodeU8(samples);
303 if (!encoded)
304 return std::unexpected(encoded.error());
305 wireData = std::move(*encoded);
306 }
307 else
308 wireData = std::move(samples);
309
310 if (wireData.empty())
311 return {};
312
313 if (postSilenceState_ == PostSilenceState::Collecting)
314 {
315 const bool delayReached =
316 tick >= firstNoiseTick_ + settings_.postSilenceDelayTicks;
317 const std::size_t room = PostSilenceStorageLimit -
318 std::min(postSilenceStorage_.size(),
320 const bool storageFull = wireData.size() > room;
321 if (!delayReached && !storageFull)
322 {
323 postSilenceStorage_.insert(postSilenceStorage_.end(),
324 wireData.begin(), wireData.end());
325 return {};
326 }
327
328 if (!postSilenceStorage_.empty())
329 {
330 const auto first = sendData(postSilenceStorage_, true);
331 if (!first) return first;
332 postSilenceStorage_.clear();
333 const auto current = sendData(wireData, false);
334 if (!current) return current;
335 }
336 else
337 {
338 const auto first = sendData(wireData, true);
339 if (!first) return first;
340 }
341 postSilenceState_ = PostSilenceState::Streaming;
342 return {};
343 }
344
345 return sendData(wireData, false);
346 }
347
348 std::expected<void, std::string> AudioRuntime::openReceiver(
349 const packet::Event& event, std::uint32_t sourceId)
350 {
351 const bool gsm = event.format.formatTag == gsm610::FormatTag;
353 return std::unexpected(
354 "unsupported GSM 6.10 WAVEFORMATEX parameters");
355 if (!gsm && event.format.formatTag != PcmFormatTag)
356 return std::unexpected("unsupported voice-chat wave format");
357 if (!gsm &&
358 ((event.format.bitsPerSample != 8u &&
359 event.format.bitsPerSample != 16u) ||
360 event.format.channels == 0u || event.format.channels > 2u ||
361 event.format.samplesPerSecond == 0u ||
362 event.format.samplesPerSecond > 192000u))
363 return std::unexpected("invalid PCM voice-chat format");
364
365 const auto ready = audio_.ensureReady();
366 if (!ready)
367 return std::unexpected(ready.error());
368
369 SDL_AudioSpec spec{};
370 if (gsm)
371 {
372 spec.format = SDL_AUDIO_U8;
373 spec.channels = 1;
374 spec.freq = static_cast<int>(event.format.samplesPerSecond);
375 }
376 else
377 {
378 spec.format = pcmFormat(event.format.bitsPerSample);
379 spec.channels = static_cast<int>(event.format.channels);
380 spec.freq = static_cast<int>(event.format.samplesPerSecond);
381 }
382
384 SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
385 if (stream == nullptr)
386 return std::unexpected(
387 sdlFailure("SDL_OpenAudioDeviceStream(voice playback)"));
388
389 auto receiver = std::make_unique<Receiver>();
390 receiver->stream = stream;
391 receiver->encodedFormat = event.format;
392 receiver->maxFeedingSize =
393 event.format.averageBytesPerSecond / 4u;
394 const auto align =
395 std::max<std::uint16_t>(event.format.blockAlign, 1u);
396 receiver->maxFeedingSize = std::max<std::uint32_t>(
397 align, receiver->maxFeedingSize / align * align);
398 if (gsm)
399 {
400 receiver->gsmDecoder = std::make_unique<gsm610::Decoder>();
401 if (!receiver->gsmDecoder->available())
402 return std::unexpected(
403 "libgsm WAV49 decoder is unavailable");
404 }
405 if (!SDL_ResumeAudioStreamDevice(stream))
406 return std::unexpected(
407 sdlFailure("SDL_ResumeAudioStreamDevice(voice playback)"));
408 receivers_[sourceId] = std::move(receiver);
409 return {};
410 }
411
412 bool AudioRuntime::feedReceiver(
413 Receiver& receiver, std::span<const std::uint8_t> bytes,
414 bool cutToCurrentData)
415 {
416 const auto align = std::max<std::uint16_t>(
417 receiver.encodedFormat.blockAlign, 1u);
418 if (bytes.size() % align != 0u)
419 return false;
421 {
424 return false;
425 }
426
427 std::size_t offset{};
428 while (offset < bytes.size())
429 {
430 const std::size_t amount = std::min<std::size_t>(
431 receiver.maxFeedingSize, bytes.size() - offset);
432 const auto input = bytes.subspan(offset, amount);
433 if (receiver.gsmDecoder)
434 {
435 auto decoded = receiver.gsmDecoder->decodeToU8(input);
436 if (!decoded)
437 return false;
438 if (!decoded->empty() &&
440 decoded->data(), static_cast<int>(decoded->size())))
441 return false;
442 }
443 else if (!SDL_PutAudioStreamData(receiver.stream,
444 input.data(), static_cast<int>(input.size())))
445 return false;
446 offset += amount;
447 }
448 return !cutToCurrentData ||
450 }
451
453 const packet::Event& event, std::uint32_t sourceId)
454 {
455 if (event.kind == packet::EventKind::Start)
456 {
457 receivers_.erase(sourceId);
458 return static_cast<bool>(openReceiver(event, sourceId));
459 }
460
461 const auto found = receivers_.find(sourceId);
462 if (event.kind == packet::EventKind::Stop)
463 {
464 if (found != receivers_.end())
465 receivers_.erase(found);
466 return true;
467 }
468 if (found == receivers_.end())
469 return true;
470
471 auto& receiver = *found->second;
472 switch (event.kind)
473 {
475 return SDL_SetAudioStreamGain(receiver.stream,
476 static_cast<float>(std::min(event.value, 100u)) / 100.0F);
478 return feedReceiver(receiver, event.payload, false);
480 return feedReceiver(receiver, event.payload, true);
483 return true;
486 break;
487 }
488 return true;
489 }
490
492 {
493 receivers_.clear();
494 }
495}
std::size_t count
std::size_t offset
std::vector< std::byte > bytes
unsigned index
data::DataTag first
std::string error
Bytes input
SDL_GPUShaderFormat format
std::expected< void, std::string > ensureReady()
AudioRuntime(audio::Runtime &audio) noexcept
std::expected< void, std::string > pumpCapture(std::uint64_t tick)
std::expected< void, std::string > startCapture(Settings settings={})
bool handleEvent(const packet::Event &event, std::uint32_t sourceId)
constexpr std::uint16_t FormatTag
bool matchesRetailWireFormat(const packet::Event &event) noexcept
constexpr WireFormat retailWireFormat() noexcept
bool makeDataPacket(std::span< const std::uint8_t > data, bool afterSilence, std::vector< std::uint8_t > &packet)
bool makeStartPacket(WaveFormat format, std::span< const std::uint8_t > extraFormatBytes, std::uint32_t dimensions, std::uint32_t volume, std::vector< std::uint8_t > &packet)
bool makeStopPacket(std::vector< std::uint8_t > &packet)
bool sendPacket(std::span< const std::uint8_t > payload, bool importantDataDoNotDiscard)
std::unique_ptr< gsm610::Decoder > gsmDecoder
std::span< const std::uint8_t > payload