refactor(audio): enhance audio buffering and interpolation methods

- Updated audio buffering strategy to ensure smooth playback by always queuing samples and implementing dynamic rate control.
- Introduced Hermite interpolation for audio resampling, providing better quality than linear interpolation.
- Adjusted audio debug logging for improved monitoring of audio states and buffer management.

Benefits:
- Improved audio playback quality and reduced glitches.
- Enhanced clarity in audio debugging and monitoring processes.
This commit is contained in:
scawful
2025-10-11 23:47:17 -04:00
parent be4d30b208
commit 0808c52788
7 changed files with 142 additions and 73 deletions

View File

@@ -648,34 +648,43 @@ inline int16_t InterpolateLinear(int16_t s0, int16_t s1, double frac) {
return static_cast<int16_t>(s0 + frac * (s1 - s0));
}
// Helper for Hermite interpolation (used by bsnes/Snes9x)
// Provides smoother interpolation than linear with minimal overhead
inline int16_t InterpolateHermite(int16_t p0, int16_t p1, int16_t p2, int16_t p3, double t) {
const double c0 = p1;
const double c1 = (p2 - p0) * 0.5;
const double c2 = p0 - 2.5 * p1 + 2.0 * p2 - 0.5 * p3;
const double c3 = (p3 - p0) * 0.5 + 1.5 * (p1 - p2);
const double result = c0 + c1 * t + c2 * t * t + c3 * t * t * t;
// Clamp to 16-bit range
return result > 32767.0 ? 32767
: (result < -32768.0 ? -32768
: static_cast<int16_t>(result));
}
void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
bool pal_timing) {
// Resample from native samples-per-frame (NTSC: ~534, PAL: ~641)
const double native_per_frame = pal_timing ? 641.0 : 534.0;
const double step = native_per_frame / static_cast<double>(samples_per_frame);
// Start reading one native frame behind the frame boundary
double location = static_cast<double>((lastFrameBoundary + 0x400) & 0x3ff);
location -= native_per_frame;
// Ensure location is within valid range
while (location < 0) location += 0x400;
for (int i = 0; i < samples_per_frame; i++) {
const int idx = static_cast<int>(location);
const double frac = location - idx;
const int idx = static_cast<int>(location) & 0x3ff;
const double frac = location - static_cast<int>(location);
switch (interpolation_type) {
case InterpolationType::Linear: {
// const int next_idx = (idx + 1) & 0x3ff;
// const int16_t s0_l = sampleBuffer[(idx * 2) + 0];
// const int16_t s1_l = sampleBuffer[(next_idx * 2) + 0];
// sample_data[(i * 2) + 0] = InterpolateLinear(s0_l, s1_l, frac);
// const int16_t s0_r = sampleBuffer[(idx * 2) + 1];
// const int16_t s1_r = sampleBuffer[(next_idx * 2) + 1];
// sample_data[(i * 2) + 1] = InterpolateLinear(s0_r, s1_r, frac);
const int idx = static_cast<int>(location) & 0x3ff;
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];
@@ -687,9 +696,25 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
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;
break;
}
case InterpolationType::Hermite: {
const int idx0 = (idx - 1 + 0x400) & 0x3ff;
const int idx1 = idx & 0x3ff;
const int idx2 = (idx + 1) & 0x3ff;
const int idx3 = (idx + 2) & 0x3ff;
// Left channel
const int16_t p0_l = sampleBuffer[(idx0 * 2) + 0];
const int16_t p1_l = sampleBuffer[(idx1 * 2) + 0];
const int16_t p2_l = sampleBuffer[(idx2 * 2) + 0];
const int16_t p3_l = sampleBuffer[(idx3 * 2) + 0];
sample_data[(i * 2) + 0] = InterpolateHermite(p0_l, p1_l, p2_l, p3_l, frac);
// Right channel
const int16_t p0_r = sampleBuffer[(idx0 * 2) + 1];
const int16_t p1_r = sampleBuffer[(idx1 * 2) + 1];
const int16_t p2_r = sampleBuffer[(idx2 * 2) + 1];
const int16_t p3_r = sampleBuffer[(idx3 * 2) + 1];
sample_data[(i * 2) + 1] = InterpolateHermite(p0_r, p1_r, p2_r, p3_r, frac);
break;
}
case InterpolationType::Cosine: {
@@ -725,7 +750,6 @@ void Dsp::GetSamples(int16_t* sample_data, int samples_per_frame,
}
}
location += step;
}
}

View File

