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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user