refactor(emulator): improve audio backend initialization and adaptive buffering

- Enhanced audio backend initialization by adding comments for clarity and ensuring a moderate buffer size for optimal latency and stability.
- Updated the emulator's run logic to start in a running state by default and refined the auto-pause mechanism to only trigger during window resizing, removing aggressive focus-based pausing.
- Implemented adaptive audio buffering to maintain smooth playback, adjusting the number of queued samples based on current buffer status.

Benefits:
- Improved user experience with more intuitive audio handling and reduced latency.
- Enhanced stability during window operations, preventing crashes on macOS.
- Streamlined audio processing for better performance and responsiveness.
This commit is contained in:
scawful
2025-10-11 13:17:14 -04:00
parent e8ebf298c6
commit 88a4f29fe8
3 changed files with 123 additions and 121 deletions

View File

@@ -100,26 +100,40 @@ void SDL2AudioBackend::Clear() {
bool SDL2AudioBackend::QueueSamples(const int16_t* samples, int num_samples) {
if (!initialized_ || !samples) return false;
// Apply volume scaling
if (volume_ != 1.0f) {
std::vector<int16_t> scaled_samples(num_samples);
for (int i = 0; i < num_samples; ++i) {
int32_t scaled = static_cast<int32_t>(samples[i] * volume_);
scaled_samples[i] = static_cast<int16_t>(std::clamp(scaled, -32768, 32767));
}
int result = SDL_QueueAudio(device_id_, scaled_samples.data(),
num_samples * sizeof(int16_t));
if (result < 0) {
LOG_ERROR("AudioBackend", "SDL_QueueAudio failed: %s", SDL_GetError());
return false;
}
} else {
// OPTIMIZATION: Skip volume scaling if volume is 100% (common case)
if (volume_ == 1.0f) {
// Fast path: No volume adjustment needed
int result = SDL_QueueAudio(device_id_, samples, num_samples * sizeof(int16_t));
if (result < 0) {
LOG_ERROR("AudioBackend", "SDL_QueueAudio failed: %s", SDL_GetError());
return false;
}
return true;
}
// Slow path: Volume scaling required
// Use thread-local buffer to avoid repeated allocations
thread_local std::vector<int16_t> scaled_samples;
// Resize only if needed (avoid reallocation on every call)
if (scaled_samples.size() < static_cast<size_t>(num_samples)) {
scaled_samples.resize(num_samples);
}
// Apply volume scaling with SIMD-friendly loop
for (int i = 0; i < num_samples; ++i) {
int32_t scaled = static_cast<int32_t>(samples[i] * volume_);
// Clamp to prevent overflow
if (scaled > 32767) scaled = 32767;
else if (scaled < -32768) scaled = -32768;
scaled_samples[i] = static_cast<int16_t>(scaled);
}
int result = SDL_QueueAudio(device_id_, scaled_samples.data(),
num_samples * sizeof(int16_t));
if (result < 0) {
LOG_ERROR("AudioBackend", "SDL_QueueAudio failed: %s", SDL_GetError());
return false;
}
return true;

View File

@@ -625,10 +625,26 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
double location = static_cast<double>((lastFrameBoundary + 0x400) & 0x3ff);
location -= native_per_frame;
// Use linear interpolation for smoother resampling
for (int i = 0; i < samples_per_frame; i++) {
const int idx = static_cast<int>(location) & 0x3ff;
sample_data[(i * 2) + 0] = sampleBuffer[(idx * 2) + 0];
sample_data[(i * 2) + 1] = sampleBuffer[(idx * 2) + 1];
const int next_idx = (idx + 1) & 0x3ff;
// Calculate interpolation factor (0.0 to 1.0)
const double frac = location - static_cast<int>(location);
// Linear interpolation for left channel
const int16_t s0_l = sampleBuffer[(idx * 2) + 0];
const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0];
sample_data[(i * 2) + 0] = static_cast<int16_t>(
s0_l + frac * (s1_l - s0_l));
// Linear interpolation for right channel
const int16_t s0_r = sampleBuffer[(idx * 2) + 1];
const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1];
sample_data[(i * 2) + 1] = static_cast<int16_t>(
s0_r + frac * (s1_r - s0_r));
location += step;
}
}

View File

