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:
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace emu {
|
||||
|
||||
enum class InterpolationType {
|
||||
Linear,
|
||||
Hermite, // Used by bsnes/Snes9x - better quality than linear
|
||||
Cosine,
|
||||
Cubic,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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_) {
|
||||
|
||||
@@ -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", ¤t_item, items, IM_ARRAYSIZE(items))) {
|
||||
emu->snes().apu().dsp().interpolation_type =
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user