yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
audio_backend.cc
Go to the documentation of this file.
1// audio_backend.cc - Audio Backend Implementation
2
4
6
7#if defined(__APPLE__)
8#include <TargetConditionals.h>
9#endif
10
11#include <algorithm>
12#include <vector>
13
14#include "util/log.h"
15
16#ifdef YAZE_USE_SDL3
18#endif
19
20#ifdef __EMSCRIPTEN__
22#endif
23
24namespace yaze {
25namespace emu {
26namespace audio {
27
28// ============================================================================
29// SDL2AudioBackend Implementation
30// ============================================================================
31
32#ifdef YAZE_USE_SDL3
33
34// SDL2 backend is not available in SDL3 builds; provide stubs to satisfy
35// legacy interfaces while deferring to SDL3AudioBackend in the factory.
37
38bool SDL2AudioBackend::Initialize(const AudioConfig& /*config*/) {
39 LOG_ERROR("AudioBackend",
40 "SDL2AudioBackend is unavailable when building with SDL3");
41 return false;
42}
43
49
50bool SDL2AudioBackend::QueueSamples(const int16_t* /*samples*/,
51 int /*num_samples*/) {
52 return false;
53}
54
55bool SDL2AudioBackend::QueueSamples(const float* /*samples*/,
56 int /*num_samples*/) {
57 return false;
58}
59
60bool SDL2AudioBackend::QueueSamplesNative(const int16_t* /*samples*/,
61 int /*frames_per_channel*/,
62 int /*channels*/,
63 int /*native_rate*/) {
64 return false;
65}
66
67AudioStatus SDL2AudioBackend::GetStatus() const { return {}; }
68bool SDL2AudioBackend::IsInitialized() const { return false; }
69AudioConfig SDL2AudioBackend::GetConfig() const { return config_; }
70
71void SDL2AudioBackend::SetVolume(float /*volume*/) {}
72float SDL2AudioBackend::GetVolume() const { return 0.0f; }
73
75 int /*native_rate*/,
76 int /*channels*/) {}
77
78bool SDL2AudioBackend::IsAudioStreamEnabled() const { return false; }
79
80#else
81
85
87 if (initialized_) {
88 LOG_WARN("AudioBackend", "Already initialized, shutting down first");
89 Shutdown();
90 }
91
92 config_ = config;
93
94 SDL_AudioSpec want, have;
95 SDL_memset(&want, 0, sizeof(want));
96
97 // Force 48000Hz request to ensure we get a standard rate that SDL/CoreAudio
98 // handles reliably. We will handle 32kHz -> 48kHz resampling ourselves
99 // using SDL_AudioStream.
100 want.freq = 48000;
101 want.format = (config.format == SampleFormat::INT16) ? AUDIO_S16 : AUDIO_F32;
102 want.channels = config.channels;
103 want.samples = config.buffer_frames;
104 want.callback = nullptr; // Use queue-based audio
105
106 // Allow SDL to change any parameter to match the hardware.
107 // This is CRITICAL: If we force 32040Hz on a 48000Hz device without allowing changes,
108 // SDL might claim success but playback will be at the wrong speed (chipmunk effect).
109 // By allowing changes, 'have' will contain the REAL hardware spec (e.g. 48000Hz),
110 // which allows us to detect the mismatch and enable our software resampler.
111 // Allow format and channel changes, but FORCE frequency to 48000Hz.
112 // This prevents issues where SDL reports a weird frequency (e.g. 32040Hz)
113 // but the hardware actually runs at 48000Hz, causing pitch/speed issues.
114 // SDL will handle internal resampling if the hardware doesn't support 48000Hz.
115 int allowed_changes = SDL_AUDIO_ALLOW_FORMAT_CHANGE | SDL_AUDIO_ALLOW_CHANNELS_CHANGE;
116#if defined(__APPLE__) && (TARGET_OS_IPHONE == 1 || TARGET_IPHONE_SIMULATOR == 1)
117 SDL_SetHint(SDL_HINT_AUDIO_CATEGORY, "ambient");
118#endif
119 device_id_ = SDL_OpenAudioDevice(nullptr, 0, &want, &have, allowed_changes);
120
121 if (device_id_ == 0) {
122 LOG_ERROR("AudioBackend", "Failed to open SDL audio device: %s",
123 SDL_GetError());
124 return false;
125 }
126
127 device_format_ = have.format;
128 device_channels_ = have.channels;
129 device_freq_ = have.freq;
130
131 // Verify we got what we asked for
132 if (have.freq != want.freq || have.channels != want.channels) {
133 LOG_WARN("AudioBackend",
134 "Audio spec mismatch - wanted %dHz %dch, got %dHz %dch", want.freq,
135 want.channels, have.freq, have.channels);
136 // Update config with actual values
137 config_.sample_rate = have.freq;
138 config_.channels = have.channels;
139 }
140
141 LOG_INFO("AudioBackend",
142 "SDL2 audio initialized: %dHz, %d channels, buffer: want=%d, have=%d",
143 have.freq, have.channels, want.samples, have.samples);
144 LOG_INFO("AudioBackend",
145 "Device actual: freq=%d, format=0x%04X, channels=%d (device_id=%u)",
147
148 initialized_ = true;
149 audio_stream_enabled_ = false;
151 if (audio_stream_) {
152 SDL_FreeAudioStream(audio_stream_);
153 audio_stream_ = nullptr;
154 }
155 stream_buffer_.clear();
156
157 // Start playback immediately (unpause)
158 SDL_PauseAudioDevice(device_id_, 0);
159
160 return true;
161}
162
164 if (!initialized_)
165 return;
166
167 if (audio_stream_) {
168 SDL_FreeAudioStream(audio_stream_);
169 audio_stream_ = nullptr;
170 }
171 audio_stream_enabled_ = false;
173 stream_buffer_.clear();
174
175 if (device_id_ != 0) {
176 SDL_PauseAudioDevice(device_id_, 1);
177 SDL_CloseAudioDevice(device_id_);
178 device_id_ = 0;
179 }
180
181 initialized_ = false;
182 LOG_INFO("AudioBackend", "SDL2 audio shut down");
183}
184
186 if (!initialized_) {
187 LOG_WARN("AudioBackend", "Play() called but not initialized!");
188 return;
189 }
190 SDL_AudioStatus status_before = SDL_GetAudioDeviceStatus(device_id_);
191 SDL_PauseAudioDevice(device_id_, 0);
192 SDL_AudioStatus status_after = SDL_GetAudioDeviceStatus(device_id_);
193 LOG_INFO("AudioBackend", "Play() device=%u: status %d -> %d (0=stopped,1=playing,2=paused)",
194 device_id_, status_before, status_after);
195}
196
198 if (!initialized_)
199 return;
200 SDL_PauseAudioDevice(device_id_, 1);
201}
202
204 if (!initialized_)
205 return;
206 Clear();
207 SDL_PauseAudioDevice(device_id_, 1);
208}
209
211 if (!initialized_)
212 return;
213 uint32_t before = SDL_GetQueuedAudioSize(device_id_);
214 SDL_ClearQueuedAudio(device_id_);
215 if (audio_stream_) {
216 SDL_AudioStreamClear(audio_stream_);
217 }
218 uint32_t after = SDL_GetQueuedAudioSize(device_id_);
219 LOG_INFO("AudioBackend", "Clear() device=%u: queue %u -> %u bytes", device_id_, before, after);
220}
221
222bool SDL2AudioBackend::QueueSamples(const int16_t* samples, int num_samples) {
223 if (!initialized_ || !samples)
224 return false;
225
226 // Periodic logging (debug only, very infrequent)
227 static int queue_log = 0;
228 if (++queue_log % 2000 == 0) {
229 LOG_DEBUG("AudioBackend", "QueueSamples: %d samples to device %u", num_samples, device_id_);
230 }
231
232 if (volume_ == 1.0f) {
233 // Fast path: No volume adjustment needed
234 int result =
235 SDL_QueueAudio(device_id_, samples, num_samples * sizeof(int16_t));
236 if (result < 0) {
237 static int error_log = 0;
238 if (++error_log % 60 == 0) {
239 LOG_ERROR("AudioBackend", "SDL_QueueAudio failed: %s", SDL_GetError());
240 }
241 return false;
242 }
243 return true;
244 }
245
246 // Slow path: Volume scaling required
247 // Use thread-local buffer to avoid repeated allocations
248 thread_local std::vector<int16_t> scaled_samples;
249
250 // Resize only if needed (avoid reallocation on every call)
251 if (scaled_samples.size() < static_cast<size_t>(num_samples)) {
252 scaled_samples.resize(num_samples);
253 }
254
255 // Apply volume scaling with SIMD-friendly loop
256 for (int i = 0; i < num_samples; ++i) {
257 int32_t scaled = static_cast<int32_t>(samples[i] * volume_);
258 // Clamp to prevent overflow
259 if (scaled > 32767)
260 scaled = 32767;
261 else if (scaled < -32768)
262 scaled = -32768;
263 scaled_samples[i] = static_cast<int16_t>(scaled);
264 }
265
266 int result = SDL_QueueAudio(device_id_, scaled_samples.data(),
267 num_samples * sizeof(int16_t));
268 if (result < 0) {
269 static int error_log = 0;
270 if (++error_log % 60 == 0) {
271 LOG_ERROR("AudioBackend", "SDL_QueueAudio failed: %s", SDL_GetError());
272 }
273 return false;
274 }
275
276 return true;
277}
278
279bool SDL2AudioBackend::QueueSamples(const float* samples, int num_samples) {
280 if (!initialized_ || !samples)
281 return false;
282
283 // Convert float to int16
284 std::vector<int16_t> int_samples(num_samples);
285 for (int i = 0; i < num_samples; ++i) {
286 float scaled = std::clamp(samples[i] * volume_, -1.0f, 1.0f);
287 int_samples[i] = static_cast<int16_t>(scaled * 32767.0f);
288 }
289
290 return QueueSamples(int_samples.data(), num_samples);
291}
292
293bool SDL2AudioBackend::QueueSamplesNative(const int16_t* samples,
294 int frames_per_channel, int channels,
295 int native_rate) {
296 // DIAGNOSTIC: Track which backend instance is calling (per-instance, not static)
297 call_count_++;
298 if (call_count_ <= 2 || call_count_ % 1000 == 0) {
299 LOG_DEBUG("AudioBackend",
300 "QueueSamplesNative [device=%u]: frames=%d, ch=%d, native=%dHz, "
301 "stream_enabled=%d, stream=%p, device_freq=%dHz, calls=%d",
302 device_id_, frames_per_channel, channels, native_rate,
305 }
306
307 if (!initialized_ || samples == nullptr) {
308 static int init_fail_log = 0;
309 if (++init_fail_log % 300 == 0) {
310 LOG_WARN("AudioBackend", "QueueSamplesNative: FAILED (init=%d, samples=%p)",
311 initialized_, samples);
312 }
313 return false;
314 }
315
316 if (!audio_stream_enabled_ || audio_stream_ == nullptr) {
317 static int stream_fail_log = 0;
318 if (++stream_fail_log % 600 == 0) {
319 LOG_WARN("AudioBackend", "QueueSamplesNative: STREAM DISABLED (enabled=%d, stream=%p) - Audio will play at WRONG SPEED!",
320 audio_stream_enabled_, static_cast<void*>(audio_stream_));
321 }
322 return false;
323 }
324
325 static int native_log = 0;
326 if (++native_log % 300 == 0) {
327 LOG_DEBUG("AudioBackend", "QueueSamplesNative: %d frames (Native: %d, Stream: %d)", frames_per_channel, native_rate, stream_native_rate_);
328 }
329
330 if (native_rate != stream_native_rate_ || channels != config_.channels) {
331 SetAudioStreamResampling(true, native_rate, channels);
332 if (audio_stream_ == nullptr) {
333 return false;
334 }
335 }
336
337 const int bytes_in =
338 frames_per_channel * channels * static_cast<int>(sizeof(int16_t));
339
340 if (SDL_AudioStreamPut(audio_stream_, samples, bytes_in) < 0) {
341 static int put_log = 0;
342 if (++put_log % 60 == 0) {
343 LOG_ERROR("AudioBackend", "SDL_AudioStreamPut failed: %s", SDL_GetError());
344 }
345 return false;
346 }
347
348 const int available_bytes = SDL_AudioStreamAvailable(audio_stream_);
349 if (available_bytes < 0) {
350 static int avail_log = 0;
351 if (++avail_log % 60 == 0) {
352 LOG_ERROR("AudioBackend", "SDL_AudioStreamAvailable failed: %s",
353 SDL_GetError());
354 }
355 return false;
356 }
357
358 if (available_bytes == 0) {
359 return true;
360 }
361
362 const int available_samples =
363 available_bytes / static_cast<int>(sizeof(int16_t));
364 if (static_cast<int>(stream_buffer_.size()) < available_samples) {
365 stream_buffer_.resize(available_samples);
366 }
367
368 int bytes_read = SDL_AudioStreamGet(audio_stream_, stream_buffer_.data(),
369 available_bytes);
370 if (bytes_read < 0) {
371 static int get_log = 0;
372 if (++get_log % 60 == 0) {
373 LOG_ERROR("AudioBackend", "SDL_AudioStreamGet failed: %s", SDL_GetError());
374 }
375 return false;
376 }
377
378 // Debug resampling ratio occasionally
379 static int log_counter = 0;
380 if (++log_counter % 600 == 0) {
381 LOG_DEBUG("AudioBackend",
382 "Resample trace: In=%d bytes (%dHz), Out=%d bytes (%dHz)", bytes_in,
383 stream_native_rate_, bytes_read, device_freq_);
384 }
385
386 return QueueSamples(stream_buffer_.data(), available_samples);
387}
388
390 AudioStatus status;
391
392 if (!initialized_)
393 return status;
394
395 status.is_playing =
396 (SDL_GetAudioDeviceStatus(device_id_) == SDL_AUDIO_PLAYING);
397 status.queued_bytes = SDL_GetQueuedAudioSize(device_id_);
398
399 // Calculate queued frames (each frame = channels * sample_size)
400 int bytes_per_frame =
402 status.queued_frames = status.queued_bytes / bytes_per_frame;
403
404 // Check for underrun (queue too low while playing)
405 if (status.is_playing && status.queued_frames < 100) {
406 status.has_underrun = true;
407 static int underrun_log = 0;
408 if (++underrun_log % 300 == 0) {
409 LOG_WARN("AudioBackend", "Audio underrun risk: queued_frames=%u (device=%u)",
410 status.queued_frames, device_id_);
411 }
412 }
413
414 return status;
415}
416
418 return initialized_;
419}
420
424
425void SDL2AudioBackend::SetAudioStreamResampling(bool enable, int native_rate,
426 int channels) {
427 if (!initialized_)
428 return;
429
430 if (!enable) {
431 if (audio_stream_) {
432 SDL_FreeAudioStream(audio_stream_);
433 audio_stream_ = nullptr;
434 }
435 audio_stream_enabled_ = false;
437 stream_buffer_.clear();
438 return;
439 }
440
441 const bool needs_recreate = (audio_stream_ == nullptr) ||
442 (stream_native_rate_ != native_rate) ||
443 (channels != config_.channels);
444
445 if (!needs_recreate) {
447 return;
448 }
449
450 if (audio_stream_) {
451 SDL_FreeAudioStream(audio_stream_);
452 audio_stream_ = nullptr;
453 }
454
456 SDL_NewAudioStream(AUDIO_S16, channels, native_rate, device_format_,
458 if (!audio_stream_) {
459 LOG_ERROR("AudioBackend", "SDL_NewAudioStream failed: %s", SDL_GetError());
460 audio_stream_enabled_ = false;
462 return;
463 }
464
465 LOG_INFO("AudioBackend", "SDL_AudioStream created: %dHz %dch -> %dHz %dch",
466 native_rate, channels, device_freq_, device_channels_);
467
468 SDL_AudioStreamClear(audio_stream_);
470 stream_native_rate_ = native_rate;
471 stream_buffer_.clear();
472}
473
474void SDL2AudioBackend::SetVolume(float volume) {
475 volume_ = std::clamp(volume, 0.0f, 1.0f);
476}
477
479 return volume_;
480}
481
485
486#endif // YAZE_USE_SDL3
487
488// ============================================================================
489// NullAudioBackend Implementation (for testing/headless)
490// ============================================================================
491
493 config_ = config;
494 initialized_ = true;
495 playing_ = false;
499 LOG_INFO("AudioBackend", "Null audio backend initialized (%dHz, %d channels)",
500 config.sample_rate, config.channels);
501 return true;
502}
503
505 initialized_ = false;
506 playing_ = false;
507 LOG_INFO("AudioBackend", "Null audio backend shut down");
508}
509
511 if (initialized_) playing_ = true;
512}
513
515 if (initialized_) playing_ = false;
516}
517
519 if (initialized_) {
520 playing_ = false;
522 }
523}
524
528
529bool NullAudioBackend::QueueSamples(const int16_t* samples, int num_samples) {
530 if (!initialized_ || !samples) return false;
531
532 total_queued_samples_ += num_samples;
533 current_queued_bytes_ += num_samples * sizeof(int16_t);
534 return true;
535}
536
537bool NullAudioBackend::QueueSamples(const float* samples, int num_samples) {
538 if (!initialized_ || !samples) return false;
539
540 total_queued_samples_ += num_samples;
541 current_queued_bytes_ += num_samples * sizeof(float);
542 return true;
543}
544
545bool NullAudioBackend::QueueSamplesNative(const int16_t* samples,
546 int frames_per_channel, int channels,
547 int native_rate) {
548 if (!initialized_ || !samples) return false;
549
550 // Track frames queued (for timing verification)
551 total_queued_frames_ += frames_per_channel;
552 total_queued_samples_ += frames_per_channel * channels;
553 current_queued_bytes_ += frames_per_channel * channels * sizeof(int16_t);
554
555 return true;
556}
557
559 AudioStatus status;
560 status.is_playing = playing_;
561 status.queued_bytes = static_cast<uint32_t>(current_queued_bytes_);
562 status.queued_frames = static_cast<uint32_t>(current_queued_bytes_ /
563 (config_.channels * sizeof(int16_t)));
564 status.has_underrun = false;
565 return status;
566}
567
569 return initialized_;
570}
571
575
576void NullAudioBackend::SetVolume(float volume) {
577 volume_ = std::clamp(volume, 0.0f, 1.0f);
578}
579
581 return volume_;
582}
583
584void NullAudioBackend::SetAudioStreamResampling(bool enable, int native_rate,
585 int channels) {
586 audio_stream_enabled_ = enable;
587 stream_native_rate_ = native_rate;
588 stream_channels_ = channels;
589 LOG_INFO("AudioBackend", "Null backend: resampling %s (%dHz -> %dHz)",
590 enable ? "enabled" : "disabled", native_rate, config_.sample_rate);
591}
592
596
602
603// ============================================================================
604// AudioBackendFactory Implementation
605// ============================================================================
606
607std::unique_ptr<IAudioBackend> AudioBackendFactory::Create(BackendType type) {
608 switch (type) {
610#ifdef YAZE_USE_SDL3
611 // Prefer SDL3 backend when SDL3 is in use.
612 return std::make_unique<SDL3AudioBackend>();
613#else
614 return std::make_unique<SDL2AudioBackend>();
615#endif
616
618#ifdef YAZE_USE_SDL3
619 return std::make_unique<SDL3AudioBackend>();
620#else
621 LOG_ERROR("AudioBackend", "SDL3 backend requested but not compiled with SDL3 support");
622 return std::make_unique<SDL2AudioBackend>();
623#endif
624
626#ifdef __EMSCRIPTEN__
627 return std::make_unique<WasmAudioBackend>();
628#else
629 LOG_ERROR("AudioBackend", "WASM backend requested but not compiled for Emscripten");
630 return std::make_unique<SDL2AudioBackend>();
631#endif
632
634 return std::make_unique<NullAudioBackend>();
635
636 default:
637 LOG_ERROR("AudioBackend", "Unknown backend type, using default backend");
638#ifdef YAZE_USE_SDL3
639 return std::make_unique<SDL3AudioBackend>();
640#else
641 return std::make_unique<SDL2AudioBackend>();
642#endif
643 }
644}
645
646} // namespace audio
647} // namespace emu
648} // namespace yaze
static std::unique_ptr< IAudioBackend > Create(BackendType type)
bool QueueSamplesNative(const int16_t *samples, int frames_per_channel, int channels, int native_rate) override
AudioStatus GetStatus() const override
void SetAudioStreamResampling(bool enable, int native_rate, int channels) override
bool IsAudioStreamEnabled() const override
AudioConfig GetConfig() const override
bool IsInitialized() const override
bool QueueSamples(const int16_t *samples, int num_samples) override
bool Initialize(const AudioConfig &config) override
void SetVolume(float volume) override
AudioConfig GetConfig() const override
bool IsAudioStreamEnabled() const override
void SetVolume(float volume) override
bool Initialize(const AudioConfig &config) override
bool IsInitialized() const override
std::vector< int16_t > stream_buffer_
AudioStatus GetStatus() const override
bool QueueSamplesNative(const int16_t *samples, int frames_per_channel, int channels, int native_rate) override
bool QueueSamples(const int16_t *samples, int num_samples) override
void SetAudioStreamResampling(bool enable, int native_rate, int channels) override
#define LOG_DEBUG(category, format,...)
Definition log.h:103
#define LOG_ERROR(category, format,...)
Definition log.h:109
#define LOG_WARN(category, format,...)
Definition log.h:107
#define LOG_INFO(category, format,...)
Definition log.h:105
SDL2/SDL3 compatibility layer.