Monopoly — Portage moderne 0.1.0
Portage moderne C/C++23 du Monopoly de 1999
Loading...
Searching...
No Matches
AudioRuntime.cpp
Go to the documentation of this file.
1#include "AudioRuntime.hpp"
2
5
6#include <SDL3/SDL.h>
7
8#include <algorithm>
9#include <atomic>
10#include <climits>
11#include <limits>
12#include <utility>
13
15{
16 namespace
17 {
18 struct StereoPanState
19 {
20 std::atomic<float> left{1.0F};
21 std::atomic<float> right{1.0F};
22
23 void set(std::int32_t percentage) noexcept
24 {
25 const auto gains = legacyPanGains(percentage);
26 left.store(gains.left, std::memory_order_relaxed);
27 right.store(gains.right, std::memory_order_relaxed);
28 }
29 };
30
32 void* userdata, const SDL_AudioSpec* spec,
33 float* buffer, int buflen) noexcept
34 {
35 if (!userdata || !spec || !buffer || buflen <= 0 || spec->channels < 2)
36 return;
37 const auto& pan = *static_cast<const StereoPanState*>(userdata);
38 const float left = pan.left.load(std::memory_order_relaxed);
39 const float right = pan.right.load(std::memory_order_relaxed);
40 const int samples = buflen / static_cast<int>(sizeof(float));
41 const int channels = static_cast<int>(spec->channels);
42 for (int sample = 0; sample + 1 < samples; sample += channels)
43 {
44 buffer[sample] *= left;
45 buffer[sample + 1] *= right;
46 }
47 }
48
50 SDL_AudioStream* stream, StereoPanState& pan) noexcept
51 {
52 const auto device = SDL_GetAudioStreamDevice(stream);
53 return device != 0 &&
55 }
56
57 void uninstallStereoPan(SDL_AudioStream* stream) noexcept
58 {
59 const auto device = SDL_GetAudioStreamDevice(stream);
60 if (device != 0)
61 (void)SDL_SetAudioPostmixCallback(device, nullptr, nullptr);
62 }
63 }
64
66 {
70 std::vector<Uint8> pcm;
71 std::uint32_t sourceFrequency{};
72 float gain{1.0F};
73 StereoPanState pan;
74 bool loop{};
75 bool paused{};
76 std::uint32_t bytesPerFrame{};
77 std::uint64_t submittedBytes{};
78 std::optional<std::uint64_t> seekGeneration;
79
81 {
82 if (stream != nullptr)
83 {
86 }
87 }
88 };
89 namespace
90 {
91 [[nodiscard]] std::string dataFailure(
93 {
94 std::string result(data::dataErrorCodeName(error.code));
95 if (!error.detail.empty())
96 {
97 result += ": ";
98 result += error.detail;
99 }
100 return result;
101 }
102
103 [[nodiscard]] float clampedGain(float gain) noexcept
104 {
105 return std::clamp(gain, 0.0F, 4.0F);
106 }
107
108 }
109
111 std::shared_ptr<const data::ResourceSnapshot> resources)
112 : resources_(std::move(resources))
113 {
114 }
115
117 {
118 stopAll();
119 if (ownsAudioSubsystem_)
121 }
122
123 std::expected<void, std::string> Runtime::ensureReady()
124 {
125 return ensureAudio();
126 }
127
128 std::expected<void, std::string> Runtime::ensureAudio()
129 {
130 if (audioReady_)
131 return {};
132
134 {
136 return std::unexpected(
137 std::string("SDL_InitSubSystem(audio): ") + SDL_GetError());
138 ownsAudioSubsystem_ = true;
139 }
140
141 audioReady_ = true;
142 return {};
143 }
144
145 Runtime::Voice* Runtime::find(PlaybackKey key) noexcept
146 {
147 const auto found = std::find_if(
148 voices_.begin(), voices_.end(),
149 [&](const auto& voice) { return voice->key == key; });
150 return found == voices_.end() ? nullptr : found->get();
151 }
152
153 const Runtime::Voice* Runtime::find(PlaybackKey key) const noexcept
154 {
155 const auto found = std::find_if(
156 voices_.begin(), voices_.end(),
157 [&](const auto& voice) { return voice->key == key; });
158 return found == voices_.end() ? nullptr : found->get();
159 }
160 std::expected<void, std::string> Runtime::play(
162 data::DataId waveDataId,
163 float gain,
164 bool loop,
165 std::uint32_t pitchHertz,
166 std::int32_t panPercentage)
167 {
168 if (!resources_)
169 return std::unexpected("audio runtime has no resource snapshot");
170
171 const auto ready = ensureAudio();
172 if (!ready)
173 return ready;
174
175 const auto metadata = resources_->data().metadata(waveDataId);
176 if (!metadata)
177 return std::unexpected(dataFailure(metadata.error()));
179 return std::unexpected("audio DataId is not a legacy Wave item");
180
181 const auto loaded = resources_->data().load(waveDataId);
182 if (!loaded)
183 return std::unexpected(dataFailure(loaded.error()));
184 if ((*loaded)->empty())
185 return std::unexpected("legacy Wave item is empty");
186 if ((*loaded)->size() > static_cast<std::size_t>(INT_MAX))
187 return std::unexpected("legacy Wave item exceeds SDL stream limits");
188 auto decoded = decodeWave(**loaded);
189 if (!decoded)
190 return std::unexpected(decoded.error());
191
192 const SDL_AudioSpec spec = decoded->spec;
193 std::vector<Uint8> pcm = std::move(decoded->pcm);
194 if (pcm.empty())
195 return std::unexpected("decoded legacy Wave contains no PCM data");
196 if (pcm.size() > static_cast<std::size_t>(INT_MAX))
197 return std::unexpected("decoded Wave exceeds SDL stream limits");
198
200 SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr);
201 if (stream == nullptr)
202 return std::unexpected(
203 std::string("SDL_OpenAudioDeviceStream: ") + SDL_GetError());
204 auto voice = std::make_unique<Voice>();
205 voice->key = key;
206 voice->waveDataId = waveDataId;
207 voice->stream = stream;
208 voice->pcm = std::move(pcm);
209 voice->sourceFrequency = spec.freq > 0 ?
210 static_cast<std::uint32_t>(spec.freq) : 0U;
211 voice->gain = clampedGain(gain);
212 voice->pan.set(panPercentage);
213 voice->loop = loop;
214 voice->bytesPerFrame = static_cast<std::uint32_t>(SDL_AUDIO_FRAMESIZE(spec));
215
216 if (!SDL_SetAudioStreamGain(stream, voice->gain))
217 return std::unexpected(
218 std::string("SDL_SetAudioStreamGain: ") + SDL_GetError());
220 stream,
221 legacyPitchFrequencyRatio(pitchHertz, voice->sourceFrequency)))
222 return std::unexpected(
223 std::string("SDL_SetAudioStreamFrequencyRatio: ") + SDL_GetError());
224 if (!installStereoPan(stream, voice->pan))
225 return std::unexpected(
226 std::string("SDL_SetAudioPostmixCallback: ") + SDL_GetError());
227
228 const int bytes = static_cast<int>(voice->pcm.size());
229 const int copies = loop ? 3 : 1;
230 for (int copy = 0; copy < copies; ++copy)
231 if (!SDL_PutAudioStreamData(stream, voice->pcm.data(), bytes))
232 return std::unexpected(
233 std::string("SDL_PutAudioStreamData: ") + SDL_GetError());
234
235 voice->submittedBytes = static_cast<std::uint64_t>(bytes) * copies;
236 if (!loop && !SDL_FlushAudioStream(stream))
237 return std::unexpected(
238 std::string("SDL_FlushAudioStream: ") + SDL_GetError());
239 if (!SDL_ResumeAudioStreamDevice(stream))
240 return std::unexpected(
241 std::string("SDL_ResumeAudioStreamDevice: ") + SDL_GetError());
242 stop(key);
243 voices_.push_back(std::move(voice));
244 return {};
245 }
246
247 std::expected<void, std::string> Runtime::playFile(
249 std::string_view relativePath,
250 float gain,
251 bool loop,
252 std::uint32_t pitchHertz,
253 std::int32_t panPercentage)
254 {
255 if (!resources_)
256 return std::unexpected(
257 "audio runtime has no resource snapshot");
258 if (relativePath.empty())
259 return std::unexpected(
260 "external audio filename is empty");
261
262 const auto ready = ensureAudio();
263 if (!ready)
264 return ready;
265
266 const auto resolved = resources_->paths().resolve(relativePath);
267 if (!resolved)
268 return std::unexpected(dataFailure(resolved.error()));
269
270 const auto pathBytes = resolved->u8string();
271 const std::string path(
272 reinterpret_cast<const char*>(pathBytes.data()),
273 pathBytes.size());
274
275 SDL_AudioSpec spec{};
276 Uint8* decoded{};
278 if (!SDL_LoadWAV(
279 path.c_str(), &spec, &decoded, &decodedLength))
280 return std::unexpected(
281 std::string("SDL_LoadWAV: ") + SDL_GetError());
282
283 std::vector<Uint8> pcm(decoded, decoded + decodedLength);
285 if (pcm.empty())
286 return std::unexpected(
287 "decoded external Wave contains no PCM data");
288 if (pcm.size() > static_cast<std::size_t>(INT_MAX))
289 return std::unexpected(
290 "decoded external Wave exceeds SDL stream limits");
291
294 &spec,
295 nullptr,
296 nullptr);
297 if (stream == nullptr)
298 return std::unexpected(
299 std::string("SDL_OpenAudioDeviceStream: ") +
300 SDL_GetError());
301
302 auto voice = std::make_unique<Voice>();
303 voice->key = key;
304 voice->waveDataId = data::EmptyDataId;
305 voice->stream = stream;
306 voice->pcm = std::move(pcm);
307 voice->sourceFrequency = spec.freq > 0
308 ? static_cast<std::uint32_t>(spec.freq)
309 : 0U;
310 voice->gain = clampedGain(gain);
311 voice->pan.set(panPercentage);
312 voice->loop = loop;
313 voice->bytesPerFrame = static_cast<std::uint32_t>(SDL_AUDIO_FRAMESIZE(spec));
314
315 if (!SDL_SetAudioStreamGain(stream, voice->gain))
316 return std::unexpected(
317 std::string("SDL_SetAudioStreamGain: ") +
318 SDL_GetError());
320 stream,
322 pitchHertz, voice->sourceFrequency)))
323 return std::unexpected(
324 std::string("SDL_SetAudioStreamFrequencyRatio: ") +
325 SDL_GetError());
326 if (!installStereoPan(stream, voice->pan))
327 return std::unexpected(
328 std::string("SDL_SetAudioPostmixCallback: ") +
329 SDL_GetError());
330
331 const int bytes =
332 static_cast<int>(voice->pcm.size());
333 const int copies = loop ? 3 : 1;
334 for (int copy = 0; copy < copies; ++copy)
336 stream, voice->pcm.data(), bytes))
337 return std::unexpected(
338 std::string("SDL_PutAudioStreamData: ") +
339 SDL_GetError());
340
341 voice->submittedBytes = static_cast<std::uint64_t>(bytes) * copies;
342 if (!loop && !SDL_FlushAudioStream(stream))
343 return std::unexpected(
344 std::string("SDL_FlushAudioStream: ") +
345 SDL_GetError());
346 if (!SDL_ResumeAudioStreamDevice(stream))
347 return std::unexpected(
348 std::string("SDL_ResumeAudioStreamDevice: ") +
349 SDL_GetError());
350
351 stop(key);
352 voices_.push_back(std::move(voice));
353 return {};
354 }
355
357 {
358 const auto found = std::find_if(
359 voices_.begin(), voices_.end(),
360 [&](const auto& voice) { return voice->key == key; });
361 if (found != voices_.end())
362 voices_.erase(found);
363 }
364
366 {
367 voices_.clear();
368 }
369
370 void Runtime::setGain(PlaybackKey key, float gain) noexcept
371 {
372 if (auto* voice = find(key))
373 {
374 voice->gain = clampedGain(gain);
375 (void)SDL_SetAudioStreamGain(voice->stream, voice->gain);
376 }
377 }
378
379 void Runtime::setPitch(PlaybackKey key, std::uint32_t hertz) noexcept
380 {
381 if (auto* voice = find(key))
382 {
384 voice->stream,
385 legacyPitchFrequencyRatio(hertz, voice->sourceFrequency));
386 }
387 }
388
390 PlaybackKey key, std::int32_t percentage) noexcept
391 {
392 if (auto* voice = find(key))
393 voice->pan.set(percentage);
394 }
395
397 {
398 auto* voice = find(key);
399 if (!voice || voice->loop == loop) return;
400 // Freeze the device before sampling its cursor. Removing prequeued
401 // repeats must preserve the current iteration, including after wrap.
402 if (!SDL_PauseAudioStreamDevice(voice->stream)) { stop(key); return; }
403 const auto queued = SDL_GetAudioStreamQueued(voice->stream);
404 if (queued < 0 || voice->pcm.empty()) { stop(key); return; }
405 auto offset = voice->submittedBytes -
406 std::min(voice->submittedBytes, static_cast<std::uint64_t>(queued));
407 if (voice->loop) offset %= voice->pcm.size();
408 else offset = std::min(offset, static_cast<std::uint64_t>(voice->pcm.size()));
409 voice->loop = loop;
410 if (loop) offset %= voice->pcm.size();
411 if (!queueVoiceFromByte(*voice, offset)) stop(key);
412 }
413
414 std::optional<std::int32_t> Runtime::positionTicks(PlaybackKey key) const noexcept
415 {
416 const auto* voice = find(key);
417 if (!voice || !voice->stream || voice->sourceFrequency == 0 ||
418 voice->bytesPerFrame == 0 || voice->pcm.empty()) return std::nullopt;
419 const auto queued = SDL_GetAudioStreamQueued(voice->stream);
420 if (queued < 0) return std::nullopt;
421 auto consumed = voice->submittedBytes -
422 std::min(voice->submittedBytes, static_cast<std::uint64_t>(queued));
423 if (voice->loop) consumed %= voice->pcm.size();
424 else consumed = std::min(consumed, static_cast<std::uint64_t>(voice->pcm.size()));
425 const auto ticks = consumed * 60U /
426 (static_cast<std::uint64_t>(voice->bytesPerFrame) * voice->sourceFrequency);
427 return static_cast<std::int32_t>(std::min<std::uint64_t>(ticks,
428 std::numeric_limits<std::int32_t>::max()));
429 }
430
431 std::expected<void, std::string> Runtime::seekVoice(Voice& voice, std::int32_t tick)
432 {
433 if (tick < 0 || voice.bytesPerFrame == 0 || voice.pcm.size() < voice.bytesPerFrame)
434 return std::unexpected("invalid audio seek");
435 auto offset = static_cast<std::uint64_t>(tick) * voice.sourceFrequency / 60U * voice.bytesPerFrame;
436 if (voice.loop) offset %= voice.pcm.size();
437 else offset = std::min(offset, static_cast<std::uint64_t>(
438 voice.pcm.size() - voice.bytesPerFrame));
439 return queueVoiceFromByte(voice, offset);
440 }
441
442 std::expected<void, std::string> Runtime::queueVoiceFromByte(Voice& voice, std::uint64_t offset)
443 {
444 if (!SDL_PauseAudioStreamDevice(voice.stream) || !SDL_ClearAudioStream(voice.stream))
445 return std::unexpected(std::string("audio seek: ") + SDL_GetError());
446 voice.submittedBytes = offset;
447 const auto tail = voice.pcm.size() - static_cast<std::size_t>(offset);
448 if (tail && !SDL_PutAudioStreamData(voice.stream,
449 voice.pcm.data() + offset, static_cast<int>(tail)))
450 return std::unexpected(std::string("audio seek data: ") + SDL_GetError());
451 voice.submittedBytes += tail;
452 if (voice.loop)
453 {
454 for (int copy = 0; copy < 2; ++copy)
455 {
456 if (!SDL_PutAudioStreamData(voice.stream, voice.pcm.data(),
457 static_cast<int>(voice.pcm.size())))
458 return std::unexpected(std::string("audio loop data: ") + SDL_GetError());
459 voice.submittedBytes += voice.pcm.size();
460 }
461 }
462 else if (!SDL_FlushAudioStream(voice.stream))
463 return std::unexpected(std::string("audio seek flush: ") + SDL_GetError());
464 if (!voice.paused && !SDL_ResumeAudioStreamDevice(voice.stream))
465 return std::unexpected(std::string("audio seek resume: ") + SDL_GetError());
466 return {};
467 }
468
469 std::expected<void, std::string> Runtime::synchronizeSequence(
470 PlaybackKey key, std::int32_t position, bool paused, std::uint64_t seekGeneration)
471 {
472 auto* voice = find(key);
473 if (!voice) return std::unexpected("sequence audio voice is absent");
474 // A new voice was already queued at sample zero by play/playFile.
475 // Do not clear and replay that buffer on its first synchronization.
476 if (!voice->seekGeneration && position == 0 && !paused)
477 {
478 voice->seekGeneration = seekGeneration;
479 return {};
480 }
481 const bool pauseChanged = voice->paused != paused;
482 voice->paused = paused;
483 if (voice->seekGeneration != seekGeneration)
484 {
485 const auto sought = seekVoice(*voice, position);
486 if (!sought) return sought;
487 voice->seekGeneration = seekGeneration;
488 }
489 else if (pauseChanged)
490 {
491 const bool ok = paused ? SDL_PauseAudioStreamDevice(voice->stream) :
492 SDL_ResumeAudioStreamDevice(voice->stream);
493 if (!ok) return std::unexpected(std::string("audio pause: ") + SDL_GetError());
494 }
495 return {};
496 }
497
499 {
500 for (auto& voice : voices_)
501 {
502 if (!voice->loop || voice->stream == nullptr || voice->pcm.empty())
503 continue;
504
505 const int queued = SDL_GetAudioStreamQueued(voice->stream);
506 if (queued < 0)
507 continue;
508
509 const std::size_t target = voice->pcm.size() * 2u;
510 std::size_t current = static_cast<std::size_t>(queued);
511 while (current < target)
512 {
514 voice->stream,
515 voice->pcm.data(),
516 static_cast<int>(voice->pcm.size())))
517 break;
518 current += voice->pcm.size();
519 voice->submittedBytes += voice->pcm.size();
520 }
521 }
522 }
523
524 bool Runtime::active(PlaybackKey key) const noexcept
525 {
526 const auto* voice = find(key);
527 return voice != nullptr && voice->stream != nullptr &&
528 (voice->loop || SDL_GetAudioStreamQueued(voice->stream) > 0);
529 }
530}
std::size_t offset
std::atomic< float > left
std::atomic< float > right
LegacyBitmapMetadata metadata
std::vector< std::byte > bytes
std::uint64_t key
unsigned index
std::string path
std::string error
std::expected< void, std::string > synchronizeSequence(PlaybackKey key, std::int32_t position, bool paused, std::uint64_t seekGeneration)
void stop(PlaybackKey key) noexcept
void setPitch(PlaybackKey key, std::uint32_t hertz) noexcept
void setGain(PlaybackKey key, float gain) noexcept
void setLooping(PlaybackKey key, bool loop) noexcept
void setPanning(PlaybackKey key, std::int32_t percentage) noexcept
std::expected< void, std::string > playFile(PlaybackKey key, std::string_view relativePath, float gain=1.0F, bool loop=false, std::uint32_t pitchHertz=0U, std::int32_t panPercentage=0)
Runtime(std::shared_ptr< const data::ResourceSnapshot > resources)
bool active(PlaybackKey key) const noexcept
std::expected< void, std::string > play(PlaybackKey key, data::DataId waveDataId, float gain=1.0F, bool loop=false, std::uint32_t pitchHertz=0U, std::int32_t panPercentage=0)
std::optional< std::int32_t > positionTicks(PlaybackKey key) const noexcept
std::expected< void, std::string > ensureReady()
constexpr StereoPanGains legacyPanGains(std::int32_t percentage) noexcept
std::expected< DecodedWave, std::string > decodeWave(std::span< const std::byte > bytes)
constexpr float legacyPitchFrequencyRatio(std::uint32_t requestedHertz, std::uint32_t originalHertz) noexcept
std::uint32_t DataId
Definition DataBanks.hpp:10
constexpr DataId EmptyDataId
Definition DataBanks.hpp:13
std::string_view dataErrorCodeName(DataErrorCode code) noexcept
std::optional< std::uint64_t > seekGeneration