core: APU channel disable - fix bitmasks and LQ-mode volume application

Two related bugs in the user-facing fceumm_apu_1..5 channel disable
toggles. Pre-existing since commit 448a231 (set_apu_channels) and at
least as far back as the LQ rewrite for the second one.

set_apu_channels (libretro.c)
-----------------------------

The bitmask built by check_variables uses bit i for fceumm_apu_(i+1)
enabled, ordered SQ1, SQ2, Triangle, Noise, DMC. The old code:

    FSettings.SquareVolume[1] = (chan & 1) ? 256 : 0;
    FSettings.SquareVolume[0] = (chan & 2) ? 256 : 0;
    FSettings.TriangleVolume  = (chan & 3) ? 256 : 0;
    FSettings.NoiseVolume     = (chan & 4) ? 256 : 0;
    FSettings.PCMVolume       = (chan & 5) ? 256 : 0;

crossed SQ1/SQ2 and used non-power-of-two masks for the remaining
three. Practical effect: apu_1 muted SQ2 and apu_2 muted SQ1; apu_3
through apu_5 left their channels audible in any normal config (the
masks happened to be true any time another bit was set).

Fixed to read each channel's own bit:

    FSettings.SquareVolume[0] = (chan & 0x01) ? 256 : 0;  /* SQ1  */
    FSettings.SquareVolume[1] = (chan & 0x02) ? 256 : 0;  /* SQ2  */
    FSettings.TriangleVolume  = (chan & 0x04) ? 256 : 0;  /* Tri  */
    FSettings.NoiseVolume     = (chan & 0x08) ? 256 : 0;  /* Nse  */
    FSettings.PCMVolume       = (chan & 0x10) ? 256 : 0;  /* DMC  */

RDoTriangleNoisePCMLQ (sound.c)
-------------------------------

The combined Triangle/Noise/DMC LQ mix uses a non-linear lookup
table:

    totalout = wlookup2[lq_tcout + noiseout + RawDALatch];

The old code took the noise envelope (EnvUnits[2], not Triangle -
Triangle has no envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise) and
scaled it by FSettings.TriangleVolume, despite the comment claiming
to modify Triangle. Meanwhile lq_tcout (Triangle) and RawDALatch
(DMC) went into wlookup2 unscaled, so neither TriangleVolume nor
PCMVolume had any effect in LQ mode.

Fixed:
* amptab[0] (noise envelope) is now scaled by FSettings.NoiseVolume.
* lq_tcout is scaled by FSettings.TriangleVolume into a local
  scaled_tcout fed to wlookup2.
* RawDALatch is scaled by FSettings.PCMVolume into scaled_dmc.

The volume scalars are loop-invariant within this function, so the
Triangle volume is cached once in tri_vol at function entry to keep
the inner loops tight; the != 256 fast paths preserve byte-exact
behavior at full volume.

Verification
------------

Built per-channel-only test ROMs (test_sq1_only, test_sq2_only,
test_tri_only, test_nse_only, test_dmc_only) and an apu_test
runner that measures output RMS with each fceumm_apu_N toggle.
After the fix, in both LQ and HQ modes, each apu_N toggle mutes
exactly the channel whose ROM is the only sound source, and no
other toggle has any effect on it.

Default (all-on) audio output is bit-exact identical to the
baseline across the existing test_silent / test_idle / test_active /
test_stress ROMs (1500 frames each), so games and homebrew that
never touch the apu_N options see no change. No performance
regression in the default path; the != 256 guards skip the multiplies.
This commit is contained in:
U-DESKTOP-SPFP6AQ\twistedtechre
2026-05-04 07:40:08 +02:00
parent 6ffed08948
commit 6c97c3fff4
2 changed files with 50 additions and 17 deletions

View File

@@ -1975,11 +1975,23 @@ void retro_reset(void)
static void set_apu_channels(int chan) static void set_apu_channels(int chan)
{ {
FSettings.SquareVolume[1] = (chan & 1) ? 256 : 0; /* Bitmask layout (set up by check_variables): bit i = fceumm_apu_(i+1)
FSettings.SquareVolume[0] = (chan & 2) ? 256 : 0; * is enabled. Channel order matches the libretro core options:
FSettings.TriangleVolume = (chan & 3) ? 256 : 0; * bit 0 = Square 1 (apu_1)
FSettings.NoiseVolume = (chan & 4) ? 256 : 0; * bit 1 = Square 2 (apu_2)
FSettings.PCMVolume = (chan & 5) ? 256 : 0; * bit 2 = Triangle (apu_3)
* bit 3 = Noise (apu_4)
* bit 4 = PCM / DMC (apu_5)
*
* The previous masks crossed the SQ1/SQ2 indices and used non-power-of-
* two values for the remaining channels, which silently broke the
* apu_3..apu_5 toggles entirely (any combination of bits 0/1/2 left
* those channels audible) and made apu_1 mute SQ2 and vice versa. */
FSettings.SquareVolume[0] = (chan & 0x01) ? 256 : 0;
FSettings.SquareVolume[1] = (chan & 0x02) ? 256 : 0;
FSettings.TriangleVolume = (chan & 0x04) ? 256 : 0;
FSettings.NoiseVolume = (chan & 0x08) ? 256 : 0;
FSettings.PCMVolume = (chan & 0x10) ? 256 : 0;
} }
static void check_variables(bool startup) static void check_variables(bool startup)