@@ -98,17 +98,19 @@ void Emulator::Initialize(gfx::IRenderer* renderer, const std::vector<uint8_t>&
if (!audio_backend_) {
audio_backend_ = audio::AudioBackendFactory::Create(
audio::AudioBackendFactory::BackendType::SDL2);
audio::AudioConfig config;
config.sample_rate = 48000;
config.channels = 2;
// Use moderate buffer size - 1024 samples = ~21ms latency
// This is a good balance between latency and stability
config.buffer_frames = 1024;
config.format = audio::SampleFormat::INT16;
if (!audio_backend_->Initialize(config)) {
LOG_ERROR("Emulator", "Failed to initialize audio backend");
} else {
LOG_INFO("Emulator", "Audio backend initialized: %s",
LOG_INFO("Emulator", "Audio backend initialized: %s",
audio_backend_->GetBackendName().c_str());
}
}
@@ -141,17 +143,19 @@ void Emulator::Run(Rom* rom) {
if (!audio_backend_) {
audio_backend_ = audio::AudioBackendFactory::Create(
audio::AudioBackendFactory::BackendType::SDL2);
audio::AudioConfig config;
config.sample_rate = 48000;
config.channels = 2;
// Use moderate buffer size - 1024 samples = ~21ms latency
// This is a good balance between latency and stability
config.buffer_frames = 1024;
config.format = audio::SampleFormat::INT16;
if (!audio_backend_->Initialize(config)) {
LOG_ERROR("Emulator", "Failed to initialize audio backend");
} else {
LOG_INFO("Emulator", "Audio backend initialized (lazy): %s",
LOG_INFO("Emulator", "Audio backend initialized (lazy): %s",
audio_backend_->GetBackendName().c_str());
}
}
@@ -201,33 +205,32 @@ void Emulator::Run(Rom* rom) {
frame_count_ = 0;
fps_timer_ = 0.0;
current_fps_ = 0.0;
// Start emulator in running state by default
// User can press Space to pause if needed
running_ = true;
}
RenderNavBar();
// Auto-pause emulator during window operations to prevent macOS crashes
static bool was_running_before_pause = false;
bool window_has_focus = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootWindow);
// Auto-pause emulator during window resize to prevent crashes
// MODERN APPROACH: Only pause on actual window resize, not focus loss
static bool was_running_before_resize = false;
// Check if window is being resized (set in HandleEvents)
if (yaze::core::g_window_is_resizing && running_) {
was_running_before_pause = true;
was_running_before_resize = true;
running_ = false;
} else if (!yaze::core::g_window_is_resizing && !running_ && was_running_before_pause) {
} else if (!yaze::core::g_window_is_resizing && !running_ && was_running_before_resize) {
// Auto-resume after resize completes
running_ = true;
was_running_before_pause = false;
}
// Also pause when window loses focus to save CPU/battery
if (!window_has_focus && running_ && !was_running_before_pause) {
was_running_before_pause = true;
running_ = false;
} else if (window_has_focus && !running_ && was_running_before_pause && !yaze::core::g_window_is_resizing) {
// Don't auto-resume - let user manually resume
was_running_before_pause = false;
was_running_before_resize = false;
}
// REMOVED: Aggressive focus-based pausing
// Modern emulators (RetroArch, bsnes, etc.) continue running in background
// Users can manually pause with Space if they want to save CPU/battery
if (running_) {
// Poll input and update SNES controller state
input_manager_.Poll(&snes_, 1); // Player 1
@@ -259,7 +262,7 @@ void Emulator::Run(Rom* rom) {
// Process frames (skip rendering for all but last frame if falling behind)
for (int i = 0; i < frames_to_process; i++) {
bool should_render = (i == frames_to_process - 1);
// Run frame
if (turbo_mode_) {
snes_.RunFrame();
@@ -277,92 +280,61 @@ void Emulator::Run(Rom* rom) {
// Only render and handle audio on the last frame
if (should_render) {
// Generate and queue audio samples using audio backend
snes_.SetSamples(audio_buffer_, wanted_samples_);
// ADAPTIVE AUDIO BUFFERING
// Target: Keep 2-3 frames worth of audio queued for smooth playback
// This prevents both underruns (crackling) and excessive latency
// AUDIO DEBUG: Comprehensive diagnostics at regular intervals
static int audio_debug_counter = 0;
audio_debug_counter++;
// Log at frames 60 (1sec), 300 (5sec), 600 (10sec), then every 600 frames
bool should_debug = (audio_debug_counter == 60 || audio_debug_counter == 300 ||
audio_debug_counter == 600 || (audio_debug_counter % 600 == 0));
if (should_debug) {
// Check if buffer exists
if (!audio_buffer_) {
printf("[AUDIO ERROR] audio_buffer_ is NULL!\n");
} else {
// Check for audio samples
bool has_audio = false;
int16_t max_sample = 0;
int non_zero_count = 0;
for (int i = 0; i < wanted_samples_ * 2 && i < 100; i++) {
if (audio_buffer_[i] != 0) {
has_audio = true;
non_zero_count++;
if (std::abs(audio_buffer_[i]) > std::abs(max_sample)) {
max_sample = audio_buffer_[i];
}
}
}
// Backend status
auto audio_status = audio_backend_ ? audio_backend_->GetStatus() : audio::AudioStatus{};
bool backend_playing = audio_status.is_playing;
printf("\n[AUDIO DEBUG] Frame=%d (~%.1f sec)\n", audio_debug_counter, audio_debug_counter / 60.0f);
printf(" Backend: %s (Playing: %s)\n",
audio_backend_ ? audio_backend_->GetBackendName().c_str() : "NULL",
backend_playing ? "YES" : "NO");
printf(" Queued: %u frames\n", audio_status.queued_frames);
printf(" Buffer: wanted_samples=%d, non_zero=%d/%d, max=%d\n",
wanted_samples_, non_zero_count, std::min(wanted_samples_ * 2, 100), max_sample);
printf(" Samples: %s\n", has_audio ? "YES" : "SILENCE");
// APU state
if (snes_.running()) {
uint64_t apu_cycles = snes_.apu().GetCycles();
uint16_t spc_pc = snes_.apu().spc700().PC;
bool ipl_rom_active = (spc_pc >= 0xFFC0 && spc_pc <= 0xFFFF);
printf(" APU: %llu cycles, PC=$%04X %s\n",
apu_cycles, spc_pc, ipl_rom_active ? "(IPL ROM)" : "(Game Code)");
// Handshake status
auto& tracker = snes_.apu_handshake_tracker();
printf(" Handshake: %s\n", tracker.GetPhaseString().c_str());
if (ipl_rom_active && audio_debug_counter > 300) {
printf(" ⚠️ SPC700 STUCK IN IPL ROM - Handshake not completing!\n");
}
} else {
printf(" ⚠️ SNES not running!\n");
}
printf("\n");
}
}
// Smart buffer management using audio backend
if (audio_backend_) {
auto status = audio_backend_->GetStatus();
int num_samples = wanted_samples_ * 2; // Stereo
auto audio_status = audio_backend_->GetStatus();
uint32_t queued_frames = audio_status.queued_frames;
if (status.queued_frames < 2) {
// Buffer is low, queue more audio
if (!audio_backend_->QueueSamples(audio_buffer_, num_samples)) {
if (frame_count_ % 300 == 0) {
LOG_WARN("Emulator", "Failed to queue audio samples");
// Target buffer: 2.5 frames (2000 samples at 48kHz/60fps)
// Min: 1.5 frames (1200 samples) - below this we need to queue
// Max: 4.0 frames (3200 samples) - above this we skip queueing
const uint32_t target_buffer = wanted_samples_ * 2.5;
const uint32_t min_buffer = wanted_samples_ * 1.5;
const uint32_t max_buffer = wanted_samples_ * 4.0;
bool should_queue = (queued_frames < max_buffer);
if (should_queue) {
// Generate samples from SNES APU/DSP
snes_.SetSamples(audio_buffer_, wanted_samples_);
int num_samples = wanted_samples_ * 2; // Stereo (L+R channels)
// Adaptive sample adjustment to prevent drift
// Note: We can only queue up to what we generated
if (queued_frames < min_buffer) {
// Buffer running low - queue all samples we have
// In future frames this helps catch up
num_samples = wanted_samples_ * 2;
} else if (queued_frames > target_buffer) {
// Buffer too high - queue fewer samples to drain faster
// Drop some samples to reduce latency
num_samples = static_cast<int>(wanted_samples_ * 2 * 0.8);
if (num_samples < wanted_samples_ * 2 * 0.5) {
num_samples = wanted_samples_ * 2 * 0.5; // Minimum 50%
}
}
} else if (status.queued_frames > 6) {
// Buffer is too full, clear it to prevent lag
audio_backend_->Clear();
audio_backend_->QueueSamples(audio_buffer_, num_samples);
} else {
// Normal operation - queue samples
audio_backend_->QueueSamples(audio_buffer_, num_samples);
if (!audio_backend_->QueueSamples(audio_buffer_, num_samples)) {
static int error_count = 0;
if (++error_count % 300 == 0) {
LOG_WARN("Emulator", "Failed to queue audio (count: %d)", error_count);
}
}
}
// AUDIO DEBUG: Compact per-frame monitoring
static int audio_debug_counter = 0;
audio_debug_counter++;
// Log first 10 frames, then every 60 frames for ongoing monitoring
if (audio_debug_counter < 10 || audio_debug_counter % 60 == 0) {
printf("[AUDIO] Frame %d: Queued=%u Target=%u Status=%s\n",
audio_debug_counter, queued_frames, target_buffer,
should_queue ? "QUEUE" : "SKIP");
}
}