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 MMC5SoundChip implements ExpansionSoundChip {
      //really quickly hacked together. Need better interfaces for this kind of thing.
  
      private final Timer[] timers = {new SquareTimer(8, 2), new SquareTimer(8, 2)};
      final private static int[] DUTYLOOKUP = {1, 2, 4, 6};
      private final int[] volume = new int[2];
      private final boolean[] lenCtrEnable = {true, true, true, true};
      private boolean pcmMode, pcmIRQen;
      private int cycles, pcmOut;
  
      @Override
      public final void clock(final int cycle) {
          cycles += cycle;
          if ((cycles % 7445) != cycles) {
              clockframecounter();
              cycles %= 6445;
          }
          timers[0].clock(cycle);
          timers[1].clock(cycle);
      }
  
      @Override
      public void write(int register, int data) {
          switch (register) {
              case 0x0:
                  //length counter 1 halt
                  lenctrHalt[0] = ((data & (utils.BIT5)) != 0);
                  // pulse 1 duty cycle
                  timers[0].setduty(DUTYLOOKUP[data >> 6]);
                  // and envelope
                  envConstVolume[0] = ((data & (utils.BIT4)) != 0);
                  envelopeValue[0] = data & 15;
                  //setvolumes();
                  break;
              case 0x1:
                  //pulse 1 sweep setup
                  //mmc5 lacks a sweep
                  break;
              case 0x2:
                  // pulse 1 timer low bit
                  timers[0].setperiod((timers[0].getperiod() & 0xfe00) + (data << 1));
                  break;
              case 0x3:
                  // length counter load, timer 1 high bits
                  if (lenCtrEnable[0]) {
                      lengthctr[0] = LENCTRLOAD[data >> 3];
                  }
                  timers[0].setperiod((timers[0].getperiod() & 0x1ff) + ((data & 7) << 9));
                  // sequencer restarted
                  timers[0].reset();
                  //envelope also restarted
                  envelopeStartFlag[0] = true;
                  break;
              case 0x4:
                  //length counter 2 halt
                  lenctrHalt[1] = ((data & (utils.BIT5)) != 0);
                  // pulse 2 duty cycle
                  timers[1].setduty(DUTYLOOKUP[data >> 6]);
                  // and envelope
                  envConstVolume[1] = ((data & (utils.BIT4)) != 0);
                  envelopeValue[1] = data & 15;
                  //setvolumes();
                  break;
              case 0x5:
                  //pulse 2 sweep setup
                  break;
              case 0x6:
                  // pulse 2 timer low bit
                  timers[1].setperiod((timers[1].getperiod() & 0xfe00) + (data << 1));
                  break;
              case 0x7:
                  if (lenCtrEnable[1]) {
                      lengthctr[1] = LENCTRLOAD[data >> 3];
                  }
                  timers[1].setperiod((timers[1].getperiod() & 0x1ff) + ((data & 7) << 9));
                  // sequencer restarted
                  timers[1].reset();
                  //envelope also restarted
                  envelopeStartFlag[1] = true;
                  break;
              case 0x10:
                  pcmMode = ((data & (utils.BIT0)) != 0);
                  //true = read mode, false = write mode
                  //read mode watches ALL reads in first 8k of PRG ROM
                  //and writes to dpcm reg
                  //(no way to implement w/o refactors)
                  pcmIRQen = ((data & (utils.BIT7)) != 0);
                  if (pcmIRQen || pcmMode) {
                      System.err.println("Implement the MMC5 PCM IRQ, something's using it!");
                  }
                  break;
              case 0x11:
                  if (!pcmMode) {
                      if (data != 0) {
                          pcmOut = data;
                      } else {
                          //should trip an irq, but no way to in current design
                      }
                  }
                  break;
              default:
                  break;
          }
      }
  
      @Override
      public int getval() {
          int accum = 0;
          for (int i = 0; i < 2; ++i) {
              accum += volume[i] * timers[i].getval() * 750;
          }
          accum += pcmOut << 5;
          return accum;
      }
  
      public int status() {
          return (lengthctr[0] == 0 ? 0 : 1) + (lengthctr[1] == 0 ? 0 : 2);
      }
      private int[] lengthctr = {0, 0, 0, 0};
      private final static int[] LENCTRLOAD = {10, 254, 20, 2, 40, 4, 80, 6,
          160, 8, 60, 10, 14, 12, 26, 14, 12, 16, 24, 18, 48, 20, 96, 22,
          192, 24, 72, 26, 16, 28, 32, 30};
      private final boolean[] lenctrHalt = {true, true, true, true};
  
      private void setlength() {
          for (int i = 0; i < 4; ++i) {
              if (!lenctrHalt[i] && lengthctr[i] > 0) {
                  --lengthctr[i];
                  if (lengthctr[i] == 0) {
                      setvolumes();
                  }
              }
          }
      }
      //instance variables for envelope units
      private final int[] envelopeValue = {15, 15, 15, 15};
      private final int[] envelopeCounter = {0, 0, 0, 0};
      private final int[] envelopePos = {0, 0, 0, 0};
      private final boolean[] envConstVolume = {true, true, true, true};
      private final boolean[] envelopeStartFlag = {false, false, false, false};
  
      private void setenvelope() {
          for (int i = 0; i < 2; ++i) {
              if (envelopeStartFlag[i]) {
                  envelopeStartFlag[i] = false;
                  envelopePos[i] = envelopeValue[i] + 1;
                  envelopeCounter[i] = 15;
              } else {
                  --envelopePos[i];
              }
              if (envelopePos[i] <= 0) {
                  envelopePos[i] = envelopeValue[i] + 1;
                  if (envelopeCounter[i] > 0) {
                      --envelopeCounter[i];
                  } else if (lenctrHalt[i] && envelopeCounter[i] <= 0) {
                      envelopeCounter[i] = 15;
                  }
              }
          }
      }
  
      private void setvolumes() {
          volume[0] = ((lengthctr[0] <= 0) ? 0 : (((envConstVolume[0]) ? envelopeValue[0] : envelopeCounter[0])));
          volume[1] = ((lengthctr[1] <= 0) ? 0 : (((envConstVolume[1]) ? envelopeValue[1] : envelopeCounter[1])));
          //System.err.println("setvolumes " + volume[1]);
      }
      private int framectr = 0;
      private final int ctrmode = 4;
  
      private void clockframecounter() {
          //System.err.println("frame ctr clock " + framectr);
          //should be ~4x a frame, 240 Hz
          //separate timebase from the NES APU though
          if (framectr < 4) {
              setenvelope();
          }
          if ((ctrmode == 4 && (framectr == 1 || framectr == 3))
                  || (ctrmode == 5 && (framectr == 0 || framectr == 2))) {
              setlength();
          }
          ++framectr;
          framectr %= ctrmode;
          setvolumes();
      }
  }
  