/* * 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(); } }