From 6c97c3fff4140404021e609ea129628987ef69b5 Mon Sep 17 00:00:00 2001 From: "U-DESKTOP-SPFP6AQ\\twistedtechre" Date: Mon, 4 May 2026 07:40:08 +0200 Subject: [PATCH] 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. --- src/drivers/libretro/libretro.c | 22 ++++++++++++---- src/sound.c | 45 ++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 17 deletions(-) diff --git a/src/drivers/libretro/libretro.c b/src/drivers/libretro/libretro.c index afd0db5..96dece6 100644 --- a/src/drivers/libretro/libretro.c +++ b/src/drivers/libretro/libretro.c @@ -1975,11 +1975,23 @@ void retro_reset(void) static void set_apu_channels(int chan) { - 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; + /* Bitmask layout (set up by check_variables): bit i = fceumm_apu_(i+1) + * is enabled. Channel order matches the libretro core options: + * bit 0 = Square 1 (apu_1) + * bit 1 = Square 2 (apu_2) + * 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) diff --git a/src/sound.c b/src/sound.c index 87119ba..108b54e 100644 --- a/src/sound.c +++ b/src/sound.c @@ -733,6 +733,9 @@ static void RDoTriangleNoisePCMLQ(void) { uint32_t amptab[2]; uint32_t noiseout; int nshift; + uint32_t scaled_tcout; + uint32_t scaled_dmc; + const uint32_t tri_vol = FSettings.TriangleVolume; int32_t totalout; @@ -754,13 +757,18 @@ static void RDoTriangleNoisePCMLQ(void) { else amptab[0] = EnvUnits[2].decvolume; - /* Modify Triangle wave volume based on channel volume modifiers - * Note: the formulat x = x * y /100 does not yield exact results, - * but is "close enough" and avoids the need for using double vales - * or implicit cohersion which are slower (we need speed here) - * TODO: Optimize this. */ - if (FSettings.TriangleVolume != 256) - amptab[0] = (amptab[0] * FSettings.TriangleVolume) / 256; + /* Apply per-channel volume modifiers (set via fceumm_apu_N options). + * + * EnvUnits[2] is the Noise envelope (not Triangle - Triangle has no + * envelope; EnvUnits[0]=SQ1, [1]=SQ2, [2]=Noise). The previous code + * scaled amptab[0] by FSettings.TriangleVolume, which crossed the + * Triangle and Noise mute toggles in LQ mode and left Triangle + * 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[0] <<= 1; @@ -775,7 +783,14 @@ static void RDoTriangleNoisePCMLQ(void) { else 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]) { for (V = start; V < end; V++) { @@ -792,7 +807,10 @@ static void RDoTriangleNoisePCMLQ(void) { lq_tcout = (tristep & 0xF); if (!(tristep & 0x10)) lq_tcout ^= 0xF; 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) { @@ -808,7 +826,7 @@ static void RDoTriangleNoisePCMLQ(void) { nreg &= 0x7fff; noiseout = amptab[(nreg >> 0xe) & 1]; if (lq_noiseacc <= 0) goto rea2; - totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; + totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc]; } /* noiseacc<=0 */ } /* for(V=... */ } else if (inie[0]) { @@ -825,7 +843,10 @@ static void RDoTriangleNoisePCMLQ(void) { lq_tcout = (tristep & 0xF); if (!(tristep & 0x10)) lq_tcout ^= 0xF; 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]) { @@ -845,7 +866,7 @@ static void RDoTriangleNoisePCMLQ(void) { nreg &= 0x7fff; noiseout = amptab[(nreg >> 0xe) & 1]; if (lq_noiseacc <= 0) goto area2; - totalout = wlookup2[lq_tcout + noiseout + RawDALatch]; + totalout = wlookup2[scaled_tcout + noiseout + scaled_dmc]; } /* noiseacc<=0 */ } } else {