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