Compare commits
10 Commits
6911042cf6
...
supergame-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58e4c11603 | ||
|
|
214c77052e | ||
|
|
cf6080af28 | ||
|
|
fef3de8bd9 | ||
|
|
bc5a7a9fec | ||
|
|
2a91d40f4d | ||
|
|
3f23e2b98f | ||
|
|
71bbd10ad0 | ||
|
|
5f8a864bf3 | ||
|
|
d3cd26e922 |
@@ -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++) {
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
28
src/fceu.h
28
src/fceu.h
@@ -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;
|
||||
|
||||
578
src/fds_apu.c
578
src/fds_apu.c
@@ -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) {
|
||||
|
||||
82
src/sound.c
82
src/sound.c
@@ -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" },
|
||||
|
||||
26
src/sound.h
26
src/sound.h
@@ -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. */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user