From 5f8a864bf39e100df1fa4acdfb2eaea6d0a7c2f7 Mon Sep 17 00:00:00 2001 From: libretroadmin Date: Sun, 14 Jun 2026 15:09:04 +0000 Subject: [PATCH] sound: add core option to reduce DMC channel popping (fixes #638) Games like Castlevania II: Simon's Quest drive the DMC DAC directly via $4011 writes to produce sample-style audio (separate from the DPCM bit-stream path). The DAC jumps straight to the newly-written 7-bit value on every write, and those abrupt steps are audible as clicks/pops when the writes form a low-frequency envelope. Backport reference: negativeExponent's libretro-fceumm_next applies a one-step midpoint smoother at the $4011 write site - take the old DAC value and the new requested value, and store only the average (rounded toward the new value). Successive writes still converge on the target, so percussive samples retain their attack; they just lose half of the per-write step amplitude, which is the component that produces the click. Backported as-is, gated on a new core option `fceumm_reducedmcpopping` (default disabled, so the DAC trajectory is bit-exact with the original code when the option is not set). Verified to match _next's algebra exactly across the boundary cases (full-range jumps in both directions, single-step deltas, idle no-ops). The DPCM bit-decode playback path (the +/-2 per bit RawDALatch update further down in the DMC channel inner loop) is intentionally NOT touched - that path is the well-behaved bit-stream output, no popping, and smoothing it would attenuate normal samples. This addresses the second half of issue #638 (the first being the triangle ultrasonic gate, already landed as d3cd26e). The follow-up comment on that issue asked specifically for DMC popping reduction to land in libretro-fceumm since _next has no Android port. Build verified: sound.c -O2 / -O3 / aarch64 -O2 clean, libretro.c clean. --- src/driver.h | 1 + src/drivers/libretro/libretro.c | 9 ++++++++ src/drivers/libretro/libretro_core_options.h | 14 ++++++++++++ src/fceu.h | 12 ++++++++++ src/sound.c | 24 +++++++++++++++++++- 5 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/driver.h b/src/driver.h index b17376c..3498639 100644 --- a/src/driver.h +++ b/src/driver.h @@ -161,6 +161,7 @@ void FCEUI_CheatSearchSetCurrentAsOriginal(void); void FCEUI_SetLowPass(int q); void FCEUI_RemoveTriangleNoise(int d); +void FCEUI_ReduceDmcPopping(int d); void FCEUI_NSFSetVis(int mode); int FCEUI_NSFChange(int amount); diff --git a/src/drivers/libretro/libretro.c b/src/drivers/libretro/libretro.c index efeb8d8..fde6342 100644 --- a/src/drivers/libretro/libretro.c +++ b/src/drivers/libretro/libretro.c @@ -1574,6 +1574,7 @@ static bool update_option_visibility(void) "fceumm_sndquality", "fceumm_sndlowpass", "fceumm_removetrianglenoise", + "fceumm_reducedmcpopping", "fceumm_sndstereodelay", "fceumm_swapduty", "fceumm_apu_1", @@ -2400,6 +2401,14 @@ static void check_variables(bool startup) FCEUI_RemoveTriangleNoise(newval); } + var.key = "fceumm_reducedmcpopping"; + + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) + { + int newval = (!strcmp(var.value, "enabled")) ? 1 : 0; + FCEUI_ReduceDmcPopping(newval); + } + var.key = "fceumm_sndstereodelay"; if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) diff --git a/src/drivers/libretro/libretro_core_options.h b/src/drivers/libretro/libretro_core_options.h index ffceb68..e17ca26 100644 --- a/src/drivers/libretro/libretro_core_options.h +++ b/src/drivers/libretro/libretro_core_options.h @@ -335,6 +335,20 @@ struct retro_core_option_v2_definition option_defs[] = { }, "disabled", }, + { + "fceumm_reducedmcpopping", + "Reduce DMC Channel Popping", + NULL, + "Smooth direct writes to the DMC DAC register ($4011) by stepping the DAC only halfway toward each new value. Reduces the audible click when games (Castlevania II: Simon's Quest, etc.) pulse the DAC for manual sample playback. Normal DPCM bit-stream sample playback is unaffected.", + NULL, + "audio", + { + { "disabled", NULL }, + { "enabled", NULL }, + { NULL, NULL }, + }, + "disabled", + }, { "fceumm_sndstereodelay", "Stereo Sound Effect", diff --git a/src/fceu.h b/src/fceu.h index 4ee4f2d..4534588 100644 --- a/src/fceu.h +++ b/src/fceu.h @@ -104,6 +104,18 @@ typedef struct { * that behaviour in HQ. Default off to * preserve existing HQ output bit-exactly * when the option is not set. */ + int ReduceDMCPopping; /* Smooth direct writes to the DMC DAC + * register ($4011) by stepping the DAC + * only halfway from its previous value + * toward the newly written value, which + * attenuates the audible click games + * like Castlevania II generate when they + * pulse the DAC for manual sample + * playback. Normal DPCM bit-decode + * playback (the +/-2 per bit update) is + * NOT affected. Default off so the DAC + * trajectory is bit-exact when the + * option is not set. */ } FCEUS; extern FCEUS FSettings; diff --git a/src/sound.c b/src/sound.c index 97ffbd7..a0c15e7 100644 --- a/src/sound.c +++ b/src/sound.c @@ -313,7 +313,25 @@ static DECLFW(Write_DMCRegs) { DMCFormat = V; break; case 0x01: DoPCM(); - RawDALatch = V & 0x7F; + { + /* $4011 is the 7-bit DAC latch. Games like Castlevania II + * pulse this register directly to produce sample-style audio + * out-of-band from the DPCM bit-stream; the abrupt steps + * between successive writes are the audible "pop". When + * ReduceDMCPopping is on, take only the midpoint of the + * old-vs-new transition - i.e. step the DAC halfway toward + * the requested value rather than jumping straight to it. + * This is the same algorithm libretro-fceumm_next ships + * (backport reference: negativeExponent's fceumm_next). + * The DPCM playback path (the +/-2 per bit update further + * down) is left untouched. */ + uint8_t newval = V & 0x7F; + uint8_t lastval = RawDALatch; + RawDALatch = newval; + if (FSettings.ReduceDMCPopping) { + RawDALatch = (uint8_t)(newval - ((int)newval - (int)lastval) / 2); + } + } if (RawDALatch) DMC_7bit = 1; break; @@ -1261,6 +1279,10 @@ void FCEUI_RemoveTriangleNoise(int d) { FSettings.RemoveTriangleNoise = d ? 1 : 0; } +void FCEUI_ReduceDmcPopping(int d) { + FSettings.ReduceDMCPopping = d ? 1 : 0; +} + void FCEUI_SetSoundQuality(int quality) { FSettings.soundq = quality; SetSoundVariables();