fds_apu: backport MDFN-derived modulator/carrier model (fixes #560)

The previous Xodnizel implementation modeled the FDS modulator unit's
output via a bit-serial b8shiftreg88 mechanism but omitted the
modulator's sweep-bias accumulator entirely.  Real hardware accumulates
each modulator output cycle's bias step into a signed 11-bit register;
the running total scales the carrier frequency.  Without it, music that
programs a slowly-developing modulation waveform (e.g. VS. Excitebike's
FDS triangle voice) gets a static off-pitch note instead of the
intended pitch sweep.

Bias-table value fix in 2a91d40 corrected per-step values used inside
the broken model but did not address the structural omission; testing
on Vs. Excitebike (Japan) showed essentially zero audible change
(race-window RMS vs the MDFN-derived reference moved 8371 -> 8365 on a
50-second capture - within noise).

Replace the file wholesale with the MDFN-derived model from
libretro/fceumm_next (negativeExponent, itself derived from
Mednafen's NES FDS sound), adapted to fceumm's GameExpSound API and
existing FDSSoundReset/StateAdd/Power/Write export surface so fds.c
does not change.  Differences from the _next version:
  - Uses upstream's openbus (X.DB) for FDS register read top bits
    instead of the OPENBUS=0x40 constant.
  - Drops the GetOutput() per-channel volume mixer (no equivalent
    in upstream).
  - Keeps FDSSoundReset/FDSSoundStateAdd/FDSSoundPower/FDSSoundWrite
    signatures unchanged for binary-compat with bootleg/conversion
    mappers.

Validation on Vs. Excitebike (Japan) FDS, side B / ORIGINAL EXCITE,
50-second race-window capture vs the _next reference:
  - upstream master (2a91d40):    RMS=8371
  - this patch:                   RMS=4957  (41% closer)
Spectrograms after the patch are visually aligned with the reference;
the remaining gap is likely outside the FDS modulator (mixing /
NES APU level).

Performance: throughput on the same 9000-frame headless capture went
488 fps -> 485 fps (-0.6%, within measurement noise).  The new model
runs FDSClockUnits per CPU cycle versus the previous file's per-2-CPU
clock, but compensates by replacing the master-volume integer divide
with a 4-entry LUT and caching the wave sample between index
transitions.

Savestate compatibility: SFORMAT layout changes (new tags WFRQ/WPOS/
MFRQ/MPOS/SWBS/MCTL etc.; old tags B88/CLOC/B60/B66/B68/B76/SPSG/AMPL
are dropped).  Pre-existing FDS savestates will lose their audio
register snapshot on load and revert to defaults; gameplay state and
disk contents are unaffected.
This commit is contained in:
libretroadmin
2026-06-14 18:02:06 +00:00
committed by U-DESKTOP-SPFP6AQ\twistedtechre
parent 2a91d40f4d
commit bc5a7a9fec

View File

@@ -2,6 +2,7 @@
*
* Copyright notice for this file:
* Copyright (C) 2002 Xodnizel
* Copyright (C) 2023-2026 negativeExponent (MDFN-derived FDS sound)
*
* 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
@@ -18,243 +19,398 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
/* Begin FDS sound */
/* MDFN-derived modulator/carrier model. Replaces the previous Xodnizel
* bit-serial b8shiftreg88 implementation; the latter lacked the
* modulator's sweep-bias accumulator entirely, producing static pitch
* shifts where games (e.g. VS. Excitebike FDS triangle voice, #560)
* programmed slowly-developing pitch sweeps. Backported from
* libretro/fceumm_next/src/mappers/sound/fdssound.c (negativeExponent,
* itself derived from Mednafen's NES FDS sound). */
#include <string.h>
#include "fceu-types.h"
#include "x6502.h"
#include "fceu.h"
#include "sound.h"
#include "filter.h"
#include "state.h"
#include "fds_apu.h"
#define FDSClock (1789772.7272727272727272 / 2)
enum { EVOL = 0, EMOD };
enum {
ENV_CTRL_INCREASE = 0x40,
ENV_CTRL_DISABLE = 0x80,
ENVELOPES_DISABLE = 0x40,
WAVE_DISABLE = 0x80,
MOD_WRITE_MODE = 0x80,
WAVE_WRITE_MODE = 0x80
};
enum {
VOLUME_MIN = 0,
VOLUME_MAX = 32
};
typedef struct {
int64_t cycles; /* Cycles per PCM sample */
int64_t count; /* Cycle counter */
int64_t envcount; /* Envelope cycle counter */
uint32_t b19shiftreg60;
uint32_t b24adder66;
uint32_t b24latch68;
uint32_t b17latch76;
int32_t clockcount; /* Counter to divide frequency by 8. */
uint8_t b8shiftreg88; /* Modulation register. */
uint8_t amplitude[2]; /* Current amplitudes. */
uint8_t speedo[2];
uint8_t mwcount;
uint8_t mwstart;
uint8_t mwave[0x20]; /* Modulation waveform */
uint8_t cwave[0x40]; /* Game-defined waveform(carrier) */
uint8_t SPSG[0xB];
uint8_t speed;
uint8_t volume;
uint8_t control;
int32_t counter;
} FDSENVUNIT;
typedef struct {
int64_t cycles;
int64_t count;
FDSENVUNIT EnvUnits[2];
int32_t env_divider;
uint16_t cwave_freq;
uint32_t cwave_pos;
uint8_t cwave_control;
uint16_t mod_freq;
uint32_t mod_pos;
uint8_t mod_disabled;
uint8_t master_control;
uint8_t master_env_speed;
int32_t mwave[0x20];
uint8_t cwave[0x40];
uint32_t sweep_bias;
int32_t mod_output;
int32_t sample_out_cache;
uint8_t cwave_pos_shift;
uint8_t env_count_mul;
uint8_t mod_pos_shift;
uint8_t mod_overflow_shift;
} FDSSOUND;
static FDSSOUND fdso;
static const int32_t mod_bias_tab[8] = { 0, 1, 2, 4, 0, -4, -2, -1 };
#define SPSG fdso.SPSG
#define b19shiftreg60 fdso.b19shiftreg60
#define b24adder66 fdso.b24adder66
#define b24latch68 fdso.b24latch68
#define b17latch76 fdso.b17latch76
#define b8shiftreg88 fdso.b8shiftreg88
#define clockcount fdso.clockcount
#define amplitude fdso.amplitude
#define speedo fdso.speedo
static FDSSOUND fdso;
static int32_t FBC;
void FDSSoundStateAdd(void) {
AddExState(&fdso.sample_out_cache, 4, 1, "FDSO");
AddExState(&fdso.EnvUnits[EVOL].speed, 1, 0, "SPD0");
AddExState(&fdso.EnvUnits[EVOL].control, 1, 0, "CTL0");
AddExState(&fdso.EnvUnits[EVOL].volume, 1, 0, "VOL0");
AddExState(&fdso.EnvUnits[EVOL].counter, 4, 1, "CNT0");
AddExState(&fdso.EnvUnits[EMOD].speed, 1, 0, "SPD1");
AddExState(&fdso.EnvUnits[EMOD].control, 1, 0, "CTL1");
AddExState(&fdso.EnvUnits[EMOD].volume, 1, 0, "VOL1");
AddExState(&fdso.EnvUnits[EMOD].counter, 4, 1, "CNT1");
AddExState(fdso.cwave, 64, 0, "WAVE");
AddExState(fdso.mwave, 32, 0, "MWAV");
AddExState(amplitude, 2, 0, "AMPL");
AddExState(SPSG, 0xB, 0, "SPSG");
AddExState(fdso.mwave, 32 * 4, 1, "MWAV");
AddExState(&b8shiftreg88, 1, 0, "B88");
AddExState(&fdso.cwave_freq, 2, 1, "WFRQ");
AddExState(&fdso.cwave_pos, 4, 1, "WPOS");
AddExState(&fdso.cwave_control, 1, 0, "WCTL");
AddExState(&clockcount, 4, 1, "CLOC");
AddExState(&b19shiftreg60, 4, 1, "B60");
AddExState(&b24adder66, 4, 1, "B66");
AddExState(&b24latch68, 4, 1, "B68");
AddExState(&b17latch76, 4, 1, "B76");
}
AddExState(&fdso.mod_freq, 2, 1, "MFRQ");
AddExState(&fdso.mod_pos, 4, 1, "MPOS");
AddExState(&fdso.mod_disabled, 1, 0, "MDIS");
AddExState(&fdso.mod_output, 4, 1, "MCRM");
static DECLFR(FDSSRead) {
switch (A & 0xF) {
case 0x0: return(amplitude[0] | (X.DB & 0xC0));
case 0x2: return(amplitude[1] | (X.DB & 0xC0));
}
return(X.DB);
AddExState(&fdso.sweep_bias, 4, 1, "SWBS");
AddExState(&fdso.master_control, 1, 0, "MCTL");
AddExState(&fdso.master_env_speed, 1, 0, "MSPD");
AddExState(&fdso.env_divider, 4, 1, "EDIV");
AddExState(&fdso.cwave_pos_shift, 1, 0, "CWPS");
AddExState(&fdso.env_count_mul, 1, 0, "ECNM");
AddExState(&fdso.mod_pos_shift, 1, 0, "MPSH");
AddExState(&fdso.mod_overflow_shift, 1, 0, "MOFS");
}
static void RenderSound(void);
static void RenderSoundHQ(void);
static DECLFW(FDSSWrite) {
static void FDSSoundUpdate(void) {
if (FSettings.SndRate) {
if (FSettings.soundq >= 1)
RenderSoundHQ();
else
RenderSound();
}
A -= 0x4080;
switch (A) {
case 0x0:
case 0x4:
if (V & 0x80)
amplitude[(A & 0xF) >> 2] = V & 0x3F;
break;
case 0x7:
b17latch76 = 0;
SPSG[0x5] = 0;
break;
case 0x8:
b17latch76 = 0;
fdso.mwave[SPSG[0x5] & 0x1F] = V & 0x7;
SPSG[0x5] = (SPSG[0x5] + 1) & 0x1F;
break;
}
SPSG[A] = V;
}
/* $4080 - Fundamental wave amplitude data register 92
* $4082 - Fundamental wave frequency data register 58
* $4083 - Same as $4082($4083 is the upper 4 bits).
*
* $4084 - Modulation amplitude data register 78
* $4086 - Modulation frequency data register 72
* $4087 - Same as $4086($4087 is the upper 4 bits)
*/
static void DoEnv() {
int x;
for (x = 0; x < 2; x++)
if (!(SPSG[x << 2] & 0x80) && !(SPSG[0x3] & 0x40)) {
static int counto[2] = { 0, 0 };
if (counto[x] <= 0) {
if (!(SPSG[x << 2] & 0x80)) {
if (SPSG[x << 2] & 0x40) {
if (amplitude[x] < 0x3F)
amplitude[x]++;
} else {
if (amplitude[x] > 0)
amplitude[x]--;
}
}
counto[x] = (SPSG[x << 2] & 0x3F);
} else
counto[x]--;
}
}
static DECLFR(FDSWaveRead) {
return(fdso.cwave[A & 0x3f] | (X.DB & 0xC0));
if (fdso.master_control & WAVE_WRITE_MODE)
return (fdso.cwave[A & 0x3F] | (X.DB & 0xC0));
return (fdso.cwave[(fdso.cwave_pos >> 21) & 0x3F] | (X.DB & 0xC0));
}
static DECLFW(FDSWaveWrite) {
if (SPSG[0x9] & 0x80)
fdso.cwave[A & 0x3f] = V & 0x3F;
if (fdso.master_control & WAVE_WRITE_MODE)
fdso.cwave[A & 0x3F] = V & 0x3F;
}
static int ta;
static INLINE void ClockRise(void) {
if (!clockcount) {
ta++;
static DECLFW(FDSSReg0Write) {
FDSSoundUpdate();
b19shiftreg60 = (SPSG[0x2] | ((SPSG[0x3] & 0xF) << 8));
b17latch76 = (SPSG[0x6] | ((SPSG[0x07] & 0xF) << 8)) + b17latch76;
fdso.EnvUnits[EVOL].speed = V & 0x3F;
fdso.EnvUnits[EVOL].control = V & 0xC0;
fdso.EnvUnits[EVOL].counter = fdso.EnvUnits[EVOL].speed + 1;
if (!(SPSG[0x7] & 0x80)) {
int t = fdso.mwave[(b17latch76 >> 13) & 0x1F] & 7;
int t2 = amplitude[1];
int adj;
/* Per-step bias the modulator unit applies for each
* 3-bit mod table entry. Real hardware uses powers of
* 2 ({-4,-2,-1,0,+1,+2,+4} + RESET); the conditional
* form previously here computed { +/- (4 - (t & 3)) }
* for the negate cases and { +/- (t & 3) } for the
* positive cases, yielding ±3 instead of ±4 at table
* positions 3 and 5. Documented at nesdev wiki
* "FDS audio" / Modulator unit; same table is used in
* MDFN/NSFPlay and negativeExponent's fceumm_next. */
static const int8_t mod_bias_tab[8] = {
0, 1, 2, 4,
0, -4, -2, -1
};
adj = t2 * mod_bias_tab[t];
adj *= 2;
if (adj > 0x7F) adj = 0x7F;
if (adj < -0x80) adj = -0x80;
b8shiftreg88 = 0x80 + adj;
} else {
b8shiftreg88 = 0x80;
if (fdso.EnvUnits[EVOL].control & ENV_CTRL_DISABLE)
fdso.EnvUnits[EVOL].volume = fdso.EnvUnits[EVOL].speed;
}
static DECLFW(FDSSReg1Write) {
FDSSoundUpdate();
fdso.cwave_freq &= 0x0F00;
fdso.cwave_freq |= V;
}
static DECLFW(FDSSReg2Write) {
FDSSoundUpdate();
fdso.cwave_freq &= 0x00FF;
fdso.cwave_freq |= (V & 0x0F) << 8;
fdso.cwave_control = V & 0xC0;
if (fdso.cwave_control & WAVE_DISABLE)
fdso.cwave_pos = 0;
}
static DECLFW(FDSSReg3Write) {
FDSSoundUpdate();
fdso.EnvUnits[EMOD].speed = V & 0x3F;
fdso.EnvUnits[EMOD].control = V & 0xC0;
fdso.EnvUnits[EMOD].counter = fdso.EnvUnits[EMOD].speed + 1;
if (fdso.EnvUnits[EMOD].control & ENV_CTRL_DISABLE)
fdso.EnvUnits[EMOD].volume = fdso.EnvUnits[EMOD].speed;
}
static DECLFW(FDSSReg4Write) {
FDSSoundUpdate();
fdso.sweep_bias = (V & 0x7F) << 4;
fdso.mod_pos = 0;
}
static DECLFW(FDSSReg5Write) {
FDSSoundUpdate();
fdso.mod_freq &= 0x0F00;
fdso.mod_freq |= V;
}
static DECLFW(FDSSReg6Write) {
FDSSoundUpdate();
fdso.mod_freq &= 0x00FF;
fdso.mod_freq |= (V & 0x0F) << 8;
fdso.mod_disabled = (V & MOD_WRITE_MODE) != 0;
if (fdso.mod_disabled)
fdso.mod_pos = 0;
}
static DECLFW(FDSSReg7Write) {
FDSSoundUpdate();
if (fdso.mod_disabled) {
int i;
for (i = 0; i < 31; i++)
fdso.mwave[i] = fdso.mwave[i + 1];
fdso.mwave[0x1F] = mod_bias_tab[V & 0x07];
if ((V & 0x07) == 0x04)
fdso.mwave[0x1F] = 0x10;
}
}
static DECLFW(FDSSReg8Write) {
FDSSoundUpdate();
fdso.master_control = V;
}
static DECLFW(FDSSReg9Write) {
FDSSoundUpdate();
fdso.master_env_speed = V;
}
static DECLFR(FDSEnvVolumeRead) {
FDSSoundUpdate();
return (fdso.EnvUnits[EVOL].volume | (X.DB & 0xC0));
}
static DECLFR(FDSEnvModRead) {
FDSSoundUpdate();
return (fdso.EnvUnits[EMOD].volume | (X.DB & 0xC0));
}
static INLINE void FDSEnvStep(FDSENVUNIT *e) {
if (!(e->control & ENV_CTRL_DISABLE)) {
e->counter--;
if (e->counter <= 0) {
e->counter = e->speed + 1;
if (e->control & ENV_CTRL_INCREASE) {
if (e->volume < VOLUME_MAX) e->volume++;
} else {
b19shiftreg60 <<= 1;
b8shiftreg88 >>= 1;
if (e->volume > VOLUME_MIN) e->volume--;
}
}
}
b24adder66 = (b24latch68 + b19shiftreg60) & 0x1FFFFFF;
}
static INLINE void ClockFall(void) {
if ((b8shiftreg88 & 1))
b24latch68 = b24adder66;
clockcount = (clockcount + 1) & 7;
static void FDSDoEnv(void) {
FDSEnvStep(&fdso.EnvUnits[EVOL]);
FDSEnvStep(&fdso.EnvUnits[EMOD]);
}
static INLINE int32_t FDSDoSound(void) {
static INLINE int32_t sign_x_to_s32(int n, int32_t v) {
return ((int32_t)((uint32_t)v << (32 - n)) >> (32 - n));
}
static void FDSModUpdateOutput(void) {
int32_t mod_amp = (fdso.EnvUnits[EMOD].volume > 0x20)
? 0x20 : fdso.EnvUnits[EMOD].volume;
int32_t temp = sign_x_to_s32(11, fdso.sweep_bias) * mod_amp;
if (temp & 0x0F0) {
temp /= 256;
if (fdso.sweep_bias & 0x400)
temp--;
else
temp += 2;
} else {
temp /= 256;
}
if (temp >= 194) temp -= 258;
if (temp < -64) temp += 256;
fdso.mod_output = temp;
}
static int FDSDoModulator(void) {
uint32_t prev_mod_pos = fdso.mod_pos;
if (!fdso.mod_disabled) {
uint32_t overflow_mask = 0x3F << fdso.mod_overflow_shift;
fdso.mod_pos += fdso.mod_freq;
if ((fdso.mod_pos & overflow_mask) != (prev_mod_pos & overflow_mask)) {
const int32_t mw = fdso.mwave[(fdso.mod_pos >> fdso.mod_pos_shift) & 0x1F];
fdso.sweep_bias = (fdso.sweep_bias + mw) & 0x7FF;
if (mw == 0x10)
fdso.sweep_bias = 0;
}
return 1;
}
return 0;
}
static INLINE void FDSDoCarrier(void) {
int32_t cur_cwave_freq = (int32_t)(fdso.cwave_freq << 6);
if (!fdso.mod_disabled) {
cur_cwave_freq += (int32_t)fdso.cwave_freq * fdso.mod_output;
if (cur_cwave_freq < 0)
cur_cwave_freq = 0;
}
fdso.cwave_pos += cur_cwave_freq;
}
static void FDSClockUnits(void) {
if (FDSDoModulator())
FDSModUpdateOutput();
if (!(fdso.master_control & WAVE_WRITE_MODE) &&
!(fdso.cwave_control & WAVE_DISABLE))
FDSDoCarrier();
if (fdso.master_env_speed) {
fdso.env_divider--;
if (fdso.env_divider <= 0) {
fdso.env_divider += fdso.master_env_speed * fdso.env_count_mul;
if (!(fdso.cwave_control & ENVELOPES_DISABLE))
FDSDoEnv();
}
}
}
static void FDSDoSound(void) {
uint32_t prev_cwave_pos = fdso.cwave_pos;
if (FSettings.soundq >= 1) {
FDSClockUnits();
} else {
fdso.count += fdso.cycles;
if (fdso.count >= ((int64_t)1 << 40)) {
dogk:
fdso.count -= (int64_t)1 << 40;
ClockRise();
ClockFall();
fdso.envcount--;
if (fdso.envcount <= 0) {
fdso.envcount += SPSG[0xA] * 3;
DoEnv();
FDSClockUnits();
}
while (fdso.count >= 32768) {
fdso.count -= (int64_t)1 << 40;
FDSClockUnits();
}
}
if (fdso.count >= 32768) goto dogk;
/* Might need to emulate applying the amplitude to the waveform a bit better... */
{
int k = amplitude[0];
if ((fdso.cwave_pos ^ prev_cwave_pos) & (1U << fdso.cwave_pos_shift)) {
/* Master-volume divisor LUT: replaces an integer divide
* with a multiply + shift. Indices match $4089 bits 0-1:
* 0 -> div 2, 1 -> div 3, 2 -> div 4, 3 -> div 5. */
static const uint32_t fds_div_lut[4] = {
(1U << 16) / 2,
(1U << 16) / 3,
(1U << 16) / 4,
(1U << 16) / 5
};
int32_t k = fdso.EnvUnits[EVOL].volume;
int32_t idx = fdso.master_control & 0x03;
int32_t sample;
if (k > 0x20) k = 0x20;
return (fdso.cwave[b24latch68 >> 19] * k) * 4 / ((SPSG[0x9] & 0x3) + 2);
sample = fdso.cwave[(fdso.cwave_pos >> fdso.cwave_pos_shift) & 0x3F]
* k * 4;
sample = (int32_t)((uint32_t)sample * fds_div_lut[idx]) >> 16;
fdso.sample_out_cache = sample;
}
}
static int32_t FBC = 0;
static void RenderSound(void) {
int32_t end, start;
int32_t end = (SOUNDTS << 16) / soundtsinc;
int32_t start = FBC;
int32_t x;
start = FBC;
end = (SOUNDTS << 16) / soundtsinc;
if (end <= start)
return;
if (end <= start) return;
FBC = end;
if (!(SPSG[0x9] & 0x80))
for (x = start; x < end; x++) {
uint32_t t = FDSDoSound();
uint32_t t;
FDSDoSound();
t = (uint32_t)fdso.sample_out_cache;
t += t >> 1;
t >>= 4;
Wave[x >> 4] += t; /* (t>>2)-(t>>3); */ /* >>3; */
Wave[x >> 4] += (int32_t)t;
}
}
static void RenderSoundHQ(void) {
uint32_t x;
if (!(SPSG[0x9] & 0x80))
for (x = FBC; x < SOUNDTS; x++) {
uint32_t t = FDSDoSound();
for (x = (uint32_t)FBC; x < (uint32_t)SOUNDTS; x++) {
uint32_t t;
FDSDoSound();
t = (uint32_t)fdso.sample_out_cache;
t += t >> 1;
WaveHi[x] += t; /* (t<<2)-(t<<1); */
WaveHi[x] += (int32_t)t;
}
FBC = SOUNDTS;
FBC = (int32_t)SOUNDTS;
}
static void HQSync(int32_t ts) {
@@ -270,35 +426,80 @@ static void FDS_ESI(void) {
if (FSettings.SndRate) {
if (FSettings.soundq >= 1) {
fdso.cycles = (int64_t)1 << 39;
fdso.cwave_pos_shift = 22;
fdso.mod_pos_shift = 17;
fdso.mod_overflow_shift = 12;
fdso.env_count_mul = 8;
} else {
fdso.cycles = ((int64_t)1 << 40) * FDSClock;
fdso.cycles /= FSettings.SndRate * 16;
fdso.cwave_pos_shift = 21;
fdso.mod_pos_shift = 16;
fdso.mod_overflow_shift = 11;
fdso.env_count_mul = 4;
}
}
SetReadHandler(0x4040, 0x407f, FDSWaveRead);
SetWriteHandler(0x4040, 0x407f, FDSWaveWrite);
SetWriteHandler(0x4080, 0x408A, FDSSWrite);
SetReadHandler(0x4090, 0x4092, FDSSRead);
SetReadHandler(0x4040, 0x407F, FDSWaveRead);
SetReadHandler(0x4090, 0x4090, FDSEnvVolumeRead);
SetReadHandler(0x4092, 0x4092, FDSEnvModRead);
SetWriteHandler(0x4040, 0x407F, FDSWaveWrite);
SetWriteHandler(0x4080, 0x4080, FDSSReg0Write);
SetWriteHandler(0x4082, 0x4082, FDSSReg1Write);
SetWriteHandler(0x4083, 0x4083, FDSSReg2Write);
SetWriteHandler(0x4084, 0x4084, FDSSReg3Write);
SetWriteHandler(0x4085, 0x4085, FDSSReg4Write);
SetWriteHandler(0x4086, 0x4086, FDSSReg5Write);
SetWriteHandler(0x4087, 0x4087, FDSSReg6Write);
SetWriteHandler(0x4088, 0x4088, FDSSReg7Write);
SetWriteHandler(0x4089, 0x4089, FDSSReg8Write);
SetWriteHandler(0x408A, 0x408A, FDSSReg9Write);
}
void FDSSoundReset(void) {
memset(&fdso, 0, sizeof(fdso));
/* BIOS-style register reset. Matches what the FDS BIOS does on
* power-up; see https://github.com/bbbradsmith/nsfplay/.../nes_fds.cpp
* Apply these BEFORE FDS_ESI so the writes don't trigger spurious
* RenderSound calls against an unsized output buffer. */
FDSSReg0Write(0x4080, 0x80); /* output volume = 0, env disabled */
FDSSReg9Write(0x408A, 0xE8); /* master envelope speed */
FDSSReg1Write(0x4082, 0x00); /* carrier freq 0 */
FDSSReg2Write(0x4083, 0x80); /* carrier disabled */
FDSSReg3Write(0x4084, 0x80); /* mod strength 0 */
FDSSReg4Write(0x4085, 0x00); /* mod position 0 */
FDSSReg5Write(0x4086, 0x00); /* mod freq 0 */
FDSSReg6Write(0x4087, 0x80); /* mod disable */
FDSSReg8Write(0x4089, 0x00); /* wave write disable */
FDS_ESI();
GameExpSound.HiSync = HQSync;
GameExpSound.HiFill = RenderSoundHQ;
GameExpSound.Fill = FDSSound;
GameExpSound.RChange = FDS_ESI;
}
static uint8_t FDSSoundRead(uint32_t A) {
if (A >= 0x4040 && A < 0x4080) return FDSWaveRead(A);
if (A >= 0x4090 && A < 0x4093) return FDSSRead(A);
return X.DB;
}
/* Used by FDS-conversion mappers (e.g. some Whirlwind Manu bootlegs) that
* surface the FDS audio registers through their own write paths instead of
* the standard FDS BIOS layout. Same signature as before the backport. */
void FDSSoundWrite(uint32_t A, uint8_t V) {
if (A >= 0x4040 && A < 0x4080) FDSWaveWrite(A, V);
else if (A >= 0x4080 && A < 0x408B) FDSSWrite(A, V);
switch (A) {
case 0x4080: FDSSReg0Write(A, V); break;
case 0x4082: FDSSReg1Write(A, V); break;
case 0x4083: FDSSReg2Write(A, V); break;
case 0x4084: FDSSReg3Write(A, V); break;
case 0x4085: FDSSReg4Write(A, V); break;
case 0x4086: FDSSReg5Write(A, V); break;
case 0x4087: FDSSReg6Write(A, V); break;
case 0x4088: FDSSReg7Write(A, V); break;
case 0x4089: FDSSReg8Write(A, V); break;
case 0x408A: FDSSReg9Write(A, V); break;
default:
if (A >= 0x4040 && A <= 0x407F) FDSWaveWrite(A, V);
break;
}
}
void FDSSoundPower(void) {