From d3cd26e922ce5cacb8a9d770eb791e712844241b Mon Sep 17 00:00:00 2001 From: libretroadmin Date: Sun, 14 Jun 2026 15:02:53 +0000 Subject: [PATCH] sound: add core option to mute ultrasonic triangle in HQ (fixes #638) Castlevania II, Jackal, and several other titles repeatedly drop the triangle channel into a period range that produces only ultrasonic output (raw period <= 3, > ~12 kHz at NTSC). The hardware itself plays these tones, but the DAC reconstruction filter in HQ mode folds the ultrasonic energy back into the audible range as popping. The LQ tri/noise/PCM mixer (RDoTriangleNoisePCMLQ) already gates the triangle channel with `freq[0] <= 4` (i.e. raw period <= 3) for exactly this reason -- the popping has been absent in LQ since forever. The HQ path (RDoTriangle) never had the equivalent gate, so users on Sound Quality = High/Very High hear the popping that LQ users do not. Mirror the gate in RDoTriangle, controlled by a new core option `fceumm_removetrianglenoise` (default disabled). When the option is off the codegen is identical to before -- gcc folds the `FSettings.RemoveTriangleNoise && (...)` check via short-circuit evaluation, the per-sample inner loop is untouched. When on, HQ behaves like LQ for the triangle ultrasonic case and the popping disappears. LQ is intentionally left untouched -- it has always silenced these tones unconditionally and changing that would be an audible behavior shift for existing LQ users. The new option is HQ-only. Backport reference: negativeExponent's libretro-fceumm_next fork (commit afaa8f5) added the same option, gating both LQ and HQ paths on it. This patch is the more conservative subset: only adds the HQ gate, leaves LQ at its current (always-silencing) behaviour. Build / codegen verified: - sound.c -O2 / -O3 clean, +15 lines in RDoTriangle (.text), inner per-sample for-loop unchanged. - libretro.c clean (options_list[] widened from [25] to [32] to fit the new key name). - aarch64 cross-build clean. --- src/driver.h | 1 + src/drivers/libretro/libretro.c | 11 ++++++++++- src/drivers/libretro/libretro_core_options.h | 14 ++++++++++++++ src/fceu.h | 10 ++++++++++ src/sound.c | 17 ++++++++++++++++- 5 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/driver.h b/src/driver.h index 8c2cda3..b17376c 100644 --- a/src/driver.h +++ b/src/driver.h @@ -160,6 +160,7 @@ void FCEUI_CheatSearchShowExcluded(void); void FCEUI_CheatSearchSetCurrentAsOriginal(void); void FCEUI_SetLowPass(int q); +void FCEUI_RemoveTriangleNoise(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 96dece6..efeb8d8 100644 --- a/src/drivers/libretro/libretro.c +++ b/src/drivers/libretro/libretro.c @@ -1569,10 +1569,11 @@ static bool update_option_visibility(void) struct retro_core_option_display option_display; unsigned i; unsigned size; - char options_list[][25] = { + char options_list[][32] = { "fceumm_sndvolume", "fceumm_sndquality", "fceumm_sndlowpass", + "fceumm_removetrianglenoise", "fceumm_sndstereodelay", "fceumm_swapduty", "fceumm_apu_1", @@ -2391,6 +2392,14 @@ static void check_variables(bool startup) FCEUI_SetLowPass(lowpass); } + var.key = "fceumm_removetrianglenoise"; + + if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) + { + int newval = (!strcmp(var.value, "enabled")) ? 1 : 0; + FCEUI_RemoveTriangleNoise(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 a324d92..ffceb68 100644 --- a/src/drivers/libretro/libretro_core_options.h +++ b/src/drivers/libretro/libretro_core_options.h @@ -321,6 +321,20 @@ struct retro_core_option_v2_definition option_defs[] = { }, "disabled", }, + { + "fceumm_removetrianglenoise", + "Reduce Triangle Channel Popping", + NULL, + "Mute the triangle channel when its period drops into the ultrasonic range (period <= 3, > ~12 kHz at NTSC). Eliminates the audible popping some games (Castlevania II: Simon's Quest, Jackal, etc.) produce in High / Very High sound quality, where the DAC reconstruction filter would otherwise fold the ultrasonic content back into the audible range. Low quality is unaffected (it already silences ultrasonic triangle output).", + 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 74c9026..4ee4f2d 100644 --- a/src/fceu.h +++ b/src/fceu.h @@ -94,6 +94,16 @@ typedef struct { uint32_t SndRate; int soundq; int lowpass; + int RemoveTriangleNoise; /* Mute triangle channel when its period + * is low enough to produce only ultrasonic + * output (period <= 3, > ~12 kHz at NTSC), + * which the DAC reconstruction filter + * folds back as audible popping in HQ + * mode. LQ already silences these + * unconditionally; this option mirrors + * that behaviour in HQ. Default off to + * preserve existing HQ output bit-exactly + * when the option is not set. */ } FCEUS; extern FCEUS FSettings; diff --git a/src/sound.c b/src/sound.c index e1c6438..97ffbd7 100644 --- a/src/sound.c +++ b/src/sound.c @@ -733,10 +733,21 @@ static void RDoSQLQ(void) { static void RDoTriangle(void) { uint32_t V; int32_t tcout = (tristep & 0xF); + uint32_t triangle_raw_period = (PSG[0xa] | ((PSG[0xb] & 7) << 8)); if (!(tristep & 0x10)) tcout ^= 0xF; tcout = (tcout * 3) << 16; /* (tcout<<1); */ - if (!lengthcount[2] || !TriCount) { /* Counter is halted, but we still need to output. */ + /* The LQ tri/noise/PCM mixer (RDoTriangleNoisePCMLQ below) already + * forces the triangle channel quiet when its period is low enough + * to produce only ultrasonic output - games like Castlevania II + * and Jackal repeatedly drop the triangle into that range when + * they want silence, and without the gate the DAC reconstruction + * filter folds the high-frequency content back as audible + * popping. Mirror the gate in the HQ path, conditional on the + * RemoveTriangleNoise option so the default HQ output stays + * bit-exact with the original code unless the user opts in. */ + if (!lengthcount[2] || !TriCount + || (FSettings.RemoveTriangleNoise && triangle_raw_period <= 3)) { /* Counter is halted, but we still need to output. */ int32_t *start = &WaveHi[ChannelBC[2]]; int32_t count = SOUNDTS - ChannelBC[2]; while (count--) { @@ -1246,6 +1257,10 @@ void FCEUI_SetLowPass(int q) { FSettings.lowpass = q; } +void FCEUI_RemoveTriangleNoise(int d) { + FSettings.RemoveTriangleNoise = d ? 1 : 0; +} + void FCEUI_SetSoundQuality(int quality) { FSettings.soundq = quality; SetSoundVariables();