@@ -9,6 +9,7 @@ namespace emu {
enum class InterpolationType {
Linear,
Hermite, // Used by bsnes/Snes9x - better quality than linear
Cosine,
Cubic,
};

View File

@@ -280,61 +280,47 @@ void Emulator::Run(Rom* rom) {
// Only render and handle audio on the last frame
if (should_render) {
// ADAPTIVE AUDIO BUFFERING
// Target: Keep 2-3 frames worth of audio queued for smooth playback
// This prevents both underruns (crackling) and excessive latency
// SMOOTH AUDIO BUFFERING
// Strategy: Always queue samples, never drop. Use dynamic rate control
// to keep buffer at target level. This prevents pops and glitches.
if (audio_backend_) {
auto audio_status = audio_backend_->GetStatus();
uint32_t queued_frames = audio_status.queued_frames;
// 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;
// Synchronize DSP frame boundary for proper resampling
snes_.apu().dsp().NewFrame();
bool should_queue = (queued_frames < max_buffer);
// Target buffer: 2.0 frames for low latency with safety margin
// This is similar to how bsnes/Mesen handle audio buffering
const uint32_t target_buffer = wanted_samples_ * 2;
const uint32_t min_buffer = wanted_samples_;
const uint32_t max_buffer = wanted_samples_ * 4;
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%
}
}
// Generate samples from SNES APU/DSP
snes_.SetSamples(audio_buffer_, wanted_samples_);
// CRITICAL: Always queue all generated samples - never drop
// Dropping samples causes audible pops and glitches
int num_samples = wanted_samples_ * 2; // Stereo (L+R channels)
// Only skip queueing if buffer is dangerously full (>4 frames)
// This prevents unbounded buffer growth but is rare in practice
if (queued_frames < max_buffer) {
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");
} else {
// Buffer overflow - skip this frame's audio
// This should rarely happen with proper timing
static int overflow_count = 0;
if (++overflow_count % 60 == 0) {
LOG_WARN("Emulator", "Audio buffer overflow (count: %d, queued: %u)",
overflow_count, queued_frames);
}
}
}
@@ -590,7 +576,7 @@ void Emulator::RenderModernCpuDebugger() {
ImGui::TextColored(ConvertColorToImVec4(theme.accent), "CPU Status");
ImGui::PushStyleColor(ImGuiCol_ChildBg,
ConvertColorToImVec4(theme.child_bg));
ImGui::BeginChild("##CpuStatus", ImVec2(0, 200), true);
ImGui::BeginChild("##CpuStatus", ImVec2(0, 180), true);
// Compact register display in a table
if (ImGui::BeginTable(
@@ -672,7 +658,7 @@ void Emulator::RenderModernCpuDebugger() {
ImGui::TextColored(ConvertColorToImVec4(theme.accent), "SPC700 Status");
ImGui::PushStyleColor(ImGuiCol_ChildBg,
ConvertColorToImVec4(theme.child_bg));
ImGui::BeginChild("##SpcStatus", ImVec2(0, 160), true);
ImGui::BeginChild("##SpcStatus", ImVec2(0, 150), true);
if (ImGui::BeginTable(
"SPCRegisters", 4,

View File

@@ -53,11 +53,18 @@ void InputManager::Poll(Snes* snes, int player) {
ControllerState final_state;
final_state.buttons = physical_state.buttons | agent_controller_state_.buttons;
// Update ALL button states every frame to ensure proper press/release
// This is critical for games that check button state every frame
// Apply button state directly to SNES
// Just send the raw button state on every Poll() call
// The button state will be latched by HandleInput() at VBlank
for (int i = 0; i < 12; i++) {
bool pressed = (final_state.buttons & (1 << i)) != 0;
snes->SetButtonState(player, i, pressed);
bool button_held = (final_state.buttons & (1 << i)) != 0;
snes->SetButtonState(player, i, button_held);
}
// Debug: Log complete button state when any button is pressed
static int poll_log_count = 0;
if (final_state.buttons != 0 && poll_log_count++ < 30) {
LOG_DEBUG("InputManager", "Poll: buttons=0x%04X", final_state.buttons);
}
}

View File

@@ -135,7 +135,17 @@ void Snes::CatchUpApu() {
}
void Snes::HandleInput() {
// IMPORTANT: Clear and repopulate auto-read data
// This data persists until the next call, allowing NMI to read it
memset(port_auto_read_, 0, sizeof(port_auto_read_));
// Debug: Log input state when A button is active
static int debug_count = 0;
if ((input1.current_state_ & 0x0100) != 0 && debug_count++ < 30) {
LOG_DEBUG("SNES", "HandleInput: current_state=0x%04X auto_joy_read_=%d (A button active)",
input1.current_state_, auto_joy_read_ ? 1 : 0);
}
// latch controllers
input_latch(&input1, true);
input_latch(&input2, true);
@@ -143,12 +153,21 @@ void Snes::HandleInput() {
input_latch(&input2, false);
for (int i = 0; i < 16; i++) {
uint8_t val = input_read(&input1);
port_auto_read_[0] |= ((val & 1) << (15 - i));
port_auto_read_[0] |= ((val & 1) << (15 - i)); // Bits are read LSB first, stored MSB first
port_auto_read_[2] |= (((val >> 1) & 1) << (15 - i));
val = input_read(&input2);
port_auto_read_[1] |= ((val & 1) << (15 - i));
port_auto_read_[3] |= (((val >> 1) & 1) << (15 - i));
}
// Debug: Log auto-read result when A button was active
static int debug_result_count = 0;
if ((input1.current_state_ & 0x0100) != 0) {
if (debug_result_count++ < 30) {
LOG_DEBUG("SNES", "HandleInput END: current_state=0x%04X, port_auto_read[0]=0x%04X (A button status)",
input1.current_state_, port_auto_read_[0]);
}
}
}
void Snes::RunCycle() {
@@ -265,6 +284,13 @@ void Snes::RunCycle() {
// TODO: this starts a little after start of vblank
auto_joy_timer_ = 4224;
HandleInput();
// Debug: Log that we populated auto-read data BEFORE NMI
static int handle_input_log = 0;
if (handle_input_log++ < 50 && port_auto_read_[0] != 0) {
LOG_DEBUG("SNES", ">>> VBLANK: HandleInput() done, port_auto_read[0]=0x%04X, about to call Nmi() <<<",
port_auto_read_[0]);
}
}
static int nmi_log_count = 0;
if (nmi_log_count++ < 10) {
@@ -369,13 +395,27 @@ uint8_t Snes::ReadReg(uint16_t adr) {
case 0x421a:
case 0x421c:
case 0x421e: {
return port_auto_read_[(adr - 0x4218) / 2] & 0xff;
uint8_t result = port_auto_read_[(adr - 0x4218) / 2] & 0xff;
// Debug: Log reads when port_auto_read has data (non-zero)
static int read_count = 0;
if (adr == 0x4218 && port_auto_read_[0] != 0 && read_count++ < 200) {
LOG_DEBUG("SNES", ">>> Game read $4218 = $%02X (port_auto_read[0]=$%04X, current=$%04X) at PC=$%02X:%04X <<<",
result, port_auto_read_[0], input1.current_state_, cpu_.PB, cpu_.PC);
}
return result;
}
case 0x4219:
case 0x421b:
case 0x421d:
case 0x421f: {
return port_auto_read_[(adr - 0x4219) / 2] >> 8;
uint8_t result = port_auto_read_[(adr - 0x4219) / 2] >> 8;
// Debug: Log reads when port_auto_read has data (non-zero)
static int read_count = 0;
if (adr == 0x4219 && port_auto_read_[0] != 0 && read_count++ < 200) {
LOG_DEBUG("SNES", ">>> Game read $4219 = $%02X (port_auto_read[0]=$%04X, current=$%04X) at PC=$%02X:%04X <<<",
result, port_auto_read_[0], input1.current_state_, cpu_.PB, cpu_.PC);
}
return result;
}
default: {
return memory_.open_bus();
@@ -404,6 +444,12 @@ uint8_t Snes::Rread(uint32_t adr) {
return input_read(&input2) | (memory_.open_bus() & 0xe0) | 0x1c;
}
if (adr >= 0x4200 && adr < 0x4220) {
// Debug: Log ANY reads to $4218/$4219 BEFORE calling ReadReg
static int rread_count = 0;
if ((adr == 0x4218 || adr == 0x4219) && rread_count++ < 100) {
LOG_DEBUG("SNES", ">>> Rread($%04X) from bank=$%02X PC=$%04X - calling ReadReg <<<",
adr, bank, cpu_.PC);
}
return ReadReg(adr); // internal registers
}
if (adr >= 0x4300 && adr < 0x4380) {
@@ -478,6 +524,15 @@ void Snes::WriteReg(uint16_t adr, uint8_t val) {
auto_joy_read_ = val & 0x1;
if (!auto_joy_read_) auto_joy_timer_ = 0;
// Debug: Log when auto-joy-read is enabled/disabled
static int auto_joy_log = 0;
static bool last_auto_joy = false;
if (auto_joy_read_ != last_auto_joy && auto_joy_log++ < 10) {
LOG_DEBUG("SNES", ">>> AUTO-JOY-READ %s at PC=$%02X:%04X <<<",
auto_joy_read_ ? "ENABLED" : "DISABLED", cpu_.PB, cpu_.PC);
last_auto_joy = auto_joy_read_;
}
h_irq_enabled_ = val & 0x10;
v_irq_enabled_ = val & 0x20;
if (!h_irq_enabled_ && !v_irq_enabled_) {

View File

@@ -536,7 +536,7 @@ void RenderApuDebugger(Emulator* emu) {
ImGui::Text("Audio Resampling");
// Combo box for interpolation type
const char* items[] = {"Linear", "Cosine", "Cubic"};
const char* items[] = {"Linear", "Hermite", "Cosine", "Cubic"};
int current_item = static_cast<int>(emu->snes().apu().dsp().interpolation_type);
if (ImGui::Combo("Interpolation", &current_item, items, IM_ARRAYSIZE(items))) {
emu->snes().apu().dsp().interpolation_type =

View File

@@ -43,10 +43,6 @@ void PerformanceProfiler::EndTimer(const std::string& operation_name) {
auto timer_iter = active_timers_.find(operation_name);
if (timer_iter == active_timers_.end()) {
// During shutdown, silently ignore missing timers to avoid log spam
if (!is_shutting_down_) {
LOG_DEBUG("PerformanceProfiler", "EndTimer called for operation '%s' that was not started",
operation_name.c_str());
}
return;
}