20 std::atomic<float>
left{1.0F};
26 left.store(
gains.left, std::memory_order_relaxed);
27 right.store(
gains.right, std::memory_order_relaxed);
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);
41 const int channels =
static_cast<int>(spec->channels);
42 for (
int sample = 0; sample + 1 <
samples; sample += channels)
70 std::vector<Uint8>
pcm;
95 if (!
error.detail.empty())
98 result +=
error.detail;
105 return std::clamp(gain, 0.0F, 4.0F);
111 std::shared_ptr<const data::ResourceSnapshot> resources)
112 : resources_(std::move(resources))
119 if (ownsAudioSubsystem_)
125 return ensureAudio();
128 std::expected<void, std::string> Runtime::ensureAudio()
136 return std::unexpected(
137 std::string(
"SDL_InitSubSystem(audio): ") +
SDL_GetError());
138 ownsAudioSubsystem_ =
true;
145 Runtime::Voice* Runtime::find(PlaybackKey
key)
noexcept
147 const auto found = std::find_if(
148 voices_.begin(), voices_.end(),
149 [&](
const auto& voice) { return voice->key == key; });
153 const Runtime::Voice* Runtime::find(PlaybackKey
key)
const noexcept
155 const auto found = std::find_if(
156 voices_.begin(), voices_.end(),
157 [&](
const auto& voice) { return voice->key == key; });
169 return std::unexpected(
"audio runtime has no resource snapshot");
171 const auto ready = ensureAudio();
175 const auto metadata = resources_->data().metadata(waveDataId);
179 return std::unexpected(
"audio DataId is not a legacy Wave item");
181 const auto loaded = resources_->data().load(waveDataId);
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");
190 return std::unexpected(
decoded.error());
193 std::vector<Uint8> pcm = std::move(
decoded->pcm);
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");
201 if (stream ==
nullptr)
202 return std::unexpected(
203 std::string(
"SDL_OpenAudioDeviceStream: ") +
SDL_GetError());
204 auto voice = std::make_unique<Voice>();
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) : 0
U;
217 return std::unexpected(
218 std::string(
"SDL_SetAudioStreamGain: ") +
SDL_GetError());
222 return std::unexpected(
223 std::string(
"SDL_SetAudioStreamFrequencyRatio: ") +
SDL_GetError());
225 return std::unexpected(
226 std::string(
"SDL_SetAudioPostmixCallback: ") +
SDL_GetError());
228 const int bytes =
static_cast<int>(voice->pcm.size());
232 return std::unexpected(
233 std::string(
"SDL_PutAudioStreamData: ") +
SDL_GetError());
235 voice->submittedBytes =
static_cast<std::uint64_t
>(
bytes) *
copies;
237 return std::unexpected(
238 std::string(
"SDL_FlushAudioStream: ") +
SDL_GetError());
240 return std::unexpected(
241 std::string(
"SDL_ResumeAudioStreamDevice: ") +
SDL_GetError());
243 voices_.push_back(std::move(voice));
249 std::string_view relativePath,
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");
262 const auto ready = ensureAudio();
266 const auto resolved = resources_->paths().resolve(relativePath);
271 const std::string
path(
272 reinterpret_cast<const char*
>(
pathBytes.data()),
280 return std::unexpected(
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");
297 if (stream ==
nullptr)
298 return std::unexpected(
299 std::string(
"SDL_OpenAudioDeviceStream: ") +
302 auto voice = std::make_unique<Voice>();
305 voice->stream = stream;
306 voice->pcm = std::move(pcm);
307 voice->sourceFrequency = spec.freq > 0
308 ?
static_cast<std::uint32_t
>(spec.freq)
316 return std::unexpected(
317 std::string(
"SDL_SetAudioStreamGain: ") +
323 return std::unexpected(
324 std::string(
"SDL_SetAudioStreamFrequencyRatio: ") +
327 return std::unexpected(
328 std::string(
"SDL_SetAudioPostmixCallback: ") +
332 static_cast<int>(voice->pcm.size());
336 stream, voice->pcm.data(),
bytes))
337 return std::unexpected(
338 std::string(
"SDL_PutAudioStreamData: ") +
341 voice->submittedBytes =
static_cast<std::uint64_t
>(
bytes) *
copies;
343 return std::unexpected(
344 std::string(
"SDL_FlushAudioStream: ") +
347 return std::unexpected(
348 std::string(
"SDL_ResumeAudioStreamDevice: ") +
352 voices_.push_back(std::move(voice));
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);
372 if (
auto* voice = find(
key))
381 if (
auto* voice = find(
key))
392 if (
auto* voice = find(
key))
398 auto* voice = find(
key);
399 if (!voice || voice->loop ==
loop)
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()));
411 if (!queueVoiceFromByte(*voice,
offset)) stop(
key);
416 const auto* voice = find(
key);
417 if (!voice || !voice->stream || voice->sourceFrequency == 0 ||
418 voice->bytesPerFrame == 0 || voice->pcm.empty())
return std::nullopt;
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()));
431 std::expected<void, std::string> Runtime::seekVoice(
Voice& voice, std::int32_t tick)
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);
442 std::expected<void, std::string> Runtime::queueVoiceFromByte(
Voice& voice, std::uint64_t
offset)
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);
449 voice.pcm.data() +
offset,
static_cast<int>(
tail)))
450 return std::unexpected(std::string(
"audio seek data: ") +
SDL_GetError());
451 voice.submittedBytes +=
tail;
457 static_cast<int>(voice.pcm.size())))
458 return std::unexpected(std::string(
"audio loop data: ") +
SDL_GetError());
459 voice.submittedBytes += voice.pcm.size();
463 return std::unexpected(std::string(
"audio seek flush: ") +
SDL_GetError());
465 return std::unexpected(std::string(
"audio seek resume: ") +
SDL_GetError());
470 PlaybackKey key, std::int32_t position,
bool paused, std::uint64_t seekGeneration)
472 auto* voice = find(
key);
473 if (!voice)
return std::unexpected(
"sequence audio voice is absent");
476 if (!voice->seekGeneration && position == 0 && !paused)
478 voice->seekGeneration = seekGeneration;
482 voice->paused = paused;
483 if (voice->seekGeneration != seekGeneration)
485 const auto sought = seekVoice(*voice, position);
487 voice->seekGeneration = seekGeneration;
493 if (!
ok)
return std::unexpected(std::string(
"audio pause: ") +
SDL_GetError());
500 for (
auto& voice : voices_)
502 if (!voice->loop || voice->stream ==
nullptr || voice->pcm.empty())
509 const std::size_t
target = voice->pcm.size() * 2u;
510 std::size_t current =
static_cast<std::size_t
>(queued);
516 static_cast<int>(voice->pcm.size())))
518 current += voice->pcm.size();
519 voice->submittedBytes += voice->pcm.size();
526 const auto* voice = find(
key);
527 return voice !=
nullptr && voice->stream !=
nullptr &&
std::atomic< float > left
std::atomic< float > right
LegacyBitmapMetadata metadata
std::vector< std::byte > bytes
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
constexpr DataId EmptyDataId
std::string_view dataErrorCodeName(DataErrorCode code) noexcept
std::uint64_t submittedBytes
std::uint32_t sourceFrequency
std::uint32_t bytesPerFrame
std::optional< std::uint64_t > seekGeneration