View File

@@ -733,6 +733,9 @@ static void RDoTriangleNoisePCMLQ(void) {
uint32_t amptab[2]; uint32_t amptab[2];
uint32_t noiseout; uint32_t noiseout;
int nshift; int nshift;
uint32_t scaled_tcout;
uint32_t scaled_dmc;
const uint32_t tri_vol = FSettings.TriangleVolume;
int32_t totalout; int32_t totalout;
@@ -754,13 +757,18 @@ static void RDoTriangleNoisePCMLQ(void) {
else else
amptab[0] = EnvUnits[2].decvolume; amptab[0] = EnvUnits[2].decvolume;
/* Modify Triangle wave volume based on channel volume modifiers /* Apply per-channel volume modifiers (set via fceumm_apu_N options).
* Note: the formulat x = x * y /100 does not yield exact results, *
* but is "close enough" and avoids the need for using double vales * EnvUnits[2] is the Noise envelope (not Triangle - Triangle has no
* or implicit cohersion which are slower (we need speed here) * envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise). The previous code
* TODO: Optimize this. */ * scaled amptab[0] by FSettings.TriangleVolume, which crossed the
if (FSettings.TriangleVolume != 256) * Triangle and Noise mute toggles in LQ mode and left Triangle
amptab[0] = (amptab[0] * FSettings.TriangleVolume) / 256; * itself never muted. Triangle's contribution enters wlookup2 via
* lq_tcout below; PCM enters via RawDALatch. Scale each input
* channel by its own volume before the non-linear DAC mix - 0 in
* still produces 0 out, and 256 leaves the value unchanged. */
if (FSettings.NoiseVolume != 256)
amptab[0] = (amptab[0] * FSettings.NoiseVolume) / 256;
amptab[1] = 0; amptab[1] = 0;
amptab[0] <<= 1; amptab[0] <<= 1;
@@ -775,7 +783,14 @@ static void RDoTriangleNoisePCMLQ(void) {
else else
nshift = 13; nshift = 13;
totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; scaled_tcout = (tri_vol != 256)
? ((lq_tcout * tri_vol) / 256)
: lq_tcout;
scaled_dmc = (FSettings.PCMVolume != 256)
? ((RawDALatch * FSettings.PCMVolume) / 256)
: RawDALatch;
totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
if (inie[0] && inie[1]) { if (inie[0] && inie[1]) {
for (V = start; V < end; V++) { for (V = start; V < end; V++) {
@@ -792,7 +807,10 @@ static void RDoTriangleNoisePCMLQ(void) {
lq_tcout = (tristep & 0xF); lq_tcout = (tristep & 0xF);
if (!(tristep & 0x10)) lq_tcout ^= 0xF; if (!(tristep & 0x10)) lq_tcout ^= 0xF;
lq_tcout = lq_tcout * 3; lq_tcout = lq_tcout * 3;
totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; scaled_tcout = (tri_vol != 256)
? ((lq_tcout * tri_vol) / 256)
: lq_tcout;
totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
} }
if (lq_noiseacc <= 0) { if (lq_noiseacc <= 0) {
@@ -808,7 +826,7 @@ static void RDoTriangleNoisePCMLQ(void) {
nreg &= 0x7fff; nreg &= 0x7fff;
noiseout = amptab[(nreg >> 0xe) & 1]; noiseout = amptab[(nreg >> 0xe) & 1];
if (lq_noiseacc <= 0) goto rea2; if (lq_noiseacc <= 0) goto rea2;
totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
} /* noiseacc<=0 */ } /* noiseacc<=0 */
} /* for(V=... */ } /* for(V=... */
} else if (inie[0]) { } else if (inie[0]) {
@@ -825,7 +843,10 @@ static void RDoTriangleNoisePCMLQ(void) {
lq_tcout = (tristep & 0xF); lq_tcout = (tristep & 0xF);
if (!(tristep & 0x10)) lq_tcout ^= 0xF; if (!(tristep & 0x10)) lq_tcout ^= 0xF;
lq_tcout = lq_tcout * 3; lq_tcout = lq_tcout * 3;
totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; scaled_tcout = (tri_vol != 256)
? ((lq_tcout * tri_vol) / 256)
: lq_tcout;
totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
} }
} }
} else if (inie[1]) { } else if (inie[1]) {
@@ -845,7 +866,7 @@ static void RDoTriangleNoisePCMLQ(void) {
nreg &= 0x7fff; nreg &= 0x7fff;
noiseout = amptab[(nreg >> 0xe) & 1]; noiseout = amptab[(nreg >> 0xe) & 1];
if (lq_noiseacc <= 0) goto area2; if (lq_noiseacc <= 0) goto area2;
totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc];
} /* noiseacc<=0 */ } /* noiseacc<=0 */
} }
} else { } else {