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.
This commit is contained in:
libretroadmin
2026-06-14 15:02:53 +00:00
committed by U-DESKTOP-SPFP6AQ\twistedtechre
parent 6911042cf6
commit d3cd26e922
5 changed files with 51 additions and 2 deletions

View File

@@ -160,6 +160,7 @@ void FCEUI_CheatSearchShowExcluded(void);
void FCEUI_CheatSearchSetCurrentAsOriginal(void); void FCEUI_CheatSearchSetCurrentAsOriginal(void);
void FCEUI_SetLowPass(int q); void FCEUI_SetLowPass(int q);
void FCEUI_RemoveTriangleNoise(int d);
void FCEUI_NSFSetVis(int mode); void FCEUI_NSFSetVis(int mode);
int FCEUI_NSFChange(int amount); int FCEUI_NSFChange(int amount);

View File

@@ -1569,10 +1569,11 @@ static bool update_option_visibility(void)
struct retro_core_option_display option_display; struct retro_core_option_display option_display;
unsigned i; unsigned i;
unsigned size; unsigned size;
char options_list[][25] = { char options_list[][32] = {
"fceumm_sndvolume", "fceumm_sndvolume",
"fceumm_sndquality", "fceumm_sndquality",
"fceumm_sndlowpass", "fceumm_sndlowpass",
"fceumm_removetrianglenoise",
"fceumm_sndstereodelay", "fceumm_sndstereodelay",
"fceumm_swapduty", "fceumm_swapduty",
"fceumm_apu_1", "fceumm_apu_1",
@@ -2391,6 +2392,14 @@ static void check_variables(bool startup)
FCEUI_SetLowPass(lowpass); 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"; var.key = "fceumm_sndstereodelay";
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value) if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)

View File

@@ -321,6 +321,20 @@ struct retro_core_option_v2_definition option_defs[] = {
}, },
"disabled", "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", "fceumm_sndstereodelay",
"Stereo Sound Effect", "Stereo Sound Effect",

View File

@@ -94,6 +94,16 @@ typedef struct {
uint32_t SndRate; uint32_t SndRate;
int soundq; int soundq;
int lowpass; 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; } FCEUS;
extern FCEUS FSettings; extern FCEUS FSettings;

View File

@@ -733,10 +733,21 @@ static void RDoSQLQ(void) {
static void RDoTriangle(void) { static void RDoTriangle(void) {
uint32_t V; uint32_t V;
int32_t tcout = (tristep & 0xF); int32_t tcout = (tristep & 0xF);
uint32_t triangle_raw_period = (PSG[0xa] | ((PSG[0xb] & 7) << 8));
if (!(tristep & 0x10)) tcout ^= 0xF; if (!(tristep & 0x10)) tcout ^= 0xF;
tcout = (tcout * 3) << 16; /* (tcout<<1); */ 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 *start = &WaveHi[ChannelBC[2]];
int32_t count = SOUNDTS - ChannelBC[2]; int32_t count = SOUNDTS - ChannelBC[2];
while (count--) { while (count--) {
@@ -1246,6 +1257,10 @@ void FCEUI_SetLowPass(int q) {
FSettings.lowpass = q; FSettings.lowpass = q;
} }
void FCEUI_RemoveTriangleNoise(int d) {
FSettings.RemoveTriangleNoise = d ? 1 : 0;
}
void FCEUI_SetSoundQuality(int quality) { void FCEUI_SetSoundQuality(int quality) {
FSettings.soundq = quality; FSettings.soundq = quality;
SetSoundVariables(); SetSoundVariables();