diff --git a/src/boards/69.c b/src/boards/69.c index 6bc8748..04f28b6 100644 --- a/src/boards/69.c +++ b/src/boards/69.c @@ -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++) { diff --git a/src/boards/mmc5.c b/src/boards/mmc5.c index 4792385..d4c212f 100644 --- a/src/boards/mmc5.c +++ b/src/boards/mmc5.c @@ -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))) { diff --git a/src/boards/n106.c b/src/boards/n106.c index f4c57c1..5727332 100644 --- a/src/boards/n106.c +++ b/src/boards/n106.c @@ -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; diff --git a/src/boards/vrc6.c b/src/boards/vrc6.c index 60de52f..3224bc5 100644 --- a/src/boards/vrc6.c +++ b/src/boards/vrc6.c @@ -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; diff --git a/src/boards/vrc7.c b/src/boards/vrc7.c index 6b7e10a..cdeedd5 100644 --- a/src/boards/vrc7.c +++ b/src/boards/vrc7.c @@ -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; } diff --git a/src/drivers/libretro/libretro.c b/src/drivers/libretro/libretro.c index a48a42c..5275b8a 100644 --- a/src/drivers/libretro/libretro.c +++ b/src/drivers/libretro/libretro.c @@ -2493,6 +2493,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(); diff --git a/src/drivers/libretro/libretro_core_options.h b/src/drivers/libretro/libretro_core_options.h index e17ca26..3affc04 100644 --- a/src/drivers/libretro/libretro_core_options.h +++ b/src/drivers/libretro/libretro_core_options.h @@ -501,6 +501,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", diff --git a/src/fceu.c b/src/fceu.c index ba1706a..4925662 100644 --- a/src/fceu.c +++ b/src/fceu.c @@ -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; diff --git a/src/fceu.h b/src/fceu.h index 4534588..04ae4ab 100644 --- a/src/fceu.h +++ b/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. */ diff --git a/src/fds_apu.c b/src/fds_apu.c index 0425aa3..a148e16 100644 --- a/src/fds_apu.c +++ b/src/fds_apu.c @@ -379,7 +379,7 @@ static void FDSDoSound(void) { sample = fdso.cwave[(fdso.cwave_pos >> fdso.cwave_pos_shift) & 0x3F] * k * 4; sample = (int32_t)((uint32_t)sample * fds_div_lut[idx]) >> 16; - fdso.sample_out_cache = sample; + fdso.sample_out_cache = GetExpOutput(SND_FDS, sample); } } diff --git a/src/sound.c b/src/sound.c index 4544dd4..926e9ff 100644 --- a/src/sound.c +++ b/src/sound.c @@ -1315,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" }, diff --git a/src/sound.h b/src/sound.h index c1f6dbf..ecd8493 100644 --- a/src/sound.h +++ b/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. */