Compare commits
10 Commits
6911042cf6
...
58e4c11603
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58e4c11603 | ||
|
|
214c77052e | ||
|
|
cf6080af28 | ||
|
|
fef3de8bd9 | ||
|
|
bc5a7a9fec | ||
|
|
2a91d40f4d | ||
|
|
3f23e2b98f | ||
|
|
71bbd10ad0 | ||
|
|
5f8a864bf3 | ||
|
|
d3cd26e922 |
@@ -162,6 +162,7 @@ static void DoAYSQ(int x) {
|
|||||||
int V;
|
int V;
|
||||||
|
|
||||||
amp += amp >> 1;
|
amp += amp >> 1;
|
||||||
|
amp = GetExpOutput(SND_S5B, amp);
|
||||||
|
|
||||||
start = CAYBC[x];
|
start = CAYBC[x];
|
||||||
end = (SOUNDTS << 16) / soundtsinc;
|
end = (SOUNDTS << 16) / soundtsinc;
|
||||||
@@ -186,6 +187,7 @@ static void DoAYSQHQ(int x) {
|
|||||||
int32_t amp = (sreg[0x8 + x] & 15) << 6;
|
int32_t amp = (sreg[0x8 + x] & 15) << 6;
|
||||||
|
|
||||||
amp += amp >> 1;
|
amp += amp >> 1;
|
||||||
|
amp = GetExpOutput(SND_S5B, amp);
|
||||||
|
|
||||||
if (!(sreg[0x7] & (1 << x))) {
|
if (!(sreg[0x7] & (1 << x))) {
|
||||||
for (V = CAYBC[x]; V < SOUNDTS; V++) {
|
for (V = CAYBC[x]; V < SOUNDTS; V++) {
|
||||||
|
|||||||
@@ -489,16 +489,20 @@ static void Do5PCM(void) {
|
|||||||
if (end <= start) return;
|
if (end <= start) return;
|
||||||
MMC5Sound.BC[2] = end;
|
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++)
|
for (V = start; V < end; V++)
|
||||||
Wave[V >> 4] += MMC5Sound.raw << 1;
|
Wave[V >> 4] += amp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Do5PCMHQ(void) {
|
static void Do5PCMHQ(void) {
|
||||||
uint32_t V;
|
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++)
|
for (V = MMC5Sound.BC[2]; V < SOUNDTS; V++)
|
||||||
WaveHi[V] += MMC5Sound.raw << 5;
|
WaveHi[V] += amp;
|
||||||
|
}
|
||||||
MMC5Sound.BC[2] = SOUNDTS;
|
MMC5Sound.BC[2] = SOUNDTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,7 +556,7 @@ static void Do5SQ(int P) {
|
|||||||
MMC5Sound.BC[P] = end;
|
MMC5Sound.BC[P] = end;
|
||||||
|
|
||||||
wl = MMC5Sound.wl[P] + 1;
|
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];
|
rthresh = tal[(MMC5Sound.env[P] & 0xC0) >> 6];
|
||||||
|
|
||||||
if (wl >= 8 && (MMC5Sound.running & (P + 1))) {
|
if (wl >= 8 && (MMC5Sound.running & (P + 1))) {
|
||||||
@@ -582,7 +586,7 @@ static void Do5SQHQ(int P) {
|
|||||||
int32_t amp, rthresh, wl;
|
int32_t amp, rthresh, wl;
|
||||||
|
|
||||||
wl = MMC5Sound.wl[P] + 1;
|
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];
|
rthresh = tal[(MMC5Sound.env[P] & 0xC0) >> 6];
|
||||||
|
|
||||||
if (wl >= 8 && (MMC5Sound.running & (P + 1))) {
|
if (wl >= 8 && (MMC5Sound.running & (P + 1))) {
|
||||||
|
|||||||
@@ -301,13 +301,13 @@ static void DoNamcoSoundHQ(void) {
|
|||||||
envelope = EnvCache[P];
|
envelope = EnvCache[P];
|
||||||
lengo = LengthCache[P];
|
lengo = LengthCache[P];
|
||||||
|
|
||||||
duff2 = FetchDuff(P, envelope);
|
duff2 = GetExpOutput(SND_N163, FetchDuff(P, envelope));
|
||||||
for (V = CVBC << 1; V < (int)SOUNDTS << 1; V++) {
|
for (V = CVBC << 1; V < (int)SOUNDTS << 1; V++) {
|
||||||
WaveHi[V >> 1] += duff2;
|
WaveHi[V >> 1] += duff2;
|
||||||
if (!vco) {
|
if (!vco) {
|
||||||
PlayIndex[P] += freq;
|
PlayIndex[P] += freq;
|
||||||
while ((PlayIndex[P] >> TOINDEX) >= lengo) PlayIndex[P] -= lengo << TOINDEX;
|
while ((PlayIndex[P] >> TOINDEX) >= lengo) PlayIndex[P] -= lengo << TOINDEX;
|
||||||
duff2 = FetchDuff(P, envelope);
|
duff2 = GetExpOutput(SND_N163, FetchDuff(P, envelope));
|
||||||
vco = cyclesuck;
|
vco = cyclesuck;
|
||||||
}
|
}
|
||||||
vco--;
|
vco--;
|
||||||
@@ -349,7 +349,7 @@ static void DoNamcoSound(int32_t *WaveBuf, int Count) {
|
|||||||
if ((IRAM[0x46 + (P << 3)] + PlayIndex[P]) & 1)
|
if ((IRAM[0x46 + (P << 3)] + PlayIndex[P]) & 1)
|
||||||
duff >>= 4;
|
duff >>= 4;
|
||||||
duff &= 0xF;
|
duff &= 0xF;
|
||||||
duff2 = (duff * envelope) >> 19;
|
duff2 = GetExpOutput(SND_N163, (duff * envelope) >> 19);
|
||||||
for (V = 0; V < Count * 16; V++) {
|
for (V = 0; V < Count * 16; V++) {
|
||||||
if (vco >= inc) {
|
if (vco >= inc) {
|
||||||
PlayIndex[P]++;
|
PlayIndex[P]++;
|
||||||
@@ -360,7 +360,7 @@ static void DoNamcoSound(int32_t *WaveBuf, int Count) {
|
|||||||
if ((IRAM[0x46 + (P << 3)] + PlayIndex[P]) & 1)
|
if ((IRAM[0x46 + (P << 3)] + PlayIndex[P]) & 1)
|
||||||
duff >>= 4;
|
duff >>= 4;
|
||||||
duff &= 0xF;
|
duff &= 0xF;
|
||||||
duff2 = (duff * envelope) >> 19;
|
duff2 = GetExpOutput(SND_N163, (duff * envelope) >> 19);
|
||||||
}
|
}
|
||||||
WaveBuf[V >> 4] += duff2;
|
WaveBuf[V >> 4] += duff2;
|
||||||
vco += 0x8000;
|
vco += 0x8000;
|
||||||
|
|||||||
@@ -48,9 +48,28 @@ static void FP_FASTAPASS(1) RAMBO1IRQHook(int a) {
|
|||||||
smallcount += a;
|
smallcount += a;
|
||||||
while (smallcount >= 4) {
|
while (smallcount >= 4) {
|
||||||
smallcount -= 4;
|
smallcount -= 4;
|
||||||
|
/* 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--;
|
IRQCount--;
|
||||||
if (IRQCount == 0xFF)
|
if (IRQCount == 0 && IRQa) {
|
||||||
if (IRQa) X6502_IRQBegin(FCEU_IQEXT);
|
X6502_IRQBegin(FCEU_IQEXT);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,9 +77,20 @@ static void FP_FASTAPASS(1) RAMBO1IRQHook(int a) {
|
|||||||
static void RAMBO1HBHook(void) {
|
static void RAMBO1HBHook(void) {
|
||||||
if ((!IRQmode) && (scanline != 240)) {
|
if ((!IRQmode) && (scanline != 240)) {
|
||||||
rmode = 0;
|
rmode = 0;
|
||||||
|
/* 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--;
|
IRQCount--;
|
||||||
if (IRQCount == 0xFF) {
|
if (IRQCount == 0 && IRQa) {
|
||||||
if (IRQa) {
|
|
||||||
rmode = 1;
|
rmode = 1;
|
||||||
X6502_IRQBegin(FCEU_IQEXT);
|
X6502_IRQBegin(FCEU_IQEXT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,8 @@ static void DoSawV(void);
|
|||||||
|
|
||||||
static INLINE void DoSQV(int x) {
|
static INLINE void DoSQV(int x) {
|
||||||
int32_t V;
|
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;
|
int32_t start, end;
|
||||||
|
|
||||||
start = cvbc[x];
|
start = cvbc[x];
|
||||||
@@ -251,7 +252,8 @@ static void DoSawV(void) {
|
|||||||
}
|
}
|
||||||
if (vcount[2] <= 0)
|
if (vcount[2] <= 0)
|
||||||
goto rea;
|
goto rea;
|
||||||
duff = (((phaseacc >> 3) & 0x1f) << 4) * 6 / 8;
|
duff = GetExpOutput(SND_VRC6,
|
||||||
|
(((phaseacc >> 3) & 0x1f) << 4) * 6 / 8);
|
||||||
}
|
}
|
||||||
Wave[V >> 4] += duff;
|
Wave[V >> 4] += duff;
|
||||||
}
|
}
|
||||||
@@ -260,7 +262,8 @@ static void DoSawV(void) {
|
|||||||
|
|
||||||
static INLINE void DoSQVHQ(int x) {
|
static INLINE void DoSQVHQ(int x) {
|
||||||
int32_t V;
|
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) | 0x2] & 0x80) {
|
||||||
if (vpsg1[x << 2] & 0x80) {
|
if (vpsg1[x << 2] & 0x80) {
|
||||||
@@ -300,7 +303,8 @@ static void DoSawVHQ(void) {
|
|||||||
|
|
||||||
if (vpsg2[2] & 0x80) {
|
if (vpsg2[2] & 0x80) {
|
||||||
for (V = cvbc[2]; V < (int)SOUNDTS; V++) {
|
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]--;
|
vcount[2]--;
|
||||||
if (vcount[2] <= 0) {
|
if (vcount[2] <= 0) {
|
||||||
vcount[2] = (vpsg2[1] + ((vpsg2[2] & 15) << 8) + 1) << 1;
|
vcount[2] = (vpsg2[1] + ((vpsg2[2] & 15) << 8) + 1) << 1;
|
||||||
|
|||||||
@@ -45,26 +45,50 @@ static SFORMAT StateRegs[] =
|
|||||||
|
|
||||||
/* VRC7 Sound */
|
/* 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) {
|
static void DoVRC7Sound(void) {
|
||||||
int32_t z, a;
|
int32_t z, a;
|
||||||
if (FSettings.soundq >= 1)
|
if (FSettings.soundq >= 1)
|
||||||
return;
|
return;
|
||||||
z = ((SOUNDTS << 16) / soundtsinc) >> 4;
|
z = ((SOUNDTS << 16) / soundtsinc) >> 4;
|
||||||
a = z - dwave;
|
a = z - dwave;
|
||||||
OPLL_fillbuf(VRC7Sound, &Wave[dwave], a, 1);
|
VRC7Mix(&Wave[dwave], a, 1);
|
||||||
dwave += a;
|
dwave += a;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void UpdateOPLNEO(int32_t *WaveBuf, int Count) {
|
static void UpdateOPLNEO(int32_t *WaveBuf, int Count) {
|
||||||
OPLL_fillbuf(VRC7Sound, WaveBuf, Count, 4);
|
VRC7Mix(WaveBuf, Count, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void UpdateOPL(int Count) {
|
static void UpdateOPL(int Count) {
|
||||||
int32_t z, a;
|
int32_t z, a;
|
||||||
z = ((SOUNDTS << 16) / soundtsinc) >> 4;
|
z = ((SOUNDTS << 16) / soundtsinc) >> 4;
|
||||||
a = z - dwave;
|
a = z - dwave;
|
||||||
if (VRC7Sound && a)
|
VRC7Mix(&Wave[dwave], a, 1);
|
||||||
OPLL_fillbuf(VRC7Sound, &Wave[dwave], a, 1);
|
|
||||||
dwave = 0;
|
dwave = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ void FCEUI_CheatSearchShowExcluded(void);
|
|||||||
void FCEUI_CheatSearchSetCurrentAsOriginal(void);
|
void FCEUI_CheatSearchSetCurrentAsOriginal(void);
|
||||||
|
|
||||||
void FCEUI_SetLowPass(int q);
|
void FCEUI_SetLowPass(int q);
|
||||||
|
void FCEUI_RemoveTriangleNoise(int d);
|
||||||
|
void FCEUI_ReduceDmcPopping(int d);
|
||||||
|
|
||||||
void FCEUI_NSFSetVis(int mode);
|
void FCEUI_NSFSetVis(int mode);
|
||||||
int FCEUI_NSFChange(int amount);
|
int FCEUI_NSFChange(int amount);
|
||||||
|
|||||||
@@ -10,6 +10,14 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include <libretro.h>
|
#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 <string/stdstring.h>
|
||||||
#include <file/file_path.h>
|
#include <file/file_path.h>
|
||||||
#include <streams/file_stream.h>
|
#include <streams/file_stream.h>
|
||||||
@@ -293,6 +301,9 @@ static bpp_t* fceu_video_out;
|
|||||||
|
|
||||||
/* Some timing-related variables. */
|
/* Some timing-related variables. */
|
||||||
static unsigned sndsamplerate;
|
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 sndquality;
|
||||||
static unsigned sndvolume;
|
static unsigned sndvolume;
|
||||||
unsigned swapDuty;
|
unsigned swapDuty;
|
||||||
@@ -1569,10 +1580,12 @@ static bool update_option_visibility(void)
|
|||||||
struct retro_core_option_display option_display;
|
struct retro_core_option_display option_display;
|
||||||
unsigned i;
|
unsigned i;
|
||||||
unsigned size;
|
unsigned size;
|
||||||
char options_list[][25] = {
|
char options_list[][32] = {
|
||||||
"fceumm_sndvolume",
|
"fceumm_sndvolume",
|
||||||
"fceumm_sndquality",
|
"fceumm_sndquality",
|
||||||
"fceumm_sndlowpass",
|
"fceumm_sndlowpass",
|
||||||
|
"fceumm_removetrianglenoise",
|
||||||
|
"fceumm_reducedmcpopping",
|
||||||
"fceumm_sndstereodelay",
|
"fceumm_sndstereodelay",
|
||||||
"fceumm_swapduty",
|
"fceumm_swapduty",
|
||||||
"fceumm_apu_1",
|
"fceumm_apu_1",
|
||||||
@@ -1970,6 +1983,12 @@ void retro_deinit (void)
|
|||||||
|
|
||||||
void retro_reset(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();
|
ResetNES();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1994,6 +2013,59 @@ static void set_apu_channels(int chan)
|
|||||||
FSettings.PCMVolume = (chan & 0x10) ? 256 : 0;
|
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)
|
static void check_variables(bool startup)
|
||||||
{
|
{
|
||||||
struct retro_variable var = {0};
|
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";
|
var.key = "fceumm_sndquality";
|
||||||
|
|
||||||
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
||||||
@@ -2391,6 +2504,22 @@ static void check_variables(bool startup)
|
|||||||
FCEUI_SetLowPass(lowpass);
|
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";
|
var.key = "fceumm_sndstereodelay";
|
||||||
|
|
||||||
if (environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) && var.value)
|
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);
|
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_dipswitch();
|
||||||
|
|
||||||
update_option_visibility();
|
update_option_visibility();
|
||||||
@@ -3167,7 +3329,17 @@ bool retro_unserialize(const void * data, size_t size)
|
|||||||
if (geniestage == 1)
|
if (geniestage == 1)
|
||||||
return false;
|
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;
|
return false;
|
||||||
|
|
||||||
memstream_set_buffer((uint8_t*)data, size);
|
memstream_set_buffer((uint8_t*)data, size);
|
||||||
@@ -3792,6 +3964,19 @@ bool retro_load_game(const struct retro_game_info *info)
|
|||||||
return false;
|
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)
|
if (palette_switch_enabled)
|
||||||
environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc_ps);
|
environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, desc_ps);
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -292,6 +292,23 @@ struct retro_core_option_v2_definition option_defs[] = {
|
|||||||
"disabled"
|
"disabled"
|
||||||
},
|
},
|
||||||
#endif
|
#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",
|
"fceumm_sndquality",
|
||||||
"Sound Quality",
|
"Sound Quality",
|
||||||
@@ -321,6 +338,34 @@ struct retro_core_option_v2_definition option_defs[] = {
|
|||||||
},
|
},
|
||||||
"disabled",
|
"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",
|
"fceumm_sndstereodelay",
|
||||||
"Stereo Sound Effect",
|
"Stereo Sound Effect",
|
||||||
@@ -473,6 +518,204 @@ struct retro_core_option_v2_definition option_defs[] = {
|
|||||||
},
|
},
|
||||||
"enabled",
|
"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",
|
"fceumm_turbo_enable",
|
||||||
"Turbo Enable",
|
"Turbo Enable",
|
||||||
|
|||||||
@@ -324,6 +324,15 @@ int FCEUI_Initialize(void) {
|
|||||||
FSettings.UsrLastSLine[0] = 231;
|
FSettings.UsrLastSLine[0] = 231;
|
||||||
FSettings.UsrLastSLine[1] = 239;
|
FSettings.UsrLastSLine[1] = 239;
|
||||||
FSettings.SoundVolume = 100;
|
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();
|
FCEUPPU_Init();
|
||||||
X6502_Init();
|
X6502_Init();
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
28
src/fceu.h
28
src/fceu.h
@@ -80,6 +80,12 @@ typedef struct {
|
|||||||
int SquareVolume[2];
|
int SquareVolume[2];
|
||||||
int NoiseVolume;
|
int NoiseVolume;
|
||||||
int PCMVolume;
|
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;
|
int GameGenie;
|
||||||
|
|
||||||
/* Current first and last rendered scanlines. */
|
/* Current first and last rendered scanlines. */
|
||||||
@@ -94,6 +100,28 @@ typedef struct {
|
|||||||
uint32_t SndRate;
|
uint32_t SndRate;
|
||||||
int soundq;
|
int soundq;
|
||||||
int lowpass;
|
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;
|
} FCEUS;
|
||||||
|
|
||||||
extern FCEUS FSettings;
|
extern FCEUS FSettings;
|
||||||
|
|||||||
546
src/fds_apu.c
546
src/fds_apu.c
@@ -2,6 +2,7 @@
|
|||||||
*
|
*
|
||||||
* Copyright notice for this file:
|
* Copyright notice for this file:
|
||||||
* Copyright (C) 2002 Xodnizel
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@@ -18,236 +19,398 @@
|
|||||||
* 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 <string.h>
|
||||||
|
|
||||||
#include "fceu-types.h"
|
#include "fceu-types.h"
|
||||||
#include "x6502.h"
|
#include "x6502.h"
|
||||||
#include "fceu.h"
|
#include "fceu.h"
|
||||||
#include "sound.h"
|
#include "sound.h"
|
||||||
|
#include "filter.h"
|
||||||
#include "state.h"
|
#include "state.h"
|
||||||
#include "fds_apu.h"
|
#include "fds_apu.h"
|
||||||
|
|
||||||
#define FDSClock (1789772.7272727272727272 / 2)
|
#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 {
|
typedef struct {
|
||||||
int64_t cycles; /* Cycles per PCM sample */
|
uint8_t speed;
|
||||||
int64_t count; /* Cycle counter */
|
uint8_t volume;
|
||||||
int64_t envcount; /* Envelope cycle counter */
|
uint8_t control;
|
||||||
uint32_t b19shiftreg60;
|
int32_t counter;
|
||||||
uint32_t b24adder66;
|
} FDSENVUNIT;
|
||||||
uint32_t b24latch68;
|
|
||||||
uint32_t b17latch76;
|
typedef struct {
|
||||||
int32_t clockcount; /* Counter to divide frequency by 8. */
|
int64_t cycles;
|
||||||
uint8_t b8shiftreg88; /* Modulation register. */
|
int64_t count;
|
||||||
uint8_t amplitude[2]; /* Current amplitudes. */
|
|
||||||
uint8_t speedo[2];
|
FDSENVUNIT EnvUnits[2];
|
||||||
uint8_t mwcount;
|
|
||||||
uint8_t mwstart;
|
int32_t env_divider;
|
||||||
uint8_t mwave[0x20]; /* Modulation waveform */
|
|
||||||
uint8_t cwave[0x40]; /* Game-defined waveform(carrier) */
|
uint16_t cwave_freq;
|
||||||
uint8_t SPSG[0xB];
|
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;
|
} FDSSOUND;
|
||||||
|
|
||||||
static FDSSOUND fdso;
|
static const int32_t mod_bias_tab[8] = { 0, 1, 2, 4, 0, -4, -2, -1 };
|
||||||
|
|
||||||
#define SPSG fdso.SPSG
|
static FDSSOUND fdso;
|
||||||
#define b19shiftreg60 fdso.b19shiftreg60
|
static int32_t FBC;
|
||||||
#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
|
|
||||||
|
|
||||||
void FDSSoundStateAdd(void) {
|
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.cwave, 64, 0, "WAVE");
|
||||||
AddExState(fdso.mwave, 32, 0, "MWAV");
|
AddExState(fdso.mwave, 32 * 4, 1, "MWAV");
|
||||||
AddExState(amplitude, 2, 0, "AMPL");
|
|
||||||
AddExState(SPSG, 0xB, 0, "SPSG");
|
|
||||||
|
|
||||||
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(&fdso.mod_freq, 2, 1, "MFRQ");
|
||||||
AddExState(&b19shiftreg60, 4, 1, "B60");
|
AddExState(&fdso.mod_pos, 4, 1, "MPOS");
|
||||||
AddExState(&b24adder66, 4, 1, "B66");
|
AddExState(&fdso.mod_disabled, 1, 0, "MDIS");
|
||||||
AddExState(&b24latch68, 4, 1, "B68");
|
AddExState(&fdso.mod_output, 4, 1, "MCRM");
|
||||||
AddExState(&b17latch76, 4, 1, "B76");
|
|
||||||
}
|
|
||||||
|
|
||||||
static DECLFR(FDSSRead) {
|
AddExState(&fdso.sweep_bias, 4, 1, "SWBS");
|
||||||
switch (A & 0xF) {
|
|
||||||
case 0x0: return(amplitude[0] | (X.DB & 0xC0));
|
AddExState(&fdso.master_control, 1, 0, "MCTL");
|
||||||
case 0x2: return(amplitude[1] | (X.DB & 0xC0));
|
AddExState(&fdso.master_env_speed, 1, 0, "MSPD");
|
||||||
}
|
|
||||||
return(X.DB);
|
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 RenderSound(void);
|
||||||
static void RenderSoundHQ(void);
|
static void RenderSoundHQ(void);
|
||||||
|
|
||||||
static DECLFW(FDSSWrite) {
|
static void FDSSoundUpdate(void) {
|
||||||
if (FSettings.SndRate) {
|
if (FSettings.SndRate) {
|
||||||
if (FSettings.soundq >= 1)
|
if (FSettings.soundq >= 1)
|
||||||
RenderSoundHQ();
|
RenderSoundHQ();
|
||||||
else
|
else
|
||||||
RenderSound();
|
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) {
|
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) {
|
static DECLFW(FDSWaveWrite) {
|
||||||
if (SPSG[0x9] & 0x80)
|
if (fdso.master_control & WAVE_WRITE_MODE)
|
||||||
fdso.cwave[A & 0x3f] = V & 0x3F;
|
fdso.cwave[A & 0x3F] = V & 0x3F;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int ta;
|
static DECLFW(FDSSReg0Write) {
|
||||||
static INLINE void ClockRise(void) {
|
FDSSoundUpdate();
|
||||||
if (!clockcount) {
|
|
||||||
ta++;
|
|
||||||
|
|
||||||
b19shiftreg60 = (SPSG[0x2] | ((SPSG[0x3] & 0xF) << 8));
|
fdso.EnvUnits[EVOL].speed = V & 0x3F;
|
||||||
b17latch76 = (SPSG[0x6] | ((SPSG[0x07] & 0xF) << 8)) + b17latch76;
|
fdso.EnvUnits[EVOL].control = V & 0xC0;
|
||||||
|
fdso.EnvUnits[EVOL].counter = fdso.EnvUnits[EVOL].speed + 1;
|
||||||
|
|
||||||
if (!(SPSG[0x7] & 0x80)) {
|
if (fdso.EnvUnits[EVOL].control & ENV_CTRL_DISABLE)
|
||||||
int t = fdso.mwave[(b17latch76 >> 13) & 0x1F] & 7;
|
fdso.EnvUnits[EVOL].volume = fdso.EnvUnits[EVOL].speed;
|
||||||
int t2 = amplitude[1];
|
}
|
||||||
int adj = 0;
|
|
||||||
|
|
||||||
if ((t & 3)) {
|
static DECLFW(FDSSReg1Write) {
|
||||||
if ((t & 4))
|
FDSSoundUpdate();
|
||||||
adj -= (t2 * ((4 - (t & 3))));
|
|
||||||
|
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--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
else
|
||||||
adj += (t2 * ((t & 3)));
|
temp += 2;
|
||||||
}
|
|
||||||
adj *= 2;
|
|
||||||
if (adj > 0x7F) adj = 0x7F;
|
|
||||||
if (adj < -0x80) adj = -0x80;
|
|
||||||
b8shiftreg88 = 0x80 + adj;
|
|
||||||
} else {
|
} else {
|
||||||
b8shiftreg88 = 0x80;
|
temp /= 256;
|
||||||
}
|
|
||||||
} else {
|
|
||||||
b19shiftreg60 <<= 1;
|
|
||||||
b8shiftreg88 >>= 1;
|
|
||||||
}
|
|
||||||
b24adder66 = (b24latch68 + b19shiftreg60) & 0x1FFFFFF;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static INLINE void ClockFall(void) {
|
if (temp >= 194) temp -= 258;
|
||||||
if ((b8shiftreg88 & 1))
|
if (temp < -64) temp += 256;
|
||||||
b24latch68 = b24adder66;
|
|
||||||
clockcount = (clockcount + 1) & 7;
|
fdso.mod_output = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
static INLINE int32_t FDSDoSound(void) {
|
static int FDSDoModulator(void) {
|
||||||
|
uint32_t prev_mod_pos = fdso.mod_pos;
|
||||||
|
|
||||||
|
if (!fdso.mod_disabled) {
|
||||||
|
uint32_t overflow_mask = 0x3F << fdso.mod_overflow_shift;
|
||||||
|
fdso.mod_pos += fdso.mod_freq;
|
||||||
|
if ((fdso.mod_pos & overflow_mask) != (prev_mod_pos & overflow_mask)) {
|
||||||
|
const int32_t mw = fdso.mwave[(fdso.mod_pos >> fdso.mod_pos_shift) & 0x1F];
|
||||||
|
fdso.sweep_bias = (fdso.sweep_bias + mw) & 0x7FF;
|
||||||
|
if (mw == 0x10)
|
||||||
|
fdso.sweep_bias = 0;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static INLINE void FDSDoCarrier(void) {
|
||||||
|
int32_t cur_cwave_freq = (int32_t)(fdso.cwave_freq << 6);
|
||||||
|
|
||||||
|
if (!fdso.mod_disabled) {
|
||||||
|
cur_cwave_freq += (int32_t)fdso.cwave_freq * fdso.mod_output;
|
||||||
|
if (cur_cwave_freq < 0)
|
||||||
|
cur_cwave_freq = 0;
|
||||||
|
}
|
||||||
|
fdso.cwave_pos += cur_cwave_freq;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FDSClockUnits(void) {
|
||||||
|
if (FDSDoModulator())
|
||||||
|
FDSModUpdateOutput();
|
||||||
|
if (!(fdso.master_control & WAVE_WRITE_MODE) &&
|
||||||
|
!(fdso.cwave_control & WAVE_DISABLE))
|
||||||
|
FDSDoCarrier();
|
||||||
|
if (fdso.master_env_speed) {
|
||||||
|
fdso.env_divider--;
|
||||||
|
if (fdso.env_divider <= 0) {
|
||||||
|
fdso.env_divider += fdso.master_env_speed * fdso.env_count_mul;
|
||||||
|
if (!(fdso.cwave_control & ENVELOPES_DISABLE))
|
||||||
|
FDSDoEnv();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void FDSDoSound(void) {
|
||||||
|
uint32_t prev_cwave_pos = fdso.cwave_pos;
|
||||||
|
|
||||||
|
if (FSettings.soundq >= 1) {
|
||||||
|
FDSClockUnits();
|
||||||
|
} else {
|
||||||
fdso.count += fdso.cycles;
|
fdso.count += fdso.cycles;
|
||||||
if (fdso.count >= ((int64_t)1 << 40)) {
|
if (fdso.count >= ((int64_t)1 << 40)) {
|
||||||
dogk:
|
|
||||||
fdso.count -= (int64_t)1 << 40;
|
fdso.count -= (int64_t)1 << 40;
|
||||||
ClockRise();
|
FDSClockUnits();
|
||||||
ClockFall();
|
}
|
||||||
fdso.envcount--;
|
while (fdso.count >= 32768) {
|
||||||
if (fdso.envcount <= 0) {
|
fdso.count -= (int64_t)1 << 40;
|
||||||
fdso.envcount += SPSG[0xA] * 3;
|
FDSClockUnits();
|
||||||
DoEnv();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (fdso.count >= 32768) goto dogk;
|
|
||||||
|
|
||||||
/* Might need to emulate applying the amplitude to the waveform a bit better... */
|
if ((fdso.cwave_pos ^ prev_cwave_pos) & (1U << fdso.cwave_pos_shift)) {
|
||||||
{
|
/* Master-volume divisor LUT: replaces an integer divide
|
||||||
int k = amplitude[0];
|
* 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;
|
if (k > 0x20) k = 0x20;
|
||||||
return (fdso.cwave[b24latch68 >> 19] * k) * 4 / ((SPSG[0x9] & 0x3) + 2);
|
sample = fdso.cwave[(fdso.cwave_pos >> fdso.cwave_pos_shift) & 0x3F]
|
||||||
|
* k * 4;
|
||||||
|
sample = (int32_t)((uint32_t)sample * fds_div_lut[idx]) >> 16;
|
||||||
|
fdso.sample_out_cache = GetExpOutput(SND_FDS, sample);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static int32_t FBC = 0;
|
|
||||||
|
|
||||||
static void RenderSound(void) {
|
static void RenderSound(void) {
|
||||||
int32_t end, start;
|
int32_t end = (SOUNDTS << 16) / soundtsinc;
|
||||||
|
int32_t start = FBC;
|
||||||
int32_t x;
|
int32_t x;
|
||||||
|
|
||||||
start = FBC;
|
if (end <= start) return;
|
||||||
end = (SOUNDTS << 16) / soundtsinc;
|
|
||||||
if (end <= start)
|
|
||||||
return;
|
|
||||||
FBC = end;
|
FBC = end;
|
||||||
|
|
||||||
if (!(SPSG[0x9] & 0x80))
|
|
||||||
for (x = start; x < end; x++) {
|
for (x = start; x < end; x++) {
|
||||||
uint32_t t = FDSDoSound();
|
uint32_t t;
|
||||||
|
FDSDoSound();
|
||||||
|
t = (uint32_t)fdso.sample_out_cache;
|
||||||
t += t >> 1;
|
t += t >> 1;
|
||||||
t >>= 4;
|
t >>= 4;
|
||||||
Wave[x >> 4] += t; /* (t>>2)-(t>>3); */ /* >>3; */
|
Wave[x >> 4] += (int32_t)t;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void RenderSoundHQ(void) {
|
static void RenderSoundHQ(void) {
|
||||||
uint32_t x;
|
uint32_t x;
|
||||||
|
for (x = (uint32_t)FBC; x < (uint32_t)SOUNDTS; x++) {
|
||||||
if (!(SPSG[0x9] & 0x80))
|
uint32_t t;
|
||||||
for (x = FBC; x < SOUNDTS; x++) {
|
FDSDoSound();
|
||||||
uint32_t t = FDSDoSound();
|
t = (uint32_t)fdso.sample_out_cache;
|
||||||
t += t >> 1;
|
t += t >> 1;
|
||||||
WaveHi[x] += t; /* (t<<2)-(t<<1); */
|
WaveHi[x] += (int32_t)t;
|
||||||
}
|
}
|
||||||
FBC = SOUNDTS;
|
FBC = (int32_t)SOUNDTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void HQSync(int32_t ts) {
|
static void HQSync(int32_t ts) {
|
||||||
@@ -263,35 +426,80 @@ static void FDS_ESI(void) {
|
|||||||
if (FSettings.SndRate) {
|
if (FSettings.SndRate) {
|
||||||
if (FSettings.soundq >= 1) {
|
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 {
|
} else {
|
||||||
fdso.cycles = ((int64_t)1 << 40) * FDSClock;
|
fdso.cycles = ((int64_t)1 << 40) * FDSClock;
|
||||||
fdso.cycles /= FSettings.SndRate * 16;
|
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);
|
SetReadHandler(0x4040, 0x407F, FDSWaveRead);
|
||||||
SetWriteHandler(0x4040, 0x407f, FDSWaveWrite);
|
SetReadHandler(0x4090, 0x4090, FDSEnvVolumeRead);
|
||||||
SetWriteHandler(0x4080, 0x408A, FDSSWrite);
|
SetReadHandler(0x4092, 0x4092, FDSEnvModRead);
|
||||||
SetReadHandler(0x4090, 0x4092, FDSSRead);
|
|
||||||
|
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) {
|
void FDSSoundReset(void) {
|
||||||
memset(&fdso, 0, sizeof(fdso));
|
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();
|
FDS_ESI();
|
||||||
|
|
||||||
GameExpSound.HiSync = HQSync;
|
GameExpSound.HiSync = HQSync;
|
||||||
GameExpSound.HiFill = RenderSoundHQ;
|
GameExpSound.HiFill = RenderSoundHQ;
|
||||||
GameExpSound.Fill = FDSSound;
|
GameExpSound.Fill = FDSSound;
|
||||||
GameExpSound.RChange = FDS_ESI;
|
GameExpSound.RChange = FDS_ESI;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t FDSSoundRead(uint32_t A) {
|
/* Used by FDS-conversion mappers (e.g. some Whirlwind Manu bootlegs) that
|
||||||
if (A >= 0x4040 && A < 0x4080) return FDSWaveRead(A);
|
* surface the FDS audio registers through their own write paths instead of
|
||||||
if (A >= 0x4090 && A < 0x4093) return FDSSRead(A);
|
* the standard FDS BIOS layout. Same signature as before the backport. */
|
||||||
return X.DB;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FDSSoundWrite(uint32_t A, uint8_t V) {
|
void FDSSoundWrite(uint32_t A, uint8_t V) {
|
||||||
if (A >= 0x4040 && A < 0x4080) FDSWaveWrite(A, V);
|
switch (A) {
|
||||||
else if (A >= 0x4080 && A < 0x408B) FDSSWrite(A, V);
|
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) {
|
void FDSSoundPower(void) {
|
||||||
|
|||||||
82
src/sound.c
82
src/sound.c
@@ -313,7 +313,25 @@ static DECLFW(Write_DMCRegs) {
|
|||||||
DMCFormat = V;
|
DMCFormat = V;
|
||||||
break;
|
break;
|
||||||
case 0x01: DoPCM();
|
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)
|
if (RawDALatch)
|
||||||
DMC_7bit = 1;
|
DMC_7bit = 1;
|
||||||
break;
|
break;
|
||||||
@@ -733,10 +751,21 @@ static void RDoSQLQ(void) {
|
|||||||
static void RDoTriangle(void) {
|
static void RDoTriangle(void) {
|
||||||
uint32_t V;
|
uint32_t V;
|
||||||
int32_t tcout = (tristep & 0xF);
|
int32_t tcout = (tristep & 0xF);
|
||||||
|
uint32_t triangle_raw_period = (PSG[0xa] | ((PSG[0xb] & 7) << 8));
|
||||||
if (!(tristep & 0x10)) tcout ^= 0xF;
|
if (!(tristep & 0x10)) tcout ^= 0xF;
|
||||||
tcout = (tcout * 3) << 16; /* (tcout<<1); */
|
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 *start = &WaveHi[ChannelBC[2]];
|
||||||
int32_t count = SOUNDTS - ChannelBC[2];
|
int32_t count = SOUNDTS - ChannelBC[2];
|
||||||
while (count--) {
|
while (count--) {
|
||||||
@@ -1099,7 +1128,30 @@ void FCEUSND_Reset(void) {
|
|||||||
|
|
||||||
fhcnt = fhinc;
|
fhcnt = fhinc;
|
||||||
fcnt = 0;
|
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++) {
|
for (x = 0; x < 2; x++) {
|
||||||
wlcount[x] = 2048;
|
wlcount[x] = 2048;
|
||||||
@@ -1246,6 +1298,14 @@ void FCEUI_SetLowPass(int q) {
|
|||||||
FSettings.lowpass = 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) {
|
void FCEUI_SetSoundQuality(int quality) {
|
||||||
FSettings.soundq = quality;
|
FSettings.soundq = quality;
|
||||||
SetSoundVariables();
|
SetSoundVariables();
|
||||||
@@ -1255,6 +1315,22 @@ void FCEUI_SetSoundVolume(uint32_t volume) {
|
|||||||
FSettings.SoundVolume = 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[] = {
|
SFORMAT FCEUSND_STATEINFO[] = {
|
||||||
{ &fhcnt, 4 | FCEUSTATE_RLSB, "FHCN" },
|
{ &fhcnt, 4 | FCEUSTATE_RLSB, "FHCN" },
|
||||||
|
|||||||
26
src/sound.h
26
src/sound.h
@@ -21,6 +21,32 @@
|
|||||||
#ifndef _FCEU_SOUND_H
|
#ifndef _FCEU_SOUND_H
|
||||||
#define _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 {
|
typedef struct {
|
||||||
void (*Fill)(int Count); /* Low quality ext sound. */
|
void (*Fill)(int Count); /* Low quality ext sound. */
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user