Files
ci-libretro-fceumm/src/sound.c
libretroadmin 3f23e2b98f sound: fix noise LFSR power-on state to match real hardware (fixes #466)
The internal noise shift register in this file uses a bit-reversed
layout vs the canonical nesdev wiki model: output is read from bit 14
(not bit 0), the feedback taps are at 13/14 in long mode and 8/14 in
short mode (mirrors of 0/1 and 0/6), and the register shifts left
rather than right.  The algorithm is algebraically equivalent to the
wiki form, but the power-on state has to be mirrored as well.

Real hardware initialises the LFSR to $0001 with bit 0 set.  Under
this file's mirroring, the equivalent state has bit 14 set - i.e.
nreg = 0x4000, not nreg = 1.  Starting at 1 placed the LFSR 14 steps
off-phase from real hardware in long mode and at an entirely
different position in the 93-cycle short-mode loop.

Reported by NSFPlay author Brad Smith (rainwarrior) comparing
moon8.nes audio against Mesen.  Reproduced via headless libretro
harness with per-channel solo bisection against negativeExponent's
fceumm_next branch (which uses the un-mirrored nesdev layout):

  channel    RMS diff vs _next (noise solo, 22-60s window)
  -------    ----------------------------------------------
  square1                1   ( 0.4%) - bit-identical
  square2                1   ( 0.4%) - bit-identical
  triangle             178   (14.0%) - minor difference
  noise               1914   (125.8%) - completely off
  dmc                    2   ( 0.4%) - bit-identical

After this patch the noise-solo RMS drops to 6 (0.4%), bringing the
noise channel into bit-exact agreement with _next/Mesen/NSFPlay.
Full-mix moon8 audio RMS vs _next falls from 1135 to 126 across the
60-second test.
2026-06-14 18:43:14 +02:00

1470 lines
39 KiB
C

/* FCE Ultra - NES/Famicom Emulator
*
* Copyright notice for this file:
* Copyright (C) 2002 Xodnizel
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <stdlib.h>
#include <string.h>
#include "fceu-types.h"
#include "x6502.h"
#include "fceu.h"
#include "sound.h"
#include "filter.h"
#include "state.h"
static uint32_t wlookup1[32];
static uint32_t wlookup2[203];
/* Helper: clamp wlookup2's combined index to the table size.
*
* The LQ Tri/Noise/PCM mix sums lq_tcout, noiseout, and RawDALatch (with
* per-channel volume scalars applied) and uses the result as the index
* into wlookup2[203]. Each input is bounded under normal emulation:
* lq_tcout is (tristep & 0xF) * 3 (max 45), noiseout is the noise
* envelope decvolume << 1 (max 30 with 0..15 envelope range), and
* RawDALatch is the $4011 DAC latch (writes mask off the top bit, so
* 0..127 in the running emulator). Sum max ~202 fits the table.
*
* After loading a savestate, however, every one of those state fields
* is whatever the file said, so EnvUnits[2].decvolume (loaded as raw
* uint8_t) can be 0..255, RawDALatch can be 0..255, lq_tcout (loaded
* as uint32_t) can be anything, and the sum can overflow the table by
* a wide margin - a heap-buffer-overflow read that AddressSanitizer
* surfaces in retro_run after retro_unserialize on a malformed state.
*
* Clamp at the access site so the protection holds regardless of which
* piece of state was tampered with. Out-of-range values play wrong
* audio for one frame until DoEnv / channel writes restore sane state,
* but the emulator stays alive. */
static INLINE uint32_t wl2(uint32_t idx)
{
if (idx >= sizeof(wlookup2) / sizeof(wlookup2[0]))
idx = sizeof(wlookup2) / sizeof(wlookup2[0]) - 1;
return wlookup2[idx];
}
int32_t Wave[2048 + 512];
int32_t WaveHi[40000];
int32_t WaveFinal[2048 + 512];
EXPSOUND GameExpSound = { 0, 0, 0, 0, 0, 0 };
static uint8_t TriCount = 0;
static uint8_t TriMode = 0;
static int32_t tristep = 0;
static int32_t wlcount[4] = { 0, 0, 0, 0 }; /* Wave length counters. */
static uint8_t IRQFrameMode = 0; /* $4017 / xx000000 */
static uint8_t PSG[0x10];
static uint8_t RawDALatch = 0; /* $4011 0xxxxxxx */
uint8_t EnabledChannels = 0; /* Byte written to $4015 */
typedef struct {
uint8_t Speed;
uint8_t Mode; /* Fixed volume(1), and loop(2) */
uint8_t DecCountTo1;
uint8_t decvolume;
int reloaddec;
} ENVUNIT;
unsigned DMC_7bit = 0; /* used to skip overclocking */
static ENVUNIT EnvUnits[3];
static const int RectDuties[4] = { 1, 2, 4, 6 };
static int32_t RectDutyCount[2];
static uint8_t sweepon[2];
static int32_t curfreq[2];
static uint8_t SweepCount[2];
static uint8_t sweepReload[2];
static uint16_t nreg = 0;
static uint8_t fcnt = 0;
static int32_t fhcnt = 0;
static int32_t fhinc = 0;
uint32_t soundtsoffs = 0;
/* Variables exclusively for low-quality sound. */
int32_t nesincsize = 0;
uint32_t soundtsinc = 0;
uint32_t soundtsi = 0;
static int32_t sqacc[2];
static uint32_t lq_tcout;
static int32_t lq_triacc;
static int32_t lq_noiseacc;
/* LQ variables segment ends. */
static int32_t lengthcount[4];
static const uint8_t lengthtable[0x20] =
{
0x0A, 0xFE, 0x14, 0x02, 0x28, 0x04, 0x50, 0x06,
0xa0, 0x08, 0x3c, 0x0a, 0x0e, 0x0c, 0x1a, 0x0e,
0x0c, 0x10, 0x18, 0x12, 0x30, 0x14, 0x60, 0x16,
0xc0, 0x18, 0x48, 0x1a, 0x10, 0x1c, 0x20, 0x1E
};
static const uint32_t NTSCNoiseFreqTable[0x10] =
{
0x004, 0x008, 0x010, 0x020, 0x040, 0x060, 0x080, 0x0A0,
0x0CA, 0x0FE, 0x17C, 0x1FC, 0x2FA, 0x3F8, 0x7F2, 0xFE4
};
static const uint32_t PALNoiseFreqTable[0x10] =
{
0x004, 0x008, 0x00E, 0x01E, 0x03C, 0x058, 0x076, 0x094,
0x0BC, 0x0EC, 0x162, 0x1D8, 0x2C4, 0x3B0, 0x762, 0xEC2
};
static const uint32_t NTSCDMCTable[0x10] =
{
0x1AC, 0x17C, 0x154, 0x140, 0x11E, 0x0FE, 0x0E2, 0x0D6,
0x0BE, 0x0A0, 0x08E, 0x080, 0x06A, 0x054, 0x048, 0x036
};
static const uint32_t PALDMCTable[0x10] =
{
0x18E, 0x162, 0x13C, 0x12A, 0x114, 0x0EC, 0x0D2, 0x0C6,
0x0B0, 0x094, 0x084, 0x076, 0x062, 0x04E, 0x042, 0x032
};
/* $4010 - Frequency
* $4011 - Actual data outputted
* $4012 - Address register: $c000 + V*64
* $4013 - Size register: Size in bytes = (V+1)*64
*/
static int32_t DMCacc = 1;
static int32_t DMCPeriod = 0;
static uint8_t DMCBitCount = 0;
static uint8_t DMCAddressLatch = 0, DMCSizeLatch = 0; /* writes to 4012 and 4013 */
static uint8_t DMCFormat = 0; /* Write to $4010 */
static uint32_t DMCAddress = 0;
static int32_t DMCSize = 0;
static uint8_t DMCShift = 0;
static uint8_t SIRQStat = 0;
static char DMCHaveDMA = 0;
static uint8_t DMCDMABuf = 0;
static char DMCHaveSample = 0;
static void Dummyfunc(void) { }
static void (*DoNoise)(void) = Dummyfunc;
static void (*DoTriangle)(void) = Dummyfunc;
static void (*DoPCM)(void) = Dummyfunc;
static void (*DoSQ1)(void) = Dummyfunc;
static void (*DoSQ2)(void) = Dummyfunc;
static uint32_t ChannelBC[5];
static void LoadDMCPeriod(uint8_t V) {
if (PAL)
DMCPeriod = PALDMCTable[V];
else
DMCPeriod = NTSCDMCTable[V];
}
static void PrepDPCM() {
DMCAddress = 0x4000 + (DMCAddressLatch << 6);
DMCSize = (DMCSizeLatch << 4) + 1;
}
/* Instantaneous? Maybe the new freq value is being calculated all of the time... */
static int FASTAPASS(2) CheckFreq(uint32_t cf, uint8_t sr) {
uint32_t mod;
if (!(sr & 0x8)) {
mod = cf >> (sr & 7);
if ((mod + cf) & 0x800)
return(0);
}
return(1);
}
static void SQReload(int x, uint8_t V) {
if (EnabledChannels & (1 << x))
lengthcount[x] = lengthtable[(V >> 3) & 0x1f];
curfreq[x] = (curfreq[x] & 0xff) | ((V & 7) << 8);
RectDutyCount[x] = 7;
EnvUnits[x].reloaddec = 1;
}
static DECLFW(Write_PSG) {
A &= 0x1F;
switch (A) {
case 0x0:
DoSQ1();
EnvUnits[0].Mode = (V & 0x30) >> 4;
EnvUnits[0].Speed = (V & 0xF);
if (swapDuty)
V = (V & 0x3F) | ((V & 0x80) >> 1) | ((V & 0x40) << 1);
break;
case 0x1:
DoSQ1();
sweepReload[0] = 1;
sweepon[0] = (V & 0x80);
break;
case 0x2:
DoSQ1();
curfreq[0] &= 0xFF00;
curfreq[0] |= V;
break;
case 0x3:
DoSQ1();
SQReload(0, V);
break;
case 0x4:
DoSQ2();
EnvUnits[1].Mode = (V & 0x30) >> 4;
EnvUnits[1].Speed = (V & 0xF);
if (swapDuty)
V = (V & 0x3F) | ((V & 0x80) >> 1) | ((V & 0x40) << 1);
break;
case 0x5:
DoSQ2();
sweepReload[1] = 1;
sweepon[1] = (V & 0x80);
break;
case 0x6:
DoSQ2();
curfreq[1] &= 0xFF00;
curfreq[1] |= V;
break;
case 0x7:
DoSQ2();
SQReload(1, V);
break;
case 0xa:
DoTriangle();
break;
case 0xb:
DoTriangle();
if (EnabledChannels & 0x4)
lengthcount[2] = lengthtable[(V >> 3) & 0x1f];
TriMode = 1; /* Load mode */
break;
case 0xC:
DoNoise();
EnvUnits[2].Mode = (V & 0x30) >> 4;
EnvUnits[2].Speed = (V & 0xF);
break;
case 0xE:
DoNoise();
break;
case 0xF:
DoNoise();
if (EnabledChannels & 0x8)
lengthcount[3] = lengthtable[(V >> 3) & 0x1f];
EnvUnits[2].reloaddec = 1;
break;
case 0x10:
DoPCM();
LoadDMCPeriod(V & 0xF);
if (SIRQStat & 0x80) {
if (!(V & 0x80)) {
X6502_IRQEnd(FCEU_IQDPCM);
SIRQStat &= ~0x80;
} else X6502_IRQBegin(FCEU_IQDPCM);
}
break;
}
PSG[A] = V;
}
static DECLFW(Write_DMCRegs) {
A &= 0xF;
switch (A) {
case 0x00: DoPCM();
LoadDMCPeriod(V & 0xF);
if (SIRQStat & 0x80) {
if (!(V & 0x80)) {
X6502_IRQEnd(FCEU_IQDPCM);
SIRQStat &= ~0x80;
} else X6502_IRQBegin(FCEU_IQDPCM);
}
DMCFormat = V;
break;
case 0x01: DoPCM();
{
/* $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;
case 0x02:
DMCAddressLatch = V;
if (V)
DMC_7bit = 0;
break;
case 0x03:
DMCSizeLatch = V;
if (V)
DMC_7bit = 0;
break;
}
}
static DECLFW(StatusWrite) {
int x;
DoSQ1();
DoSQ2();
DoTriangle();
DoNoise();
DoPCM();
for (x = 0; x < 4; x++)
if (!(V & (1 << x))) lengthcount[x] = 0; /* Force length counters to 0. */
if (V & 0x10) {
if (!DMCSize)
PrepDPCM();
} else {
DMCSize = 0;
}
SIRQStat &= ~0x80;
X6502_IRQEnd(FCEU_IQDPCM);
EnabledChannels = V & 0x1F;
}
static DECLFR(StatusRead) {
int x;
uint8_t ret;
ret = SIRQStat;
for (x = 0; x < 4; x++) ret |= lengthcount[x] ? (1 << x) : 0;
if (DMCSize) ret |= 0x10;
{
SIRQStat &= ~0x40;
X6502_IRQEnd(FCEU_IQFCOUNT);
}
return ret;
}
static void FASTAPASS(1) FrameSoundStuff(int V) {
int P;
DoSQ1();
DoSQ2();
DoNoise();
DoTriangle();
if (!(V & 1)) { /* Envelope decay, linear counter, length counter, freq sweep */
if (!(PSG[8] & 0x80))
if (lengthcount[2] > 0)
lengthcount[2]--;
if (!(PSG[0xC] & 0x20)) /* Make sure loop flag is not set. */
if (lengthcount[3] > 0)
lengthcount[3]--;
for (P = 0; P < 2; P++) {
if (!(PSG[P << 2] & 0x20)) /* Make sure loop flag is not set. */
if (lengthcount[P] > 0)
lengthcount[P]--;
/* Frequency Sweep Code Here */
/* xxxx 0000 */
/* xxxx = hz. 120/(x+1)*/
/* http://wiki.nesdev.com/w/index.php/APU_Sweep */
if (SweepCount[P] > 0) SweepCount[P]--;
if (SweepCount[P] <= 0) {
uint32_t sweepShift = (PSG[(P << 2) + 0x1] & 7);
if (sweepon[P] && sweepShift && curfreq[P] >= 8) {
int32_t mod = (curfreq[P] >> sweepShift);
if (PSG[(P << 2) + 0x1] & 0x8) {
curfreq[P] -= (mod + (P ^ 1));
} else if ((mod + curfreq[P]) < 0x800) {
curfreq[P] += mod;
}
}
SweepCount[P] = (((PSG[(P << 2) + 0x1] >> 4) & 7) + 1);
}
if (sweepReload[P]) {
SweepCount[P] = (((PSG[(P << 2) + 0x1] >> 4) & 7) + 1);
sweepReload[P] = 0;
}
}
}
/* Now do envelope decay + linear counter. */
if (TriMode)/* In load mode? */
TriCount = PSG[0x8] & 0x7F;
else if (TriCount)
TriCount--;
if (!(PSG[0x8] & 0x80))
TriMode = 0;
for (P = 0; P < 3; P++) {
if (EnvUnits[P].reloaddec) {
EnvUnits[P].decvolume = 0xF;
EnvUnits[P].DecCountTo1 = EnvUnits[P].Speed + 1;
EnvUnits[P].reloaddec = 0;
continue;
}
if (EnvUnits[P].DecCountTo1 > 0) EnvUnits[P].DecCountTo1--;
if (EnvUnits[P].DecCountTo1 == 0) {
EnvUnits[P].DecCountTo1 = EnvUnits[P].Speed + 1;
if (EnvUnits[P].decvolume || (EnvUnits[P].Mode & 0x2)) {
EnvUnits[P].decvolume--;
EnvUnits[P].decvolume &= 0xF;
}
}
}
}
void FrameSoundUpdate(void) {
/* Linear counter: Bit 0-6 of $4008
* Length counter: Bit 4-7 of $4003, $4007, $400b, $400f
*/
if (fcnt == 3) {
if (IRQFrameMode & 0x2)
fhcnt += fhinc;
}
FrameSoundStuff(fcnt);
fcnt = (fcnt + 1) & 3;
/* has to be moved here to fix Dragon Warrior 4
* after irq inhibit fix for $4017 */
if (!fcnt && !(IRQFrameMode & 0x3)) {
SIRQStat |= 0x40;
X6502_IRQBegin(FCEU_IQFCOUNT);
}
}
static INLINE void tester(void) {
if (DMCBitCount == 0) {
if (!DMCHaveDMA)
DMCHaveSample = 0;
else {
DMCHaveSample = 1;
DMCShift = DMCDMABuf;
DMCHaveDMA = 0;
}
}
}
static INLINE void DMCDMA(void) {
if (DMCSize && !DMCHaveDMA) {
X6502_DMR(0x8000 + DMCAddress);
X6502_DMR(0x8000 + DMCAddress);
X6502_DMR(0x8000 + DMCAddress);
DMCDMABuf = X6502_DMR(0x8000 + DMCAddress);
DMCHaveDMA = 1;
DMCAddress = (DMCAddress + 1) & 0x7fff;
DMCSize--;
if (!DMCSize) {
if (DMCFormat & 0x40)
PrepDPCM();
else {
if (DMCFormat & 0x80) {
SIRQStat |= 0x80;
X6502_IRQBegin(FCEU_IQDPCM);
}
}
}
}
}
void FASTAPASS(1) FCEU_SoundCPUHook(int cycles) {
fhcnt -= cycles * 48;
if (fhcnt <= 0) {
FrameSoundUpdate();
fhcnt += fhinc;
}
DMCDMA();
DMCacc -= cycles;
while (DMCacc <= 0) {
if (DMCHaveSample) {
uint8_t bah = RawDALatch;
int t = ((DMCShift & 1) << 2) - 2;
/* Unbelievably ugly hack */
if (FSettings.SndRate) {
soundtsoffs += DMCacc;
DoPCM();
soundtsoffs -= DMCacc;
}
RawDALatch += t;
if (RawDALatch & 0x80)
RawDALatch = bah;
}
DMCacc += DMCPeriod;
DMCBitCount = (DMCBitCount + 1) & 7;
DMCShift >>= 1;
tester();
}
}
static void RDoPCM(void) {
uint32_t V;
for (V = ChannelBC[4]; V < SOUNDTS; V++)
/* TODO: get rid of floating calculations to binary. set log volume scaling. */
WaveHi[V] += (((RawDALatch << 16) / 256) * FSettings.PCMVolume ) & (~0xFFFF);
ChannelBC[4] = SOUNDTS;
}
/* This has the correct phase. Don't mess with it. */
static INLINE void RDoSQ(int x) {
int32_t V;
int32_t amp;
int32_t rthresh;
int32_t *D;
int32_t currdc;
int32_t cf;
int32_t rc;
V = SOUNDTS - ChannelBC[x];
cf = (curfreq[x] + 1) * 2;
rc = wlcount[x];
/* added 2018/12/08 */
/* when pulse channel is silenced, resets length counters but not
* duty cycle, instead of resetting both */
if ((curfreq[x] < 8 || curfreq[x] > 0x7ff) ||
!CheckFreq(curfreq[x], PSG[(x << 2) | 0x1]) ||
!lengthcount[x]) {
rc -= V;
if (rc <= 0) {
rc = cf - (-rc % cf);
}
} else {
int dutyCycle;
if (EnvUnits[x].Mode & 0x1)
amp = EnvUnits[x].Speed;
else
amp = EnvUnits[x].decvolume;
/* Modify Square 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 values
* or implicit cohersion which are slower (we need speed here) */
/* TODO: Optimize this. */
if (FSettings.SquareVolume[x] != 256)
amp = (amp * FSettings.SquareVolume[x]) / 256;
amp <<= 24;
dutyCycle = (PSG[(x << 2)] & 0xC0) >> 6;
rthresh = RectDuties[dutyCycle];
currdc = RectDutyCount[x];
D = &WaveHi[ChannelBC[x]];
while (V > 0) {
if (currdc < rthresh)
*D += amp;
rc--;
if (!rc) {
rc = cf;
currdc = (currdc + 1) & 7;
}
V--;
D++;
}
RectDutyCount[x] = currdc;
}
wlcount[x] = rc;
ChannelBC[x] = SOUNDTS;
}
static void RDoSQ1(void) {
RDoSQ(0);
}
static void RDoSQ2(void) {
RDoSQ(1);
}
static void RDoSQLQ(void) {
int32_t start, end;
int32_t V;
int32_t amp[2];
int32_t rthresh[2];
int32_t freq[2];
int x;
int32_t inie[2];
int32_t ttable[2][8];
int32_t totalout;
start = ChannelBC[0];
end = (SOUNDTS << 16) / soundtsinc;
if (end <= start) return;
ChannelBC[0] = end;
for (x = 0; x < 2; x++) {
int y;
int dutyCycle;
inie[x] = nesincsize;
if (curfreq[x] < 8 || curfreq[x] > 0x7ff)
inie[x] = 0;
if (!CheckFreq(curfreq[x], PSG[(x << 2) | 0x1]))
inie[x] = 0;
if (!lengthcount[x])
inie[x] = 0;
if (EnvUnits[x].Mode & 0x1)
amp[x] = EnvUnits[x].Speed;
else
amp[x] = EnvUnits[x].decvolume;
/* Modify Square 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)
* fixed - setting up maximum volume for square2 caused complete mute square2 channel.
* TODO: Optimize this. */
if (FSettings.SquareVolume[x] != 256)
amp[x] = (amp[x] * FSettings.SquareVolume[x]) / 256;
if (!inie[x]) amp[x] = 0; /* Correct? Buzzing in MM2, others otherwise... */
dutyCycle = (PSG[(x << 2)] & 0xC0) >> 6;
rthresh[x] = RectDuties[dutyCycle];
for (y = 0; y < 8; y++) {
if (y < rthresh[x])
ttable[x][y] = amp[x];
else
ttable[x][y] = 0;
}
freq[x] = (curfreq[x] + 1) << 1;
freq[x] <<= 17;
}
/* RectDutyCount[] is reloaded from savestate as raw int32_t and may
* hold any value at this point - the inner-loop increments later mask
* it back to 0..7, but this first lookup hits the table before any
* such mask. Without bounds protection, ttable[0..7] (int[8]) reads
* garbage memory and feeds it into wlookup1[32], which then OOB-reads
* - reachable via a malformed savestate. Mask both indices defensively
* here and at every other ttable/wlookup1 call site below. The mask
* is one instruction and runs only at the chunk boundaries, not per
* inner-loop sample. */
totalout = wlookup1[(ttable[0][RectDutyCount[0] & 7]
+ ttable[1][RectDutyCount[1] & 7]) & 31];
if (!inie[0] && !inie[1]) {
/* Both squares silent: amp[x] was forced to 0 above (line
* "if (!inie[x]) amp[x] = 0"), which propagates through
* ttable[x] to make totalout = wlookup1[0] = 0. The
* previous code looped (end - start) iterations adding 0
* to Wave[V>>4] - genuinely no-op for ~30000 NES cycles
* per frame. Skip the loop entirely. */
} else {
for (V = start; V < end; V++) {
/* int tmpamp=0;
if(RectDutyCount[0]<rthresh[0])
tmpamp=amp[0];
if(RectDutyCount[1]<rthresh[1])
tmpamp+=amp[1];
tmpamp=wlookup1[tmpamp];
tmpamp = wlookup1[ ttable[0][RectDutyCount[0]] + ttable[1][RectDutyCount[1]] ];
*/
Wave[V >> 4] += totalout; /* tmpamp; */
sqacc[0] -= inie[0];
sqacc[1] -= inie[1];
if (sqacc[0] <= 0) {
rea:
sqacc[0] += freq[0];
RectDutyCount[0] = (RectDutyCount[0] + 1) & 7;
if (sqacc[0] <= 0) goto rea;
totalout = wlookup1[(ttable[0][RectDutyCount[0]]
+ ttable[1][RectDutyCount[1] & 7]) & 31];
}
if (sqacc[1] <= 0) {
rea2:
sqacc[1] += freq[1];
RectDutyCount[1] = (RectDutyCount[1] + 1) & 7;
if (sqacc[1] <= 0) goto rea2;
totalout = wlookup1[(ttable[0][RectDutyCount[0] & 7]
+ ttable[1][RectDutyCount[1]]) & 31];
}
}
}
}
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); */
/* 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--) {
*start += (tcout / 256 * FSettings.TriangleVolume) & (~0xFFFF); /* TODO OPTIMIZE ME */
start++;
}
/* cout = (tcout / 256 * FSettings.TriangleVolume) & (~0xFFFF);
for(V = ChannelBC[2]; V < SOUNDTS; V++)
WaveHi[V] += cout; */
} else {
for (V = ChannelBC[2]; V < SOUNDTS; V++) {
WaveHi[V] += (tcout / 256 * FSettings.TriangleVolume) & (~0xFFFF); /* TODO OPTIMIZE ME! */
wlcount[2]--;
if (!wlcount[2]) {
wlcount[2] = (PSG[0xa] | ((PSG[0xb] & 7) << 8)) + 1;
tristep++;
tcout = (tristep & 0xF);
if (!(tristep & 0x10)) tcout ^= 0xF;
tcout = (tcout * 3) << 16;
}
}
}
ChannelBC[2] = SOUNDTS;
}
static void RDoTriangleNoisePCMLQ(void) {
int32_t V;
int32_t start, end;
int32_t freq[2];
int32_t inie[2];
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;
start = ChannelBC[2];
end = (SOUNDTS << 16) / soundtsinc;
if (end <= start) return;
ChannelBC[2] = end;
inie[0] = inie[1] = nesincsize;
freq[0] = (((PSG[0xa] | ((PSG[0xb] & 7) << 8)) + 1));
if (!lengthcount[2] || !TriCount || freq[0] <= 4)
inie[0] = 0;
freq[0] <<= 17;
if (EnvUnits[2].Mode & 0x1)
amptab[0] = EnvUnits[2].Speed;
else
amptab[0] = EnvUnits[2].decvolume;
/* 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;
if (!lengthcount[3])
amptab[0] = inie[1] = 0; /* Quick hack speedup, set inie[1] to 0 */
noiseout = amptab[(nreg >> 0xe) & 1];
if (PSG[0xE] & 0x80)
nshift = 8;
else
nshift = 13;
scaled_tcout = (tri_vol != 256)
? ((lq_tcout * tri_vol) / 256)
: lq_tcout;
scaled_dmc = (FSettings.PCMVolume != 256)
? ((RawDALatch * FSettings.PCMVolume) / 256)
: RawDALatch;
totalout = wl2(scaled_tcout + noiseout + scaled_dmc);
if (inie[0] && inie[1]) {
for (V = start; V < end; V++) {
Wave[V >> 4] += totalout;
lq_triacc -= inie[0];
lq_noiseacc -= inie[1];
if (lq_triacc <= 0) {
rea:
lq_triacc += freq[0]; /* t; */
tristep = (tristep + 1) & 0x1F;
if (lq_triacc <= 0) goto rea;
lq_tcout = (tristep & 0xF);
if (!(tristep & 0x10)) lq_tcout ^= 0xF;
lq_tcout = lq_tcout * 3;
scaled_tcout = (tri_vol != 256)
? ((lq_tcout * tri_vol) / 256)
: lq_tcout;
totalout = wl2(scaled_tcout + noiseout + scaled_dmc);
}
if (lq_noiseacc <= 0) {
rea2:
/* used to added <<(16+2) when the noise table
* values were half.
*/
if (PAL)
lq_noiseacc += PALNoiseFreqTable[PSG[0xE] & 0xF] << (16 + 1);
else
lq_noiseacc += NTSCNoiseFreqTable[PSG[0xE] & 0xF] << (16 + 1);
nreg = (nreg << 1) + (((nreg >> nshift) ^ (nreg >> 14)) & 1);
nreg &= 0x7fff;
noiseout = amptab[(nreg >> 0xe) & 1];
if (lq_noiseacc <= 0) goto rea2;
totalout = wl2(scaled_tcout + noiseout + scaled_dmc);
} /* noiseacc<=0 */
} /* for(V=... */
} else if (inie[0]) {
for (V = start; V < end; V++) {
Wave[V >> 4] += totalout;
lq_triacc -= inie[0];
if (lq_triacc <= 0) {
area:
lq_triacc += freq[0]; /* t; */
tristep = (tristep + 1) & 0x1F;
if (lq_triacc <= 0) goto area;
lq_tcout = (tristep & 0xF);
if (!(tristep & 0x10)) lq_tcout ^= 0xF;
lq_tcout = lq_tcout * 3;
scaled_tcout = (tri_vol != 256)
? ((lq_tcout * tri_vol) / 256)
: lq_tcout;
totalout = wl2(scaled_tcout + noiseout + scaled_dmc);
}
}
} else if (inie[1]) {
for (V = start; V < end; V++) {
Wave[V >> 4] += totalout;
lq_noiseacc -= inie[1];
if (lq_noiseacc <= 0) {
area2:
/* used to be added <<(16+2) when the noise table
* values were half.
*/
if (PAL)
lq_noiseacc += PALNoiseFreqTable[PSG[0xE] & 0xF] << (16 + 1);
else
lq_noiseacc += NTSCNoiseFreqTable[PSG[0xE] & 0xF] << (16 + 1);
nreg = (nreg << 1) + (((nreg >> nshift) ^ (nreg >> 14)) & 1);
nreg &= 0x7fff;
noiseout = amptab[(nreg >> 0xe) & 1];
if (lq_noiseacc <= 0) goto area2;
totalout = wl2(scaled_tcout + noiseout + scaled_dmc);
} /* noiseacc<=0 */
}
} else {
for (V = start; V < end; V++)
Wave[V >> 4] += totalout;
}
}
static void RDoNoise(void) {
uint32_t V;
int32_t outo;
uint32_t amptab[2];
if (EnvUnits[2].Mode & 0x1)
amptab[0] = EnvUnits[2].Speed;
else
amptab[0] = EnvUnits[2].decvolume;
/* Modify Noise 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.NoiseVolume != 256)
amptab[0] = (amptab[0] * FSettings.NoiseVolume) / 256;
amptab[0] <<= 16;
amptab[1] = 0;
amptab[0] <<= 1;
outo = amptab[(nreg >> 0xe) & 1];
if (!lengthcount[3]) {
outo = amptab[0] = 0;
}
if (PSG[0xE] & 0x80) {/* "short" noise */
for (V = ChannelBC[3]; V < SOUNDTS; V++) {
WaveHi[V] += outo;
wlcount[3]--;
if (!wlcount[3]) {
uint8_t feedback;
if (PAL)
wlcount[3] = PALNoiseFreqTable[PSG[0xE] & 0xF];
else
wlcount[3] = NTSCNoiseFreqTable[PSG[0xE] & 0xF];
feedback = ((nreg >> 8) & 1) ^ ((nreg >> 14) & 1);
nreg = (nreg << 1) + feedback;
nreg &= 0x7fff;
outo = amptab[(nreg >> 0xe) & 1];
}
}
} else {
for (V = ChannelBC[3]; V < SOUNDTS; V++) {
WaveHi[V] += outo;
wlcount[3]--;
if (!wlcount[3]) {
uint8_t feedback;
if (PAL)
wlcount[3] = PALNoiseFreqTable[PSG[0xE] & 0xF];
else
wlcount[3] = NTSCNoiseFreqTable[PSG[0xE] & 0xF];
feedback = ((nreg >> 13) & 1) ^ ((nreg >> 14) & 1);
nreg = (nreg << 1) + feedback;
nreg &= 0x7fff;
outo = amptab[(nreg >> 0xe) & 1];
}
}
}
ChannelBC[3] = SOUNDTS;
}
static DECLFW(Write_IRQFM) {
V = (V & 0xC0) >> 6;
fcnt = 0;
if (V & 2)
FrameSoundUpdate();
/* fcnt = 1; */
fhcnt = fhinc;
if (V & 1) {
X6502_IRQEnd(FCEU_IQFCOUNT);
SIRQStat &= ~0x40;
}
IRQFrameMode = V;
}
void SetNESSoundMap(void) {
SetWriteHandler(0x4000, 0x400F, Write_PSG);
SetWriteHandler(0x4010, 0x4013, Write_DMCRegs);
SetWriteHandler(0x4017, 0x4017, Write_IRQFM);
SetWriteHandler(0x4015, 0x4015, StatusWrite);
SetReadHandler(0x4015, 0x4015, StatusRead);
}
static int32_t inbuf = 0;
int FlushEmulateSound(void) {
int x;
int32_t end, left;
if (!sound_timestamp) return(0);
if (!FSettings.SndRate) {
left = 0;
end = 0;
goto nosoundo;
}
DoSQ1();
DoSQ2();
DoTriangle();
DoNoise();
DoPCM();
if (FSettings.soundq >= 1) {
int32_t *tmpo = &WaveHi[soundtsoffs];
if (GameExpSound.HiFill) GameExpSound.HiFill();
for (x = sound_timestamp; x; x--) {
uint32_t b = *tmpo;
*tmpo = (b & 65535) + wl2((b >> 16) & 255) + wlookup1[(b >> 24) & 31];
tmpo++;
}
end = NeoFilterSound(WaveHi, WaveFinal, SOUNDTS, &left);
/* Slide the trailing `left` coefficient-history samples back
* to the start of the buffer for next frame's filter, then
* clear the area between left and SOUNDTS so next frame's
* channel accumulators start at zero.
*
* The previous code cleared all the way to sizeof(WaveHi),
* but only indices [left, SOUNDTS) were dirtied this frame -
* everything past SOUNDTS is still zero from the prior
* frame's clear (or from FCEUSND_Power on first frame).
* WaveHi is 40000 entries = 160 KB; SOUNDTS is bounded by
* NES cycles per frame (~30000), so this saves ~40 KB of
* memset per HQ frame. The (SOUNDTS > left) guard handles
* the degenerate case of a very short frame where SOUNDTS
* may not have advanced past the coefficient history. */
memmove(WaveHi, WaveHi + SOUNDTS - left, left * sizeof(uint32_t));
if ((uint32_t)SOUNDTS > (uint32_t)left)
memset(WaveHi + left, 0, (SOUNDTS - left) * sizeof(uint32_t));
if (GameExpSound.HiSync) GameExpSound.HiSync(left);
for (x = 0; x < 5; x++)
ChannelBC[x] = left;
} else {
end = (SOUNDTS << 16) / soundtsinc;
if (GameExpSound.Fill)
GameExpSound.Fill(end & 0xF);
SexyFilter(Wave, WaveFinal, end >> 4);
if (FSettings.lowpass)
SexyFilter2(WaveFinal, end >> 4);
if (end & 0xF)
Wave[0] = Wave[(end >> 4)];
Wave[end >> 4] = 0;
}
nosoundo:
if (FSettings.soundq >= 1) {
soundtsoffs = left;
} else {
for (x = 0; x < 5; x++)
ChannelBC[x] = end & 0xF;
soundtsoffs = (soundtsinc * (end & 0xF)) >> 16;
end >>= 4;
}
inbuf = end;
return end;
}
int GetSoundBuffer(int32_t **W) {
*W = WaveFinal;
return(inbuf);
}
/* FIXME: Find out what sound registers get reset on reset. I know $4001/$4005 don't,
due to that whole MegaMan 2 Game Genie thing.
*/
void FCEUSND_Reset(void) {
int x;
fhcnt = fhinc;
fcnt = 0;
/* Power-on noise shift register state.
*
* Real hardware initializes the 15-bit noise LFSR to $0001 with bit
* 0 set (the output bit, muting the channel until the first feedback
* cycle). This file stores the LFSR with the bit order reversed -
* the output is read from bit 14, the feedback taps are at 13/14
* (long mode) or 8/14 (short mode), and the shift goes left rather
* than right (see RDoNoise / NoLQNoise). Under that mirroring, the
* real-hardware $0001 state corresponds to nreg = 0x4000 here, not
* nreg = 1.
*
* Initialising to 1 left the LFSR running 14 long-mode steps ahead
* of every other accurate emulator (Mesen, NSFPlay, _next), and made
* short-mode output diverge entirely - the 93-cycle period is short
* enough that the position offset is audible as "rougher" or
* subtly wrong percussion. Reported as libretro-fceumm issue #466
* (Moon8 audio inaccuracy, by NSFPlay author Brad Smith).
*
* Per-channel bisection of moon8.nes against negativeExponent's
* _next branch (which uses the un-mirrored layout from nesdev wiki)
* shows the noise channel as the only meaningful divergence after
* the music kicks in at ~22 s; squares and DMC are bit-identical.
*/
nreg = 0x4000;
for (x = 0; x < 2; x++) {
wlcount[x] = 2048;
if (nesincsize) /* lq mode */
sqacc[x] = ((uint32_t)2048 << 17) / nesincsize;
else
sqacc[x] = 1;
sweepon[x] = 0;
curfreq[x] = 0;
}
wlcount[2] = 1; /* 2048; */
wlcount[3] = 2048;
DMCHaveDMA = DMCHaveSample = 0;
SIRQStat = 0x00;
RawDALatch = 0x00;
TriCount = 0;
TriMode = 0;
tristep = 0;
EnabledChannels = 0;
for (x = 0; x < 4; x++)
lengthcount[x] = 0;
DMCAddressLatch = 0;
DMCSizeLatch = 0;
DMCFormat = 0;
DMCAddress = 0;
DMCSize = 0;
DMCShift = 0;
DMCacc=1;
DMCBitCount=0;
}
void FCEUSND_Power(void) {
int x;
SetNESSoundMap();
memset(PSG, 0x00, sizeof(PSG));
FCEUSND_Reset();
memset(Wave, 0, sizeof(Wave));
memset(WaveHi, 0, sizeof(WaveHi));
memset(&EnvUnits, 0, sizeof(EnvUnits));
for (x = 0; x < 5; x++)
ChannelBC[x] = 0;
soundtsoffs = 0;
IRQFrameMode = 0x1; /* Only initialized by power-on reset, not by soft reset. NRS: don't start with Frame IRQ enabled for greater compatibility. Any game that actually uses frame IRQ will explicitly enable it, anyway. */
LoadDMCPeriod(DMCFormat & 0xF);
/* Reset post-mix filter accumulators. These are file-scope in
* filter.c and were not previously cleared on cart load, so a
* second cart loaded in the same process inherited the first
* cart's IIR state. Audibly minor on its own but breaks
* frame-determinism for the first samples of a new run. */
SexyFilter_Reset();
}
void SetSoundVariables(void) {
int x;
fhinc = PAL ? 16626 : 14915; /* *2 CPU clock rate */
fhinc *= 24;
if (FSettings.SndRate) {
wlookup1[0] = 0;
for (x = 1; x < 32; x++) {
wlookup1[x] = (double)16 * 16 * 16 * 4 * 95.52 / ((double)8128 / (double)x + 100);
if (!FSettings.soundq) wlookup1[x] >>= 4;
}
wlookup2[0] = 0;
for (x = 1; x < 203; x++) {
wlookup2[x] = (double)16 * 16 * 16 * 4 * 163.67 / ((double)24329 / (double)x + 100);
if (!FSettings.soundq) wlookup2[x] >>= 4;
}
if (FSettings.soundq >= 1) {
DoNoise = RDoNoise;
DoTriangle = RDoTriangle;
DoPCM = RDoPCM;
DoSQ1 = RDoSQ1;
DoSQ2 = RDoSQ2;
} else {
/* All five Do* pointers in LQ mode end up at one of two
* worker functions: RDoSQLQ (handles both squares) and
* RDoTriangleNoisePCMLQ (handles tri/noise/PCM).
*
* Pass 6 had stubbed DoSQ2 / DoNoise / DoPCM to Dummyfunc
* here on the reasoning that the workers guard with
* "if (end <= start) return;" and re-entry within one
* FlushEmulateSound is a no-op. That reasoning is
* incorrect: the Do* hooks are also called from
* Write_PSG (sound.c:189) on every APU register write
* AND from FCEU_SoundCPUHook (line 493) on every DMC
* bit advance. Between those callers, sound_timestamp
* grows with each CPU instruction, so each Do* call IS
* legitimately doing work - it flushes pending samples
* up to the current SOUNDTS using the pre-write register
* state, before the write updates the registers. Stubbing
* those hooks to Dummyfunc skips the mid-frame flushes
* and audibly changes output for any game that writes to
* multiple APU registers in sequence (essentially all
* of them). Verified bit-identical regression vs upstream
* for the test_idle ROM (channels enabled with steady
* settings) - the audio diverged starting at the first
* post-init register write. Restored here. */
DoSQ1 = RDoSQLQ;
DoSQ2 = RDoSQLQ;
DoTriangle = RDoTriangleNoisePCMLQ;
DoNoise = RDoTriangleNoisePCMLQ;
DoPCM = RDoTriangleNoisePCMLQ;
}
} else {
DoNoise = DoTriangle = DoPCM = DoSQ1 = DoSQ2 = Dummyfunc;
return;
}
MakeFilters(FSettings.SndRate);
if (GameExpSound.RChange)
GameExpSound.RChange();
nesincsize = (int64_t)(((int64_t)1 << 17) * (double)(PAL ? PAL_CPU : NTSC_CPU) / (FSettings.SndRate * 16));
memset(sqacc, 0, sizeof(sqacc));
memset(ChannelBC, 0, sizeof(ChannelBC));
LoadDMCPeriod(DMCFormat & 0xF); /* For changing from PAL to NTSC */
/* Use double rather than long double here. long double has
* platform-dependent precision (80-bit on x87, 64-bit with
* -mfpmath=sse, 128-bit on some non-x86), so the cast-to-uint32
* result varies across platforms. double is guaranteed 64-bit
* IEEE-754 on every platform we target, keeping soundtsinc
* deterministic across builds for replay/netplay. */
soundtsinc = (uint32_t)((uint64_t)((double)(PAL ? PAL_CPU : NTSC_CPU) * 65536.0) / (FSettings.SndRate * 16));
}
void FCEUI_Sound(int Rate) {
FSettings.SndRate = Rate;
SetSoundVariables();
}
void FCEUI_SetLowPass(int q) {
FSettings.lowpass = q;
}
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();
}
void FCEUI_SetSoundVolume(uint32_t volume) {
FSettings.SoundVolume = volume;
}
SFORMAT FCEUSND_STATEINFO[] = {
{ &fhcnt, 4 | FCEUSTATE_RLSB, "FHCN" },
{ &fcnt, 1, "FCNT" },
{ PSG, 0x10, "PSG" },
{ &EnabledChannels, 1, "ENCH" },
{ &IRQFrameMode, 1, "IQFM" },
{ &nreg, 2 | FCEUSTATE_RLSB, "NREG" },
{ &TriMode, 1, "TRIM" },
{ &TriCount, 1, "TRIC" },
{ &EnvUnits[0].Speed, 1, "E0SP" },
{ &EnvUnits[1].Speed, 1, "E1SP" },
{ &EnvUnits[2].Speed, 1, "E2SP" },
{ &EnvUnits[0].Mode, 1, "E0MO" },
{ &EnvUnits[1].Mode, 1, "E1MO" },
{ &EnvUnits[2].Mode, 1, "E2MO" },
{ &EnvUnits[0].DecCountTo1, 1, "E0D1" },
{ &EnvUnits[1].DecCountTo1, 1, "E1D1" },
{ &EnvUnits[2].DecCountTo1, 1, "E2D1" },
{ &EnvUnits[0].decvolume, 1, "E0DV" },
{ &EnvUnits[1].decvolume, 1, "E1DV" },
{ &EnvUnits[2].decvolume, 1, "E2DV" },
{ &lengthcount[0], 4 | FCEUSTATE_RLSB, "LEN0" },
{ &lengthcount[1], 4 | FCEUSTATE_RLSB, "LEN1" },
{ &lengthcount[2], 4 | FCEUSTATE_RLSB, "LEN2" },
{ &lengthcount[3], 4 | FCEUSTATE_RLSB, "LEN3" },
{ sweepon, 2, "SWEE" },
{ &curfreq[0], 4 | FCEUSTATE_RLSB, "CRF1" },
{ &curfreq[1], 4 | FCEUSTATE_RLSB, "CRF2" },
{ SweepCount, 2, "SWCT" },
{ &SIRQStat, 1, "SIRQ" },
{ &DMCacc, 4 | FCEUSTATE_RLSB, "5ACC" },
{ &DMCBitCount, 1, "5BIT" },
{ &DMCAddress, 4 | FCEUSTATE_RLSB, "5ADD" },
{ &DMCSize, 4 | FCEUSTATE_RLSB, "5SIZ" },
{ &DMCShift, 1, "5SHF" },
{ &DMCHaveDMA, 1, "5VDM" },
{ &DMCHaveSample, 1, "5VSP" },
{ &DMCSizeLatch, 1, "5SZL" },
{ &DMCAddressLatch, 1, "5ADL" },
{ &DMCFormat, 1, "5FMT" },
{ &RawDALatch, 1, "RWDA" },
/* these are important for smooth sound after loading state */
{ &sqacc[0], sizeof(sqacc[0]) | FCEUSTATE_RLSB, "SAC1" },
{ &sqacc[1], sizeof(sqacc[1]) | FCEUSTATE_RLSB, "SAC2" },
{ &RectDutyCount[0], sizeof(RectDutyCount[0]) | FCEUSTATE_RLSB, "RCD1"},
{ &RectDutyCount[1], sizeof(RectDutyCount[1]) | FCEUSTATE_RLSB, "RCD2"},
{ &tristep, sizeof(tristep) | FCEUSTATE_RLSB, "TRIS"},
{ &lq_triacc, sizeof(lq_triacc) | FCEUSTATE_RLSB, "TACC" },
{ &lq_noiseacc, sizeof(lq_noiseacc) | FCEUSTATE_RLSB, "NACC" },
/* less important but still necessary */
{ &ChannelBC[0], sizeof(ChannelBC[0]) | FCEUSTATE_RLSB, "CBC1" },
{ &ChannelBC[1], sizeof(ChannelBC[1]) | FCEUSTATE_RLSB, "CBC2" },
{ &ChannelBC[2], sizeof(ChannelBC[2]) | FCEUSTATE_RLSB, "CBC3" },
{ &ChannelBC[3], sizeof(ChannelBC[3]) | FCEUSTATE_RLSB, "CBC4" },
{ &ChannelBC[4], sizeof(ChannelBC[4]) | FCEUSTATE_RLSB, "CBC5" },
{ &sound_timestamp, sizeof(sound_timestamp) | FCEUSTATE_RLSB, "SNTS" },
{ &soundtsoffs, sizeof(soundtsoffs) | FCEUSTATE_RLSB, "TSOF"},
{ &wlcount[0], sizeof(wlcount[0]) | FCEUSTATE_RLSB, "WLC1" },
{ &wlcount[1], sizeof(wlcount[1]) | FCEUSTATE_RLSB, "WLC2" },
{ &wlcount[2], sizeof(wlcount[2]) | FCEUSTATE_RLSB, "WLC3" },
{ &wlcount[3], sizeof(wlcount[3]) | FCEUSTATE_RLSB, "WLC4" },
{ &sexyfilter_acc1, sizeof(sexyfilter_acc1) | FCEUSTATE_RLSB, "FAC1" },
{ &sexyfilter_acc2, sizeof(sexyfilter_acc2) | FCEUSTATE_RLSB, "FAC2" },
{ &sexyfilter2_acc, sizeof(sexyfilter2_acc) | FCEUSTATE_RLSB, "FAC3" },
{ &lq_tcout, sizeof(lq_tcout) | FCEUSTATE_RLSB, "TCOU"},
/* 2018-12-14 - Wii and possibly other big-endian platforms are having
* issues loading states with this. Increasing it only helps a few games.
* Disabling this state variable for Wii/WiiU/GC for now. */
/* TODO: fix this for better runahead feature for big-endian */
/* UPDATE: Try to ignore this for all big-endian for now */
#ifndef MSB_FIRST
/* wave buffer is used for filtering, only need first 17 values from it */
{ &Wave, 32 * sizeof(int32_t), "WAVE"},
#endif
{ 0 }
};
void FCEUSND_SaveState(void) {
}
void FCEUSND_LoadState(int version) {
int i;
LoadDMCPeriod(DMCFormat & 0xF);
RawDALatch &= 0x7F;
DMCAddress &= 0x7FFF;
/* minimal validation */
for (i = 0; i < 5; i++)
{
uint32_t BC_max = 15;
if (FSettings.soundq == 2)
{
BC_max = 1025;
}
else if (FSettings.soundq == 1)
{
BC_max = 485;
}
if (/* ChannelBC[i] < 0 || */ ChannelBC[i] > BC_max)
{
ChannelBC[i] = 0;
}
}
for (i = 0; i < 4; i++)
{
if (wlcount[i] < 0 || wlcount[i] > 2048)
{
wlcount[i] = 2048;
}
}
for (i = 0; i < 2; i++)
{
if (RectDutyCount[i] < 0 || RectDutyCount[i] > 7)
{
RectDutyCount[i] = 7;
}
}
/* Comparison is always false because access to array >= 0. */
/* if (sound_timestamp < 0)
{
sound_timestamp = 0;
}
if (soundtsoffs < 0)
{
soundtsoffs = 0;
} */
if (soundtsoffs + sound_timestamp >= soundtsinc)
{
soundtsoffs = 0;
sound_timestamp = 0;
}
if (tristep > 32)
{
tristep &= 0x1F;
}
}