Compare commits

...

10 Commits

Author SHA1 Message Date
LibretroAdmin
58e4c11603 libretro: add Sound Samplerate (Hint) core option
Some checks failed
CI / build-ps2 (push) Has been cancelled
The NES has no sample-based audio hardware; sound is synthesized in
realtime, so there is no fixed native output samplerate. Add an audio
core option, "Sound Samplerate (Hint)", offering Auto/32KHz/44KHz/48KHz/
96KHz. Higher rates lower latency and reduce aliasing and the low-pass
"smearing" introduced by frontend resampling, at a small performance
cost.

"Auto" queries the frontend via RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE
and snaps to whichever supported rate is closest to the host output
rate. When the callback is unavailable (older frontends return false) or
returns an implausible value, it falls back to a platform-appropriate
default (32000 Hz on Wii, else 48000 Hz).

Changing the option at runtime re-inits the synthesizer and propagates
the new rate to the frontend via SET_SYSTEM_AV_INFO. The existing core
filter coefficient tables already cover 44100/48000/96000, and the fixed
WaveFinal[2048+512] buffer accommodates the worst-case per-frame fill
(1920 samples at 96 kHz PAL), so no buffer resizing is required.

The experimental env define is provided locally guarded by #ifndef so
the core still builds against older bundled libretro.h headers.
2026-06-15 09:48:04 +02:00
libretroadmin
214c77052e libretro: include turbo toggle phase in savestate (fixes #75)
The libretro driver's per-frame turbo toggle counters
(turbo_button_toggle[MAX_PLAYERS][TURBO_BUTTONS]) are advanced in the
input poll while the turbo button is held, and the modular phase of
that counter drives when the underlying NES button fires inside the
turbo cycle.  These counters were module-level state with no
savestate involvement: not serialized, not reset on retro_reset, not
reset on retro_unserialize.

In netplay, the host and client tick this counter independently from
their own local input.  When the host snapshots state and ships it to
the client to align them, the chunk parser restores every NES
hardware register, mapper register, and core-level setting - but
both ends' turbo counters keep whatever local phase they had.  As
long as the turbo button stays held, the two ends compute different
turbo-fired-this-frame bits, which produce different NES input
sequences, which produce divergent game state.  Issue #75 has been
open on this since 2017.

Two-line fix:

 1. After FCEUI_LoadGame returns successfully in retro_load_game, the
    core has just finished populating SFMDATA[] with all mapper
    chunks via AddExState.  Append one more chunk
    (AddExState(turbo_button_toggle, sizeof(...), 0, "TBTG")) so
    the toggle counters save and restore alongside everything else.
    The "TBTG" tag is unused by any known mapper.
    ReadStateChunk's skip-unknown logic handles older states that
    predate this chunk - the array stays at the value retro_reset
    zero-initialised it to, which is now the correct post-load
    default.

 2. memset turbo_button_toggle to zero in retro_reset.  Without this,
    a held turbo button would resume mid-cycle on Reset and produce
    a different first-frame NES input bit than a fresh boot - small
    but observably non-deterministic, and the same surface that #75
    targets.

Verified by inspecting the serialized state binary:

  - Pre-fix: no TBTG chunk present.
  - Post-fix, after 5 frames of held turbo with delay=3:
      TBTG @ offset 5624, size 8, payload 02 00 00 00 00 00 00 00
      (counter for player-0 turbo-A = 2, as expected from
       5 increments mod (delay+1) = mod 4)

ReadStateChunk + CheckS handle the restore via the standard tag-match
path (state.c:277), which is bit-for-bit identical to how every
mapper chunk gets reloaded.  No additional load-side wiring needed.
2026-06-14 21:29:56 +02:00
libretroadmin
cf6080af28 sound: per-channel expansion-audio volume controls (#512)
Adds six new core options for individually scaling the output of each
emulated Famicom expansion-audio chip:

  - fceumm_apu_fds   - Famicom Disk System 2C33 wavetable
  - fceumm_apu_s5b   - Sunsoft 5B (YM2149F clone)
  - fceumm_apu_n163  - Namco 163
  - fceumm_apu_vrc6  - Konami VRC6
  - fceumm_apu_vrc7  - Konami VRC7 (YM2413-clone)
  - fceumm_apu_mmc5  - Nintendo MMC5 PCM + square

Each is a 0..100 range in steps of 5: 100 (default) is the unscaled
output and is bit-identical to pre-patch builds; 0 silences the chip;
intermediate values do (sample * vol) / 256 with vol in the internal
0..256 range used by the existing per-NES-APU-channel TriangleVolume /
SquareVolume / NoiseVolume / PCMVolume fields.

The original report (#512) asked for on/off toggles; the followup
asked for volume.  This implements volume - on/off falls out as the
endpoints (0/100) of the same control, matching what fceumm_next
exposes.  The three rare PCM/speech chips mentioned in the original
report (NEC uPD7755C/56C Jaleco, Mitsubishi M50805 Bandai) are not
included; they're outside fceumm_next's scope as well and not all of
those chip emulations have a clean output stage to wrap.

Implementation:

 - One new int[6] in FSettings (fceu.h) and an init loop in fceu.c
   that sets all six slots to 256 on startup.  Matches the existing
   per-NES-APU per-channel volume convention so the default mixing
   path stays unchanged.
 - One enum (SND_FDS..SND_MMC5, sound.h) and one helper, GetExpOutput
   (sound.c), that wraps the per-channel scale.  Fast path on the
   default value 256 returns the input unchanged with no multiply, so
   the hot mixing loops keep the same instruction sequence as before.
 - Each of the six chip's audio module wraps its output amplitude
   through GetExpOutput(SND_XXX, amp) at the point where the
   computed sample is written into Wave[] / WaveHi[].  For VRC7 the
   wrap is a small dispatch helper (VRC7Mix) that defers to plain
   OPLL_fillbuf at vol=256 and falls back to a local mix loop
   calling OPLL_calc when scaling is needed - this keeps the
   default path identical to before the patch.  For the FDS module
   the wrap is at the cached sample-out point, reusing the same
   site (and reinstating the GetOutput-style scaling that the
   recent MDFN backport had to drop for lack of the API).
 - libretro.c's check_variables polls the six new options at the
   same point it polls the existing fceumm_apu_1..5 bitmask, via
   a small {SND_XXX, "fceumm_apu_xxx"} table.

Validation on Vs. Excitebike (Japan) FDS, 9000-frame headless capture:

  - default (no opts set):                 md5 9c62be1651...
  - --opt fceumm_apu_fds=100:              md5 9c62be1651... (bit-identical)
  - --opt fceumm_apu_s5b=0 ... mmc5=0:     md5 9c62be1651... (no-op on FDS-only game)
  - --opt fceumm_apu_fds=50:               race-window amplitude std 7662 -> 5623,
                                            FDS contribution scales linearly within
                                            integer rounding (RMS error 30 of 5547)
  - --opt fceumm_apu_fds=0:                race-window std 7662 -> 4457, FDS muted

A non-FDS test ROM (mapper 64, Skull & Crossbones) runs cleanly with
arbitrary expansion-chip options set - those values are no-op for the
chip the game doesn't use.

Performance: GetExpOutput is one bounds check, one array load, one
compare, and on the default value an immediate return.  The mixing
loops compile to the same hot path as before for default settings.
Non-default values add one multiply + one shift per sample, which is
the same cost the per-NES-APU per-channel scaling already pays.
2026-06-14 21:03:03 +02:00
libretroadmin
fef3de8bd9 libretro: relax retro_unserialize size check to permit cross-version state loads
The previous `size != retro_serialize_size()` guard rejected any state
whose serialized length didn't exactly match the current build's
SFORMAT-derived size.  That sounds defensive, but the file format
already carries its own 16-byte header with an explicit `totalsize`
and `ReadStateChunk()` already skips unknown chunk tags safely (size-
bounded `memstream_seek` past the payload), so the outer check adds
no parser safety - it just turns every SFORMAT change into a hard
break of existing saves with no user recourse.

This came up in the recent FDS audio rewrite for #560: the backport
added/removed audio-register chunk tags, leaving pre-rewrite states
240 bytes smaller than the new core's expected size, and users with
in-progress FDS games hit "unserialize FAIL" on first reload after
updating the core.

Accept any buffer that's at least the 16-byte header and at most
4x the current serialize size (a sanity cap against pathological
input).  ReadStateChunk does the actual work of validating each
chunk's size against the header's totalsize, and the FCS magic in
the header rejects non-fceumm states.

Verified on Vs. Excitebike (Japan):
  - pre-rewrite state -> new core: load OK, audio plays normally
  - new-rewrite state -> new core: round-trip OK (same audio as before)
  - 15-byte input:                 rejected (< header size)
  - 4 MB input:                    rejected (> 4 * serialize_size)
2026-06-14 20:19:23 +02:00
libretroadmin
bc5a7a9fec 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.
2026-06-14 20:09:13 +02:00
libretroadmin
2a91d40f4d fds_apu: correct modulator bias values at table indices 3 and 5
The FDS modulator unit's per-step bias table is documented on the
nesdev wiki and used in MDFN, NSFPlay, and negativeExponent's
fceumm_next as { 0, +1, +2, +4, RESET, -4, -2, -1 }.  The conditional
form in this file expanded to { 0, +1, +2, +3, RESET, -3, -2, -1 } -
producing the wrong magnitude at indices 3 and 5 where real hardware
takes a power-of-2 step.

Replace the conditional with a literal lookup table matching the
hardware.  Bit-identical for mod table values 0, 1, 2, 4, 6, 7; only
indices 3 and 5 change.

This is a step toward #560 (VS Excitebike FDS triangle pitch) but
unlikely to resolve it on its own.  The modulator's deeper bug is
that this file is missing the sweep_bias integrator entirely: real
hardware accumulates the bias steps over time and the resulting
running total scales the carrier frequency adjustment, whereas this
implementation applies each mod table entry's bias instantaneously to
the b8shiftreg88 sub-clock gating without accumulation.  Tracked
separately; the structural fix needs the modulator unit rebuilt
around an sweep_bias accumulator (or a wholesale backport of
_next's fds_apu rewrite).
2026-06-14 19:48:17 +02:00
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
libretroadmin
71bbd10ad0 tengen: auto-reload + trigger-on-reach-0 for RAMBO-1 IRQ (#544)
The Tengen 800032 (RAMBO-1) IRQ counter on real hardware auto-reloads
from its latch when it would otherwise underflow, and the IRQ
triggers when the counter REACHES zero (going from 1 to 0) rather
than on the underflow step that follows.

Upstream's earlier model triggered on underflow (count 0 -> 0xFF)
and then continued decrementing through 0xFE, 0xFD, ... until the
game wrote a reload register ($C000/$C001/$E000/$E001 with
rmode==1).  For titles that don't write the reload register on
every IRQ - Skull & Crossbones in 2-player mode is the documented
case (libretro docs Compatibility entry), but Hard Drivin', Klax,
Rolling Thunder, Road Runner, Xenophobe etc. all use the same
mapper - subsequent IRQs after the first underflow are silently
swallowed, displacing the status-bar split point and producing the
screen-shake / glitch reported in #544.

Backport reference: negativeExponent's libretro-fceumm_next commit
7f277a8 ("Cleanup mapper 64/158") plus the Mesen RAMBO-1 model.
This patch is the minimal algorithmic delta against upstream:

  - RAMBO1IRQHook (CPU-cycle mode, IRQmode==1): trigger on
    IRQCount reaching 0, auto-reload from IRQLatch on the next
    clock after the trigger.
  - RAMBO1HBHook (scanline / A12-approximation mode, IRQmode==0):
    same change.  rmode is still set on IRQ fire so existing
    register-reload semantics on $C000/$C001/$E000/$E001 are
    preserved bit-for-bit - the writes do exactly what they did
    before, the only behaviour change is what the IRQ hook itself
    does between writes.
  - IRQLatch == 0 edge case handled (reload yields 0, which is the
    reach-0 trigger condition, so IRQs fire on every clock - the
    documented hardware behaviour at latch=0).

Crucially, this does NOT change:
  - SFORMAT chunk layout (REGS/CMDR/MIRR/RMOD/IRQM/IRQC/IRQA/IRQL
    chunks all still present, same byte sizes, same order).  Old
    savestates continue to load correctly.
  - The write-side dispatch (RAMBO1_Write) for $A000, $8000,
    $8001, $C000, $C001, $E000, $E001 - all behave exactly as
    before, including the rmode==1 conditional reload.
  - Mapper 158 (TENGEN 800037, Alien Syndrome Unl) wiring - it
    shares the RAMBO1 IRQ hook and gets the same fix for free.
  - PRG/CHR/mirroring sync logic.
  - Power-on and StateRestore behaviour.

Build verified: tengen.c -O2 / -O3 / aarch64 -O2 clean.
2026-06-14 17:30:41 +02:00
libretroadmin
5f8a864bf3 sound: add core option to reduce DMC channel popping (fixes #638)
Games like Castlevania II: Simon's Quest drive the DMC DAC directly
via $4011 writes to produce sample-style audio (separate from the
DPCM bit-stream path).  The DAC jumps straight to the newly-written
7-bit value on every write, and those abrupt steps are audible as
clicks/pops when the writes form a low-frequency envelope.

Backport reference: negativeExponent's libretro-fceumm_next applies
a one-step midpoint smoother at the $4011 write site - take the
old DAC value and the new requested value, and store only the
average (rounded toward the new value).  Successive writes still
converge on the target, so percussive samples retain their attack;
they just lose half of the per-write step amplitude, which is the
component that produces the click.

Backported as-is, gated on a new core option
`fceumm_reducedmcpopping` (default disabled, so the DAC
trajectory is bit-exact with the original code when the option is
not set).  Verified to match _next's algebra exactly across the
boundary cases (full-range jumps in both directions, single-step
deltas, idle no-ops).

The DPCM bit-decode playback path (the +/-2 per bit RawDALatch
update further down in the DMC channel inner loop) is intentionally
NOT touched - that path is the well-behaved bit-stream output, no
popping, and smoothing it would attenuate normal samples.

This addresses the second half of issue #638 (the first being the
triangle ultrasonic gate, already landed as d3cd26e).  The
follow-up comment on that issue asked specifically for DMC popping
reduction to land in libretro-fceumm since _next has no Android
port.

Build verified: sound.c -O2 / -O3 / aarch64 -O2 clean, libretro.c
clean.
2026-06-14 17:08:29 +02:00
libretroadmin
d3cd26e922 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.
2026-06-14 17:02:44 +02:00
14 changed files with 1055 additions and 214 deletions

View File

@@ -162,6 +162,7 @@ static void DoAYSQ(int x) {
int V;
amp += amp >> 1;
amp = GetExpOutput(SND_S5B, amp);
start = CAYBC[x];
end = (SOUNDTS << 16) / soundtsinc;
@@ -186,6 +187,7 @@ static void DoAYSQHQ(int x) {
int32_t amp = (sreg[0x8 + x] & 15) << 6;
amp += amp >> 1;
amp = GetExpOutput(SND_S5B, amp);
if (!(sreg[0x7] & (1 << x))) {
for (V = CAYBC[x]; V < SOUNDTS; V++) {

View File

@@ -489,16 +489,20 @@ static void Do5PCM(void) {
if (end <= start) return;
MMC5Sound.BC[2] = end;
if (!(MMC5Sound.rawcontrol & 0x40) && MMC5Sound.raw)
if (!(MMC5Sound.rawcontrol & 0x40) && MMC5Sound.raw) {
int32_t amp = GetExpOutput(SND_MMC5, MMC5Sound.raw << 1);
for (V = start; V < end; V++)
Wave[V >> 4] += MMC5Sound.raw << 1;
Wave[V >> 4] += amp;
}
}
static void Do5PCMHQ(void) {
uint32_t V;
if (!(MMC5Sound.rawcontrol & 0x40) && MMC5Sound.raw)
if (!(MMC5Sound.rawcontrol & 0x40) && MMC5Sound.raw) {
int32_t amp = GetExpOutput(SND_MMC5, MMC5Sound.raw << 5);
for (V = MMC5Sound.BC[2]; V < SOUNDTS; V++)
WaveHi[V] += MMC5Sound.raw << 5;
WaveHi[V] += amp;
}
MMC5Sound.BC[2] = SOUNDTS;
}
@@ -552,7 +556,7 @@ static void Do5SQ(int P) {
MMC5Sound.BC[P] = end;
wl = MMC5Sound.wl[P] + 1;
amp = (MMC5Sound.env[P] & 0xF) << 4;
amp = GetExpOutput(SND_MMC5, (MMC5Sound.env[P] & 0xF) << 4);
rthresh = tal[(MMC5Sound.env[P] & 0xC0) >> 6];
if (wl >= 8 && (MMC5Sound.running & (P + 1))) {
@@ -582,7 +586,7 @@ static void Do5SQHQ(int P) {
int32_t amp, rthresh, wl;
wl = MMC5Sound.wl[P] + 1;
amp = ((MMC5Sound.env[P] & 0xF) << 8);
amp = GetExpOutput(SND_MMC5, ((MMC5Sound.env[P] & 0xF) << 8));
rthresh = tal[(MMC5Sound.env[P] & 0xC0) >> 6];
if (wl >= 8 && (MMC5Sound.running & (P + 1))) {

View File

@@ -301,13 +301,13 @@ static void DoNamcoSoundHQ(void) {
envelope = EnvCache[P];
lengo = LengthCache[P];
duff2 = FetchDuff(P, envelope);
duff2 = GetExpOutput(SND_N163, FetchDuff(P, envelope));
for (V = CVBC << 1; V < (int)SOUNDTS << 1; V++) {
WaveHi[V >> 1] += duff2;
if (!vco) {
PlayIndex[P] += freq;
while ((PlayIndex[P] >> TOINDEX) >= lengo) PlayIndex[P] -= lengo << TOINDEX;
duff2 = FetchDuff(P, envelope);
duff2 = GetExpOutput(SND_N163, FetchDuff(P, envelope));
vco = cyclesuck;
}
vco--;
@@ -349,7 +349,7 @@ static void DoNamcoSound(int32_t *WaveBuf, int Count) {
if ((IRAM[0x46 + (P << 3)] + PlayIndex[P]) & 1)
duff >>= 4;
duff &= 0xF;
duff2 = (duff * envelope) >> 19;
duff2 = GetExpOutput(SND_N163, (duff * envelope) >> 19);
for (V = 0; V < Count * 16; V++) {
if (vco >= inc) {
PlayIndex[P]++;
@@ -360,7 +360,7 @@ static void DoNamcoSound(int32_t *WaveBuf, int Count) {
if ((IRAM[0x46 + (P << 3)] + PlayIndex[P]) & 1)
duff >>= 4;
duff &= 0xF;
duff2 = (duff * envelope) >> 19;
duff2 = GetExpOutput(SND_N163, (duff * envelope) >> 19);
}
WaveBuf[V >> 4] += duff2;
vco += 0x8000;

View File

@@ -48,9 +48,28 @@ static void FP_FASTAPASS(1) RAMBO1IRQHook(int a) {
smallcount += a;
while (smallcount >= 4) {
smallcount -= 4;
IRQCount--;
if (IRQCount == 0xFF)
if (IRQa) X6502_IRQBegin(FCEU_IQEXT);
/* Mesen / _next reference: the Tengen RAMBO-1 IRQ counter
* auto-reloads from the latch when it would otherwise
* underflow, and the IRQ triggers when the counter
* REACHES zero (going from 1 to 0) rather than on the
* underflow step that follows. Upstream's earlier model
* triggered on underflow (count 0 -> 0xFF) and then
* continued decrementing through 0xFE, 0xFD, ... until
* the game wrote a reload register. Match the hardware
* model: trigger on reach-0, and on the following clock
* reload from latch automatically so the counter cycles
* continuously without needing per-IRQ register writes. */
if (IRQCount == 0) {
IRQCount = IRQLatch;
if (IRQCount == 0 && IRQa) {
X6502_IRQBegin(FCEU_IQEXT);
}
} else {
IRQCount--;
if (IRQCount == 0 && IRQa) {
X6502_IRQBegin(FCEU_IQEXT);
}
}
}
}
}
@@ -58,9 +77,20 @@ static void FP_FASTAPASS(1) RAMBO1IRQHook(int a) {
static void RAMBO1HBHook(void) {
if ((!IRQmode) && (scanline != 240)) {
rmode = 0;
IRQCount--;
if (IRQCount == 0xFF) {
if (IRQa) {
/* See comment in RAMBO1IRQHook above for the auto-reload /
* trigger-on-reach-0 model. Same change applies here for the
* scanline-clocked (A12 approximation) IRQ path. */
if (IRQCount == 0) {
IRQCount = IRQLatch;
/* Latch == 0 means fire on every clock - reload yields
* 0 again, which is the reach-0 trigger condition. */
if (IRQCount == 0 && IRQa) {
rmode = 1;
X6502_IRQBegin(FCEU_IQEXT);
}
} else {
IRQCount--;
if (IRQCount == 0 && IRQa) {
rmode = 1;
X6502_IRQBegin(FCEU_IQEXT);
}

View File

@@ -179,7 +179,8 @@ static void DoSawV(void);
static INLINE void DoSQV(int x) {
int32_t V;
int32_t amp = (((vpsg1[x << 2] & 15) << 8) * 6 / 8) >> 4;
int32_t amp = GetExpOutput(SND_VRC6,
(((vpsg1[x << 2] & 15) << 8) * 6 / 8) >> 4);
int32_t start, end;
start = cvbc[x];
@@ -251,7 +252,8 @@ static void DoSawV(void) {
}
if (vcount[2] <= 0)
goto rea;
duff = (((phaseacc >> 3) & 0x1f) << 4) * 6 / 8;
duff = GetExpOutput(SND_VRC6,
(((phaseacc >> 3) & 0x1f) << 4) * 6 / 8);
}
Wave[V >> 4] += duff;
}
@@ -260,7 +262,8 @@ static void DoSawV(void) {
static INLINE void DoSQVHQ(int x) {
int32_t V;
int32_t amp = ((vpsg1[x << 2] & 15) << 8) * 6 / 8;
int32_t amp = GetExpOutput(SND_VRC6,
((vpsg1[x << 2] & 15) << 8) * 6 / 8);
if (vpsg1[(x << 2) | 0x2] & 0x80) {
if (vpsg1[x << 2] & 0x80) {
@@ -300,7 +303,8 @@ static void DoSawVHQ(void) {
if (vpsg2[2] & 0x80) {
for (V = cvbc[2]; V < (int)SOUNDTS; V++) {
WaveHi[V] += (((phaseacc >> 3) & 0x1f) << 8) * 6 / 8;
WaveHi[V] += GetExpOutput(SND_VRC6,
(((phaseacc >> 3) & 0x1f) << 8) * 6 / 8);
vcount[2]--;
if (vcount[2] <= 0) {
vcount[2] = (vpsg2[1] + ((vpsg2[2] & 15) << 8) + 1) << 1;

View File

@@ -45,26 +45,50 @@ static SFORMAT StateRegs[] =
/* VRC7 Sound */
/* Apply per-channel volume (#512) around OPLL_fillbuf. Default volume
* 256 dispatches to the plain OPLL_fillbuf for a bit-identical
* pre-#512 path; volume 0 drops the chip silently; intermediate
* values run a local mix loop that calls OPLL_calc (equivalent to
* the internal calc() when quality=0, which is the VRC7 default).
* The (calc + 32768) << shift formulation matches OPLL_fillbuf's
* inner expression. */
static void VRC7Mix(int32_t *buf, int32_t len, int shift) {
int v;
if (!VRC7Sound || len <= 0)
return;
v = FSettings.ExpVolume[SND_VRC7];
if (v == 0)
return;
if (v == 256) {
OPLL_fillbuf(VRC7Sound, buf, len, shift);
} else {
int32_t i;
for (i = 0; i < len; i++) {
int32_t s = (OPLL_calc(VRC7Sound) + 32768);
buf[i] += ((s * v) / 256) << shift;
}
}
}
static void DoVRC7Sound(void) {
int32_t z, a;
if (FSettings.soundq >= 1)
return;
z = ((SOUNDTS << 16) / soundtsinc) >> 4;
a = z - dwave;
OPLL_fillbuf(VRC7Sound, &Wave[dwave], a, 1);
VRC7Mix(&Wave[dwave], a, 1);
dwave += a;
}
static void UpdateOPLNEO(int32_t *WaveBuf, int Count) {
OPLL_fillbuf(VRC7Sound, WaveBuf, Count, 4);
VRC7Mix(WaveBuf, Count, 4);
}
static void UpdateOPL(int Count) {
int32_t z, a;
z = ((SOUNDTS << 16) / soundtsinc) >> 4;
a = z - dwave;
if (VRC7Sound && a)
OPLL_fillbuf(VRC7Sound, &Wave[dwave], a, 1);
VRC7Mix(&Wave[dwave], a, 1);
dwave = 0;
}

View File

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

View File

@@ -10,6 +10,14 @@
#endif
#include <libretro.h>
/* Older bundled libretro.h headers may not define this experimental
* environment callback. Define it locally so the core still builds and
* can probe for it at runtime (the frontend returns false when absent). */
#ifndef RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE
#define RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE (81 | RETRO_ENVIRONMENT_EXPERIMENTAL)
#endif
#include <string/stdstring.h>
#include <file/file_path.h>
#include <streams/file_stream.h>
@@ -293,6 +301,9 @@ static bpp_t* fceu_video_out;
/* Some timing-related variables. */
static unsigned sndsamplerate;
/* User-selected samplerate hint: 0 = Auto, else an explicit rate in Hz
* (one of the supported 32000/44100/48000/96000 values). */
static unsigned sndsamplerate_hint = 0;
static unsigned sndquality;
static unsigned sndvolume;
unsigned swapDuty;
@@ -1569,10 +1580,12 @@ static bool update_option_visibility(void)
struct retro_core_option_display option_display;
unsigned i;
unsigned size;
char options_list[][25] = {
char options_list[][32] = {
"fceumm_sndvolume",
"fceumm_sndquality",
"fceumm_sndlowpass",
"fceumm_removetrianglenoise",
"fceumm_reducedmcpopping",
"fceumm_sndstereodelay",
"fceumm_swapduty",
"fceumm_apu_1",
@@ -1970,6 +1983,12 @@ void retro_deinit (void)
void retro_reset(void)
{
/* Reset clears the turbo toggle phase so the rapid-fire cycle starts
* at the same point every reset, matching how the core's other
* input-poll state behaves on power-cycle. Without this, a held
* turbo button would resume mid-cycle after Reset and produce a
* different first-frame input than a fresh boot. */
memset(turbo_button_toggle, 0, sizeof(turbo_button_toggle));
ResetNES();
}
@@ -1994,6 +2013,59 @@ static void set_apu_channels(int chan)
FSettings.PCMVolume = (chan & 0x10) ? 256 : 0;
}
/* Resolve the user's samplerate hint into an actual output rate in Hz.
*
* The NES has no sample-based audio hardware; sound is synthesized in
* realtime, so there is no single 'native' rate. We support 32000, 44100,
* 48000 and 96000 Hz. When the hint is "Auto", we query the frontend for
* its target output rate via RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE and
* snap to whichever of our supported rates is closest, minimising the work
* the frontend's resampler has to do (lower latency, less aliasing, no
* low-pass "smearing", better time-domain resolution at the higher rates).
*
* Fallback: if the frontend does not implement the callback (older
* frontends return false), or returns an implausible value, we fall back
* to a platform-appropriate default (32000 Hz on Wii, else 48000 Hz). */
static unsigned resolve_samplerate_hint(void)
{
static const unsigned supported[4] = { 32000, 44100, 48000, 96000 };
/* Explicit user selection always wins over Auto. */
if (sndsamplerate_hint != 0)
return sndsamplerate_hint;
/* Auto: ask the frontend what it is targeting. */
{
unsigned target = 0;
if (environ_cb(RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE, &target) &&
target >= 8000 && target <= 384000)
{
unsigned best = supported[0];
unsigned best_dist = (target > supported[0]) ?
(target - supported[0]) : (supported[0] - target);
int i;
for (i = 1; i < 4; i++)
{
unsigned dist = (target > supported[i]) ?
(target - supported[i]) : (supported[i] - target);
if (dist < best_dist)
{
best_dist = dist;
best = supported[i];
}
}
return best;
}
}
/* Fallback: callback unavailable or returned an implausible value. */
#ifdef GEKKO
return 32000;
#else
return 48000;
#endif
}
static void check_variables(bool startup)
{
struct retro_variable var = {0};
@@ -2368,6 +2440,47 @@ static void check_variables(bool startup)
}
}
var.key = "fceumm_sndrate_hint";
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
{
unsigned oldhint = sndsamplerate_hint;
unsigned oldrate = sndsamplerate;
unsigned newrate;
if (!strcmp(var.value, "32KHz"))
sndsamplerate_hint = 32000;
else if (!strcmp(var.value, "44KHz"))
sndsamplerate_hint = 44100;
else if (!strcmp(var.value, "48KHz"))
sndsamplerate_hint = 48000;
else if (!strcmp(var.value, "96KHz"))
sndsamplerate_hint = 96000;
else /* "Auto" */
sndsamplerate_hint = 0;
newrate = resolve_samplerate_hint();
if (startup)
{
/* During load, just settle the rate before the av_info is first
* reported to the frontend. No SET_SYSTEM_AV_INFO needed yet. */
if (newrate != oldrate)
{
sndsamplerate = newrate;
FCEUI_Sound(sndsamplerate);
}
}
/* Re-init the synthesizer and request a frontend av_info update only
* when the resolved output rate actually changes at runtime. */
else if (sndsamplerate_hint != oldhint || newrate != oldrate)
{
sndsamplerate = newrate;
FCEUI_Sound(sndsamplerate);
audio_video_updated = 2;
}
}
var.key = "fceumm_sndquality";
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
@@ -2391,6 +2504,22 @@ static void check_variables(bool startup)
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_reducedmcpopping";
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
{
int newval = (!strcmp(var.value, "enabled")) ? 1 : 0;
FCEUI_ReduceDmcPopping(newval);
}
var.key = "fceumm_sndstereodelay";
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
@@ -2475,6 +2604,39 @@ static void check_variables(bool startup)
}
set_apu_channels(enable_apu);
/* Per-channel expansion-audio volume controls (#512). Six options
* map 1:1 to SND_FDS..SND_MMC5 indexing FSettings.ExpVolume[]. UI
* values are 0..100 in steps of 5; the internal scale is 0..256
* (matching the convention used for FSettings.SquareVolume[] etc).
* A value of 256 (i.e. UI "100") is the default and leaves the
* mixing path bit-identical to pre-#512 builds. */
{
static const struct { int channel; const char *key; } expvol_opts[] = {
{ SND_FDS, "fceumm_apu_fds" },
{ SND_S5B, "fceumm_apu_s5b" },
{ SND_N163, "fceumm_apu_n163" },
{ SND_VRC6, "fceumm_apu_vrc6" },
{ SND_VRC7, "fceumm_apu_vrc7" },
{ SND_MMC5, "fceumm_apu_mmc5" },
};
size_t j;
for (j = 0; j < sizeof(expvol_opts) / sizeof(expvol_opts[0]); j++)
{
struct retro_variable expv = { 0 };
expv.key = expvol_opts[j].key;
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &expv) && expv.value)
{
int pct = atoi(expv.value);
int newval;
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
newval = (256 * pct) / 100;
if (FSettings.ExpVolume[expvol_opts[j].channel] != newval)
FSettings.ExpVolume[expvol_opts[j].channel] = newval;
}
}
}
update_dipswitch();
update_option_visibility();
@@ -3167,7 +3329,17 @@ bool retro_unserialize(const void * data, size_t size)
if (geniestage == 1)
return false;
if (!data || size != retro_serialize_size())
/* The state file's own 16-byte header carries an explicit totalsize
* and ReadStateChunk already skips unknown chunk tags, so a strict
* size-equality check against the current build's serialize_size is
* not necessary for parser safety - and is actively harmful when
* SFORMAT contents change between builds (recent example: the FDS
* audio rewrite for #560 added/removed chunk tags, leaving older
* savestates a fixed delta smaller than the new core's expected
* size, with no recourse for the user). Accept any buffer at least
* as large as the header; cap the upper end to a generous multiple
* of the current size as a sanity guard against pathological input. */
if (!data || size < 16 || size > retro_serialize_size() * 4)
return false;
memstream_set_buffer((uint8_t*)data, size);
@@ -3792,6 +3964,19 @@ bool retro_load_game(const struct retro_game_info *info)
return false;
}
/* Piggyback the libretro-side turbo toggle phase into the core's
* savestate (#75). FCEUI_LoadGame just finished populating SFMDATA[]
* with the mapper's chunks via AddExState; appending one more chunk
* here for the turbo counters makes them save/restore alongside
* everything else. Without this, the toggle phase isn't part of
* the state - host and client in a netplay session can drift on
* the counter even after exchanging savestates, producing input
* desync whenever the turbo button is held. The "TBTG" tag is
* unused by any mapper, and ReadStateChunk's skip-unknown logic
* handles older states gracefully (the array stays at retro_reset's
* zero initialisation, which is the post-#75-fix steady state). */
AddExState(turbo_button_toggle, sizeof(turbo_button_toggle), 0, "TBTG");
if (palette_switch_enabled)
environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc_ps);
else

View File

@@ -292,6 +292,23 @@ struct retro_core_option_v2_definition option_defs[] = {
"disabled"
},
#endif
{
"fceumm_sndrate_hint",
"Sound Samplerate (Hint)",
NULL,
"The NES has no sample-based audio hardware; sound is synthesized in realtime, so there is no fixed output samplerate. Select the rate the core generates audio at. Higher rates lower latency and reduce aliasing and low-pass 'smearing' from frontend resampling, at a small performance cost. 'Auto' picks whichever supported rate is closest to the frontend's target output rate.",
NULL,
"audio",
{
{ "Auto", NULL },
{ "32KHz", NULL },
{ "44KHz", NULL },
{ "48KHz", NULL },
{ "96KHz", NULL },
{ NULL, NULL },
},
"Auto",
},
{
"fceumm_sndquality",
"Sound Quality",
@@ -321,6 +338,34 @@ struct retro_core_option_v2_definition option_defs[] = {
},
"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_reducedmcpopping",
"Reduce DMC Channel Popping",
NULL,
"Smooth direct writes to the DMC DAC register ($4011) by stepping the DAC only halfway toward each new value. Reduces the audible click when games (Castlevania II: Simon's Quest, etc.) pulse the DAC for manual sample playback. Normal DPCM bit-stream sample playback is unaffected.",
NULL,
"audio",
{
{ "disabled", NULL },
{ "enabled", NULL },
{ NULL, NULL },
},
"disabled",
},
{
"fceumm_sndstereodelay",
"Stereo Sound Effect",
@@ -473,6 +518,204 @@ struct retro_core_option_v2_definition option_defs[] = {
},
"enabled",
},
{
"fceumm_apu_fds",
"Channel Volume (FDS)",
NULL,
"Scale Famicom Disk System 2C33 wavetable expansion-audio output (0 = mute, 100 = unscaled).",
NULL,
"audio",
{
{ "0", NULL },
{ "5", NULL },
{ "10", NULL },
{ "15", NULL },
{ "20", NULL },
{ "25", NULL },
{ "30", NULL },
{ "35", NULL },
{ "40", NULL },
{ "45", NULL },
{ "50", NULL },
{ "55", NULL },
{ "60", NULL },
{ "65", NULL },
{ "70", NULL },
{ "75", NULL },
{ "80", NULL },
{ "85", NULL },
{ "90", NULL },
{ "95", NULL },
{ "100", NULL },
{ NULL, NULL },
},
"100",
},
{
"fceumm_apu_s5b",
"Channel Volume (S5B)",
NULL,
"Scale Sunsoft 5B (YM2149F) expansion-audio output (0 = mute, 100 = unscaled).",
NULL,
"audio",
{
{ "0", NULL },
{ "5", NULL },
{ "10", NULL },
{ "15", NULL },
{ "20", NULL },
{ "25", NULL },
{ "30", NULL },
{ "35", NULL },
{ "40", NULL },
{ "45", NULL },
{ "50", NULL },
{ "55", NULL },
{ "60", NULL },
{ "65", NULL },
{ "70", NULL },
{ "75", NULL },
{ "80", NULL },
{ "85", NULL },
{ "90", NULL },
{ "95", NULL },
{ "100", NULL },
{ NULL, NULL },
},
"100",
},
{
"fceumm_apu_n163",
"Channel Volume (N163)",
NULL,
"Scale Namco 163 expansion-audio output (0 = mute, 100 = unscaled).",
NULL,
"audio",
{
{ "0", NULL },
{ "5", NULL },
{ "10", NULL },
{ "15", NULL },
{ "20", NULL },
{ "25", NULL },
{ "30", NULL },
{ "35", NULL },
{ "40", NULL },
{ "45", NULL },
{ "50", NULL },
{ "55", NULL },
{ "60", NULL },
{ "65", NULL },
{ "70", NULL },
{ "75", NULL },
{ "80", NULL },
{ "85", NULL },
{ "90", NULL },
{ "95", NULL },
{ "100", NULL },
{ NULL, NULL },
},
"100",
},
{
"fceumm_apu_vrc6",
"Channel Volume (VRC6)",
NULL,
"Scale Konami VRC6 expansion-audio output (0 = mute, 100 = unscaled).",
NULL,
"audio",
{
{ "0", NULL },
{ "5", NULL },
{ "10", NULL },
{ "15", NULL },
{ "20", NULL },
{ "25", NULL },
{ "30", NULL },
{ "35", NULL },
{ "40", NULL },
{ "45", NULL },
{ "50", NULL },
{ "55", NULL },
{ "60", NULL },
{ "65", NULL },
{ "70", NULL },
{ "75", NULL },
{ "80", NULL },
{ "85", NULL },
{ "90", NULL },
{ "95", NULL },
{ "100", NULL },
{ NULL, NULL },
},
"100",
},
{
"fceumm_apu_vrc7",
"Channel Volume (VRC7)",
NULL,
"Scale Konami VRC7 (YM2413-clone) expansion-audio output (0 = mute, 100 = unscaled).",
NULL,
"audio",
{
{ "0", NULL },
{ "5", NULL },
{ "10", NULL },
{ "15", NULL },
{ "20", NULL },
{ "25", NULL },
{ "30", NULL },
{ "35", NULL },
{ "40", NULL },
{ "45", NULL },
{ "50", NULL },
{ "55", NULL },
{ "60", NULL },
{ "65", NULL },
{ "70", NULL },
{ "75", NULL },
{ "80", NULL },
{ "85", NULL },
{ "90", NULL },
{ "95", NULL },
{ "100", NULL },
{ NULL, NULL },
},
"100",
},
{
"fceumm_apu_mmc5",
"Channel Volume (MMC5)",
NULL,
"Scale Nintendo MMC5 PCM + square expansion-audio output (0 = mute, 100 = unscaled).",
NULL,
"audio",
{
{ "0", NULL },
{ "5", NULL },
{ "10", NULL },
{ "15", NULL },
{ "20", NULL },
{ "25", NULL },
{ "30", NULL },
{ "35", NULL },
{ "40", NULL },
{ "45", NULL },
{ "50", NULL },
{ "55", NULL },
{ "60", NULL },
{ "65", NULL },
{ "70", NULL },
{ "75", NULL },
{ "80", NULL },
{ "85", NULL },
{ "90", NULL },
{ "95", NULL },
{ "100", NULL },
{ NULL, NULL },
},
"100",
},
{
"fceumm_turbo_enable",
"Turbo Enable",

View File

@@ -324,6 +324,15 @@ int FCEUI_Initialize(void) {
FSettings.UsrLastSLine[0] = 231;
FSettings.UsrLastSLine[1] = 239;
FSettings.SoundVolume = 100;
/* Default expansion-audio channel volumes to 256 (unscaled). Mirrors
* the convention used for the NES APU per-channel volume fields:
* a value of 256 in any of these slots leaves the corresponding
* mixing path bit-identical to pre-#512 builds. */
{
int i;
for (i = 0; i < (int)(sizeof(FSettings.ExpVolume) / sizeof(FSettings.ExpVolume[0])); i++)
FSettings.ExpVolume[i] = 256;
}
FCEUPPU_Init();
X6502_Init();
return 1;

View File

@@ -80,6 +80,12 @@ typedef struct {
int SquareVolume[2];
int NoiseVolume;
int PCMVolume;
int ExpVolume[6]; /* Per-channel expansion-audio volume,
* indexed by SND_FDS..SND_MMC5 (see
* src/sound.h). Scale is 0-256 where 256 =
* unscaled output and 0 = silence. Default
* 256 leaves output bit-identical to builds
* predating issue #512. */
int GameGenie;
/* Current first and last rendered scanlines. */
@@ -94,6 +100,28 @@ typedef struct {
uint32_t SndRate;
int soundq;
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. */
int ReduceDMCPopping; /* Smooth direct writes to the DMC DAC
* register ($4011) by stepping the DAC
* only halfway from its previous value
* toward the newly written value, which
* attenuates the audible click games
* like Castlevania II generate when they
* pulse the DAC for manual sample
* playback. Normal DPCM bit-decode
* playback (the +/-2 per bit update) is
* NOT affected. Default off so the DAC
* trajectory is bit-exact when the
* option is not set. */
} FCEUS;
extern FCEUS FSettings;

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
@@ -15,239 +16,401 @@
*
* 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
* 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 = 0;
if (fdso.EnvUnits[EVOL].control & ENV_CTRL_DISABLE)
fdso.EnvUnits[EVOL].volume = fdso.EnvUnits[EVOL].speed;
}
if ((t & 3)) {
if ((t & 4))
adj -= (t2 * ((4 - (t & 3))));
else
adj += (t2 * ((t & 3)));
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 {
if (e->volume > VOLUME_MIN) e->volume--;
}
adj *= 2;
if (adj > 0x7F) adj = 0x7F;
if (adj < -0x80) adj = -0x80;
b8shiftreg88 = 0x80 + adj;
} else {
b8shiftreg88 = 0x80;
}
}
}
static void FDSDoEnv(void) {
FDSEnvStep(&fdso.EnvUnits[EVOL]);
FDSEnvStep(&fdso.EnvUnits[EMOD]);
}
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 {
b19shiftreg60 <<= 1;
b8shiftreg88 >>= 1;
temp /= 256;
}
b24adder66 = (b24latch68 + b19shiftreg60) & 0x1FFFFFF;
if (temp >= 194) temp -= 258;
if (temp < -64) temp += 256;
fdso.mod_output = temp;
}
static INLINE void ClockFall(void) {
if ((b8shiftreg88 & 1))
b24latch68 = b24adder66;
clockcount = (clockcount + 1) & 7;
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 int32_t FDSDoSound(void) {
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();
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();
}
}
if (fdso.count >= 32768) goto dogk;
/* Might need to emulate applying the amplitude to the waveform a bit better... */
{
int k = amplitude[0];
if (k > 0x20) k = 0x20;
return (fdso.cwave[b24latch68 >> 19] * k) * 4 / ((SPSG[0x9] & 0x3) + 2);
}
}
static int32_t FBC = 0;
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)) {
fdso.count -= (int64_t)1 << 40;
FDSClockUnits();
}
while (fdso.count >= 32768) {
fdso.count -= (int64_t)1 << 40;
FDSClockUnits();
}
}
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;
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 = GetExpOutput(SND_FDS, sample);
}
}
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();
t += t >> 1;
t >>= 4;
Wave[x >> 4] += t; /* (t>>2)-(t>>3); */ /* >>3; */
}
for (x = start; x < end; x++) {
uint32_t t;
FDSDoSound();
t = (uint32_t)fdso.sample_out_cache;
t += t >> 1;
t >>= 4;
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();
t += t >> 1;
WaveHi[x] += t; /* (t<<2)-(t<<1); */
}
FBC = SOUNDTS;
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] += (int32_t)t;
}
FBC = (int32_t)SOUNDTS;
}
static void HQSync(int32_t ts) {
@@ -262,36 +425,81 @@ static void FDSSound(int c) {
static void FDS_ESI(void) {
if (FSettings.SndRate) {
if (FSettings.soundq >= 1) {
fdso.cycles = (int64_t)1 << 39;
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.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.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) {

View File

@@ -313,7 +313,25 @@ static DECLFW(Write_DMCRegs) {
DMCFormat = V;
break;
case 0x01: DoPCM();
RawDALatch = V & 0x7F;
{
/* $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;
@@ -733,10 +751,21 @@ static void RDoSQLQ(void) {
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); */
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 count = SOUNDTS - ChannelBC[2];
while (count--) {
@@ -1099,7 +1128,30 @@ void FCEUSND_Reset(void) {
fhcnt = fhinc;
fcnt = 0;
nreg = 1;
/* 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;
@@ -1246,6 +1298,14 @@ 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();
@@ -1255,6 +1315,22 @@ void FCEUI_SetSoundVolume(uint32_t volume) {
FSettings.SoundVolume = volume;
}
/* Per-channel expansion-audio volume scaling. See sound.h for context.
* Hot-path consideration: the common case (vol == 256, the default)
* returns immediately with no multiply, keeping the existing
* bit-identical behaviour on builds where the new options haven't
* been touched. When vol is 0 the channel is silenced cleanly
* regardless of the input sample. */
int32_t GetExpOutput(int channel, int32_t in) {
int v;
if ((unsigned)channel >= (unsigned)SND_EXP_LAST)
return in;
v = FSettings.ExpVolume[channel];
if (v == 256) return in;
if (v == 0) return 0;
return (in * v) / 256;
}
SFORMAT FCEUSND_STATEINFO[] = {
{ &fhcnt, 4 | FCEUSTATE_RLSB, "FHCN" },

View File

@@ -21,6 +21,32 @@
#ifndef _FCEU_SOUND_H
#define _FCEU_SOUND_H
#include "fceu-types.h"
/* Identifiers for the expansion-audio channels exposed to the
* per-channel volume API (#512). Indices into FSettings.ExpVolume[].
* Numbering is internal-only - the savestate format and core options
* key names do not depend on these values - but the count must match
* FSettings.ExpVolume[]'s declared length in fceu.h. */
enum {
SND_FDS = 0, /* Famicom Disk System 2C33 wavetable */
SND_S5B = 1, /* Sunsoft 5B / YM2149F (mapper 69) */
SND_N163 = 2, /* Namco 163 (mapper 19 / "n106") */
SND_VRC6 = 3, /* Konami VRC6 (mappers 24, 26) */
SND_VRC7 = 4, /* Konami VRC7 / YM2413-clone (mapper 85) */
SND_MMC5 = 5, /* Nintendo MMC5 PCM + square (mapper 5) */
SND_EXP_LAST = 6
};
/* Apply the per-channel volume modifier from FSettings.ExpVolume[].
* Scale is 0..256: 256 (default) returns the sample unchanged so the
* hot path stays free of multiplies on builds where the user has not
* touched the new options; 0 returns silence; intermediate values
* scale the sample by (in * vol) / 256. Used by the six expansion-
* audio modules (see fds_apu.c, boards/vrc6.c, boards/vrc7.c,
* boards/n106.c, boards/mmc5.c, boards/69.c). */
int32_t GetExpOutput(int channel, int32_t in);
typedef struct {
void (*Fill)(int Count); /* Low quality ext sound. */