/*
* HalfNES by Andrew Hoffman
* Licensed under the GNU GPL Version 3. See LICENSE file
*/
package com.grapeshot.halfnes.audio;
import com.grapeshot.halfnes.utils;
/**
*
* @author Andrew
*/
public class FDSSoundChip implements ExpansionSoundChip {
//emulates the wavetable channel in the FDS 2C33 sound chip
//(does anything ever read back from these registers? they aren't all write-only)
//io enable: must be set for any other register to work
boolean regEnable = true;
//wavetable RAM (actually only 6 bits wide)
int[] wavetable = new int[64];
int waveAddr, waveOut,
waveAccum; //16 bits
boolean waveWriteEnable; //holds channel at last output value when on and
//allows access to wavetable RAM. does NOT stop wave unit.
//envelopes
boolean volEnvDirection, volEnvDisable, modEnvDirection, modEnvDisable;
int volEnvSpeed, modEnvSpeed, envClockMultiplier = 0xe8;//bios sets it thus?
int pitch; //12 bits
//modulation
boolean modDisable;
int modCtr, //7 bits SIGNED +63 to -64
modFreq, //12 bits
modAccum;
int[] modTable = new int[64]; //3 bits wide
int modTableAddr; //six bits, not settable from register
//Volumes
int masterVol, volGain, modGain;
//the lowpass
int lpaccum;
int modout;
boolean BothEnvDisable, haltWaveAndReset;
@Override
public void clock(int cycles) {
for (int i = 0; i < cycles; ++i) {
runUnits();
}
}
private void runUnits() {
//increment wave accumulator
if ((pitch + modout) > 0 && !haltWaveAndReset) {
waveAccum += (pitch + modout);
if ((waveAccum & 0xffff) != waveAccum) {
//increment wave position on overflow
waveAccum &= 0xffff;
waveAddr = (waveAddr + 1) & 63;
}
}
//increment modulator
if (modFreq > 0 && !modDisable) {
modAccum += modFreq;
if ((modAccum & 0xffff) != modAccum) {
//and when that overflows run the rest of the modulation stuff
modAccum &= 0xffff;
CalculateModulator();
}
} else if (modDisable) {
modAccum = 0;
modout = 0;
}
if (!haltWaveAndReset && !BothEnvDisable && (envClockMultiplier != 0)) {
CalculateEnvelopes();
}
if (!waveWriteEnable) {
waveOut = wavetable[waveAddr];
}
int tmp = (volGain > 32) ? 32 : volGain;
int out = (waveOut * tmp);
//apply master volume attenuator
switch (masterVol) {
case 0:
default:
out *= 8;
break;
case 1:
out *= 5;
break;
case 2:
out *= 4;
break;
case 3:
out *= 3;
break;
}
//do a little lowpass (about 2khz)
out += lpaccum;
lpaccum -= out >> 6;
}
private void CalculateModulator() {
switch (modTable[modTableAddr]) {
case 0:
default:
modCtr += 0;
break;
case 1:
modCtr += 1;
break;
case 2:
modCtr += 2;
break;
case 3:
modCtr += 4;
break;
case 4:
modCtr = 0;
break;
case 5:
modCtr -= 4;
break;
case 6:
modCtr -= 2;
break;
case 7:
modCtr -= 1;
break;
}
modTableAddr = ++modTableAddr & 63;
//wrap mod counter to 7 bits signed again
modCtr = (modCtr << 25) >> 25;
//apply modulator result (code pretty much from nesdev wiki)
//thanks rainwarrior (thrainwarrior)
// pitch = $4082/4083 (12-bit unsigned pitch value)
// counter = $4085 (7-bit signed mod counter)
// gain = $4084 (6-bit unsigned mod gain)
// 1. multiply counter by gain, lose lowest 4 bits of result but "round" in a strange way
int temp = modCtr * modGain;
int remainder = temp & 0xF;
temp >>= 4;
if ((remainder > 0) && ((temp & 0x80) == 0)) {
if (modCtr < 0) {
temp -= 1;
} else {
temp += 2;
}
}
// 2. wrap if a certain range is exceeded
if (temp >= 192) {
temp -= 256;
} else if (temp < -64) {
temp += 256;
}
// 3. multiply result by pitch, then round to nearest while dropping 6 bits
temp = pitch * temp;
remainder = temp & 0x3F;
temp >>= 6;
if (remainder >= 32) {
temp += 1;
}
// final mod result is in temp
modout = temp;
}
int modEnvAccum, volEnvAccum;
private void CalculateEnvelopes() {
if (!modEnvDisable) {
++modEnvAccum;
if (modEnvAccum > (8 * envClockMultiplier * (modEnvSpeed + 1))) {
modEnvAccum = 0;
if (modEnvDirection) {
//increase
if (modGain < 32) {
++modGain;
}
} else {
//decrease
if (modGain > 0) {
--modGain;
}
}
}
}
if (!volEnvDisable) {
++volEnvAccum;
if (volEnvAccum > (8 * envClockMultiplier * (volEnvSpeed + 1))) {
volEnvAccum = 0;
if (volEnvDirection) {
//increase
if (volGain < 32) {
++volGain;
}
} else {
//decrease
if (volGain > 0) {
--volGain;
}
}
}
}
}
@Override
public void write(int register, int data) {
if (register == 0x4023) {
//enable register, must be 1 for anything else to work
regEnable = ((data & (utils.BIT0)) != 0);
}
if (regEnable) {
if (register >= 0x4040 && register <= 0x407f) {
//wavetable write
if (waveWriteEnable) {
wavetable[(register - 0x4040) & 63] = (data & 63);
}
} else if (register == 0x4080) {
//volume envelope enable and speed
volEnvDisable = ((data & (utils.BIT7)) != 0); //ON when it's FALSE
volEnvDirection = ((data & (utils.BIT6)) != 0);
if (volEnvDisable) {
volGain = (data & 63);
}
volEnvSpeed = (data & 63);
volEnvAccum = 0;
} else if (register == 0x4082) {
//low 8 bits of wave frequency
pitch &= 0xf00;
pitch |= (data & 0xff);
} else if (register == 0x4083) {
//frequency high, wave reset and phase
pitch &= 0xff;
pitch |= (data & 0xf) << 8;
haltWaveAndReset = ((data & (utils.BIT7)) != 0);
if (haltWaveAndReset) {
waveAccum = 0;
waveAddr = 0;
}
//uh is it write 1 to enable or DISable here??
//todo: do something with envelope enables bit 6
BothEnvDisable = ((data & (utils.BIT6)) != 0);
} else if (register == 0x4084) {
//modulator envelope enable and speed
modEnvDisable = ((data & (utils.BIT7)) != 0);
modEnvDirection = ((data & (utils.BIT6)) != 0);
if (modEnvDisable) {
modGain = data & 0x3f;
}
modEnvSpeed = data & 0x3f;
modAccum = 0;
modEnvAccum = 0;
} else if (register == 0x4085) {
//set modulator counter directly
//Bio Miracle Bokutte Opa uses this, requires very tight timing
//sign extend
modCtr = ((data & 0x7f) << 25) >> 25;
//modTableAddr = 0;
} else if (register == 0x4086) {
//low 8 bits of mod freq
modFreq &= 0xf00;
modFreq |= (data & 0xff);
} else if (register == 0x4087) {
//high 4 bits of mod freq, reset and phase
modFreq &= 0xff;
modFreq |= (data & 0xf) << 8;
//setting frequency to 0 disables modulation too
//i think this is 1 to disable.
modDisable = ((data & (utils.BIT7)) != 0);
} else if (register == 0x4088) {
//write data to 2 consecutive entries of modulator table
if (modDisable) {
for (int i = 0; i < 2; ++i) {
modTable[modTableAddr] = data & 7;
modTableAddr = (modTableAddr + 1) & 63;
}
}
modAccum = 0; //?
} else if (register == 0x4089) {
//wave write protect and master vol
masterVol = data & 3;
waveWriteEnable = ((data & (utils.BIT7)) != 0);
} else if (register == 0x408A) {
//sets speed of volume and sweep envelopes
//(or 0 to disable them)
//normally 0xff
envClockMultiplier = data;
}
}
}
public int read(int register) {
if ((register >= 0x4040) && (register < 0x4080)) {
return wavetable[register - 0x4040] | 0x40;
} else if (register == 0x4090) {
//volume gain
return volGain;
} else if (register == 0x4092) {
//modulator gain
return modGain;
} else {
//System.err.println("what goes here " + utils.hex(register));
//why are nsfs reading from 4080 and 4084? there's nothing there
return 0x40;
}
}
@Override
public int getval() {
return lpaccum;
}
}