content: /*
   * 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;
      }
  
  }
  