/* * HalfNES by Andrew Hoffman * Licensed under the GNU GPL Version 3. See LICENSE file */ package com.grapeshot.halfnes; import com.grapeshot.halfnes.ui.Oscilloscope; import com.grapeshot.halfnes.audio.*; import com.grapeshot.halfnes.mappers.Mapper; import java.util.ArrayList; public class APU { public int samplerate; private final Timer[] timers = {new SquareTimer(8, 2), new SquareTimer(8, 2), new TriangleTimer(), new NoiseTimer()}; private double cyclespersample; public final NES nes; CPU cpu; CPURAM cpuram; public int sprdma_count; private int apucycle = 0, remainder = 0; private int[] noiseperiod; // different for PAL private long accum = 0; private final ArrayList<ExpansionSoundChip> expnSound = new ArrayList<>(); private boolean soundFiltering; private final static int[] TNDLOOKUP = initTndLookup(), SQUARELOOKUP = initSquareLookup(); private int framectrreload; private int framectrdiv = 7456; private int dckiller = 0; private int lpaccum = 0; private boolean apuintflag = true, statusdmcint = false, statusframeint = false; private int framectr = 0, ctrmode = 4; private final boolean[] lenCtrEnable = {true, true, true, true}; private final int[] volume = new int[4]; //dmc instance variables private int[] dmcperiods; private int dmcrate = 0x36, dmcpos = 0, dmcshiftregister = 0, dmcbuffer = 0, dmcvalue = 0, dmcsamplelength = 1, dmcsamplesleft = 0, dmcstartaddr = 0xc000, dmcaddr = 0xc000, dmcbitsleft = 8; private boolean dmcsilence = true, dmcirq = false, dmcloop = false, dmcBufferEmpty = true; //length ctr instance variables private final 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}; //linear counter instance vars private int linearctr = 0; private int linctrreload = 0; private boolean linctrflag = false; //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}; //instance variables for sweep unit private final boolean[] sweepenable = {false, false}, sweepnegate = {false, false}, sweepsilence = {false, false}, sweepreload = {false, false}; private final int[] sweepperiod = {15, 15}, sweepshift = {0, 0}, sweeppos = {0, 0}; private int cyclesperframe; private AudioOutInterface ai; public APU(final NES nes, final CPU cpu, final CPURAM cpuram) { this.samplerate = 1; //just in case we can't init audio //then init the audio stream this.nes = nes; this.cpu = cpu; this.cpuram = cpuram; setParameters(); } private static int[] initTndLookup() { int[] lookup = new int[203]; for (int i = 0; i < lookup.length; ++i) { lookup[i] = (int) ((163.67 / (24329.0 / i + 100)) * 49151); } return lookup; } private static int[] initSquareLookup() { //fill square, triangle volume lookup tables int[] lookup = new int[31]; for (int i = 0; i < lookup.length; ++i) { lookup[i] = (int) ((95.52 / (8128.0 / i + 100)) * 49151); } return lookup; } public final synchronized void setParameters() { Mapper.TVType tvtype = cpuram.mapper.getTVType(); soundFiltering = PrefsSingleton.get().getBoolean("soundFiltering", true); samplerate = PrefsSingleton.get().getInt("sampleRate", 44100); if (ai != null) { ai.destroy(); } ai = new SwingAudioImpl(nes, samplerate, tvtype); if (PrefsSingleton.get().getBoolean("showScope", false)) { ai = new Oscilloscope(ai); } //pick the appropriate pitches and lengths for NTSC or PAL switch (tvtype) { case NTSC: default: this.dmcperiods = new int[]{428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54}; this.noiseperiod = new int[]{4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068}; this.framectrreload = 7456; cyclespersample = 1789773.0 / samplerate; cyclesperframe = 29781; break; case DENDY: this.dmcperiods = new int[]{428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54}; this.noiseperiod = new int[]{4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068}; this.framectrreload = 7456; cyclespersample = 1773448.0 / samplerate; cyclesperframe = 35469; break; case PAL: cyclespersample = 1662607.0 / samplerate; this.dmcperiods = new int[]{398, 354, 316, 298, 276, 236, 210, 198, 176, 148, 132, 118, 98, 78, 66, 50}; this.noiseperiod = new int[]{4, 8, 14, 30, 60, 88, 118, 148, 188, 236, 354, 472, 708, 944, 1890, 3778}; this.framectrreload = 8312; cyclesperframe = 33252; break; } // ai = new Reverberator(ai, 2,0.7,0.8,0.99); // ai = new Reverberator(ai, 243,0.5,0.7,0.99); // ai = new Reverberator(ai, 4001,0.3,0.5,0.99); // ai = new Reverberator(ai, 20382,0.2,0.3,0.9); } public boolean bufferHasLessThan(int samples) { return ai.bufferHasLessThan(samples); } public final int read(final int addr) { updateto((int) cpu.clocks); switch (addr) { case 0x15: //returns channel status //for future ref: NEED to put those ternary operators in parentheses! //otherwise order of operations does the wrong thing. final int returnval = ((lengthctr[0] > 0) ? 1 : 0) | ((lengthctr[1] > 0) ? 2 : 0) | ((lengthctr[2] > 0) ? 4 : 0) | ((lengthctr[3] > 0) ? 8 : 0) | ((dmcsamplesleft > 0) ? 16 : 0) | (statusframeint ? 64 : 0) | (statusdmcint ? 128 : 0); if (statusframeint) { //System.err.println("Frame interrupt ack at " + cpu.cycles); --cpu.interrupt; statusframeint = false; } //System.err.println("*" + utils.hex(returnval)); return returnval; case 0x16: nes.getcontroller1().strobe(); return nes.getcontroller1().getbyte() | 0x40; case 0x17: nes.getcontroller2().strobe(); return nes.getcontroller2().getbyte() | 0x40; default: return 0x40; //open bus } } final private static int[][] DUTYLOOKUP = { {0, 1, 0, 0, 0, 0, 0, 0}, {0, 1, 1, 0, 0, 0, 0, 0}, {0, 1, 1, 1, 1, 0, 0, 0}, {1, 0, 0, 1, 1, 1, 1, 1} }; public void addExpnSound(ExpansionSoundChip chip) { expnSound.add(chip); } public void destroy() { ai.destroy(); } public void pause() { ai.pause(); } public void resume() { ai.resume(); } public final void write(final int reg, final int data) { //This is how values written to any of the APU's memory //mapped registers change the state of the system. updateto((int) cpu.clocks - 1); //System.err.println("Wrote " + utils.hex(data) + " to " + utils.hex(reg) + " @ cycle " + cpu.cycles); switch (reg) { 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 //sweep enabled sweepenable[0] = ((data & (utils.BIT7)) != 0); //sweep divider period sweepperiod[0] = (data >> 4) & 7; //sweep negate flag sweepnegate[0] = ((data & (utils.BIT3)) != 0); //sweep shift count sweepshift[0] = (data & 7); sweepreload[0] = true; 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 //sweep enabled sweepenable[1] = ((data & (utils.BIT7)) != 0); //sweep divider period sweepperiod[1] = (data >> 4) & 7; //sweep negate flag sweepnegate[1] = ((data & (utils.BIT3)) != 0); //sweep shift count sweepshift[1] = (data & 7); sweepreload[1] = true; 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 0x8: //triangle linear counter load linctrreload = data & 0x7f; //and length counter halt lenctrHalt[2] = ((data & (utils.BIT7)) != 0); break; case 0x9: break; case 0xA: // triangle low bits of timer timers[2].setperiod((((timers[2].getperiod() * 1) & 0xff00) + data)); break; case 0xB: // triangle length counter load // and high bits of timer if (lenCtrEnable[2]) { lengthctr[2] = lenctrload[data >> 3]; } timers[2].setperiod((((timers[2].getperiod() * 1) & 0xff) + ((data & 7) << 8))); linctrflag = true; break; case 0xC: //noise halt and envelope lenctrHalt[3] = ((data & (utils.BIT5)) != 0); envConstVolume[3] = ((data & (utils.BIT4)) != 0); envelopeValue[3] = data & 0xf; //setvolumes(); break; case 0xD: break; case 0xE: timers[3].setduty(((data & (utils.BIT7)) != 0) ? 6 : 1); timers[3].setperiod(noiseperiod[data & 15]); break; case 0xF: //noise length counter load, envelope restart if (lenCtrEnable[3]) { lengthctr[3] = lenctrload[data >> 3]; } envelopeStartFlag[3] = true; break; case 0x10: dmcirq = ((data & (utils.BIT7)) != 0); dmcloop = ((data & (utils.BIT6)) != 0); dmcrate = dmcperiods[data & 0xf]; if (!dmcirq && statusdmcint) { --cpu.interrupt; statusdmcint = false; } //System.err.println(dmcirq ? "dmc irq on" : "dmc irq off"); break; case 0x11: dmcvalue = data & 0x7f; break; case 0x12: dmcstartaddr = (data << 6) + 0xc000; break; case 0x13: dmcsamplelength = (data << 4) + 1; break; case 0x14: //sprite dma for (int i = 0; i < 256; ++i) { cpuram.write(0x2004, cpuram.read((data << 8) + i)); } //account for time stolen from cpu sprdma_count = 2; break; case 0x15: //status register // counter enable(silence channel when bit is off) for (int i = 0; i < 4; ++i) { lenCtrEnable[i] = ((data & (1 << i)) != 0); //THIS was the channels not cutting off bug! If you toggle a channel's //status on and off very quickly then the length counter should //IMMEDIATELY be forced to zero. if (!lenCtrEnable[i]) { lengthctr[i] = 0; } } if (((data & (utils.BIT4)) != 0)) { if (dmcsamplesleft == 0) { restartdmc(); } } else { dmcsamplesleft = 0; dmcsilence = true; } if (statusdmcint) { --cpu.interrupt; statusdmcint = false; } break; case 0x16: // latch controller 1 + 2 nes.getcontroller1().output(((data & (utils.BIT0)) != 0)); nes.getcontroller2().output(((data & (utils.BIT0)) != 0)); break; case 0x17: ctrmode = ((data & (utils.BIT7)) != 0) ? 5 : 4; //System.err.println("reset " + ctrmode + ' ' + cpu.cycles); apuintflag = ((data & (utils.BIT6)) != 0); //set is no interrupt, clear is an interrupt framectr = 0; framectrdiv = framectrreload + 8; //Why +8? if (apuintflag && statusframeint) { statusframeint = false; --cpu.interrupt; //System.err.println("Frame interrupt off at " + cpu.cycles); } if (ctrmode == 5) { //everything frame counter runs is clocked no matter what setenvelope(); setlinctr(); setlength(); setsweep(); } break; default: break; } } public final void updateto(final int cpucycle) { //still have to run this even if sound is disabled, some games rely on DMC IRQ etc. if (soundFiltering) { //linear sampling code //should really be a FIR filter + decimator instead //but I don't have the DSP experience to design something like that //that would be fast enough to work / not require calculating every sample //this works well enough at eliminating aliasing anyway. while (apucycle < cpucycle) { ++remainder; clockdmc(); if (--framectrdiv <= 0) { framectrdiv = framectrreload; clockframecounter(); } timers[0].clock(); timers[1].clock(); if (lengthctr[2] > 0 && linearctr > 0) { timers[2].clock(); } timers[3].clock(); if (!expnSound.isEmpty()) { for (ExpansionSoundChip c : expnSound) { c.clock(1); } } accum += getOutputLevel(); if ((apucycle % cyclespersample) < 1) { //not quite right - there's a non-integer # cycles per sample. ai.outputSample(lowpass_filter(highpass_filter((int) (accum / remainder)))); remainder = 0; accum = 0; } ++apucycle; } } else { //point sampling code while (apucycle < cpucycle) { ++remainder; clockdmc(); if (--framectrdiv <= 0) { framectrdiv = framectrreload; clockframecounter(); } if ((apucycle % cyclespersample) < 1) { //not quite right - there's a non-integer # cycles per sample. timers[0].clock(remainder); timers[1].clock(remainder); if (lengthctr[2] > 0 && linearctr > 0) { timers[2].clock(remainder); } timers[3].clock(remainder); int mixvol = getOutputLevel(); if (!expnSound.isEmpty()) { for (ExpansionSoundChip c : expnSound) { c.clock(remainder); } } remainder = 0; ai.outputSample(lowpass_filter(highpass_filter(mixvol))); } ++apucycle; } } } private int getOutputLevel() { int vol; vol = SQUARELOOKUP[volume[0] * timers[0].getval() + volume[1] * timers[1].getval()]; vol += TNDLOOKUP[3 * timers[2].getval() + 2 * volume[3] * timers[3].getval() + dmcvalue]; if (!expnSound.isEmpty()) { vol *= 0.8; for (ExpansionSoundChip c : expnSound) { vol += c.getval(); } } return vol; //as usual, lack of unsigned types causes unending pain. } private int highpass_filter(int sample) { //for killing the dc in the signal sample += dckiller; dckiller -= sample >> 8;//the actual high pass part dckiller += (sample > 0 ? -1 : 1);//guarantees the signal decays to exactly zero return sample; } private int lowpass_filter(int sample) { sample += lpaccum; lpaccum -= sample * 0.9; return lpaccum; } public final void finishframe() { updateto(cyclesperframe); apucycle = 0; ai.flushFrame(nes.isFrameLimiterOn()); } private void clockframecounter() { //System.err.println("frame ctr clock " + framectr + ' ' + cpu.cycles); //should be ~4x a frame, 240 Hz //but the problem is this isn't exactly related to the video signal, //it's a completely separate timer, so the phase can shift in relation to the //video signal. also in the current implementation APU interrupts can only be fired when //an APU register is written/read from, or @ end of frame. So both of those need work if ((ctrmode == 4) || (ctrmode == 5 && (framectr != 3))) { setenvelope(); setlinctr(); } if ((ctrmode == 4 && (framectr == 1 || framectr == 3)) || (ctrmode == 5 && (framectr == 1 || framectr == 4))) { setlength(); setsweep(); } if (!apuintflag && (framectr == 3) && (ctrmode == 4) && !statusframeint) { ++cpu.interrupt; //System.err.println("frame interrupt set at " + cpu.cycles); statusframeint = true; } ++framectr; framectr %= ctrmode; setvolumes(); } private void setvolumes() { volume[0] = ((lengthctr[0] <= 0 || sweepsilence[0]) ? 0 : (((envConstVolume[0]) ? envelopeValue[0] : envelopeCounter[0]))); volume[1] = ((lengthctr[1] <= 0 || sweepsilence[1]) ? 0 : (((envConstVolume[1]) ? envelopeValue[1] : envelopeCounter[1]))); volume[3] = ((lengthctr[3] <= 0) ? 0 : ((envConstVolume[3]) ? envelopeValue[3] : envelopeCounter[3])); //System.err.println("setvolumes " + volume[1]); } private void clockdmc() { if (dmcBufferEmpty && dmcsamplesleft > 0) { dmcfillbuffer(); } dmcpos = (dmcpos + 1) % dmcrate; if (dmcpos == 0) { if (dmcbitsleft <= 0) { dmcbitsleft = 8; if (dmcBufferEmpty) { dmcsilence = true; } else { dmcsilence = false; dmcshiftregister = dmcbuffer; dmcBufferEmpty = true; } } if (!dmcsilence) { dmcvalue += (((dmcshiftregister & (utils.BIT0)) != 0) ? 2 : -2); //DMC output register doesn't wrap around if (dmcvalue > 0x7f) { dmcvalue = 0x7f; } if (dmcvalue < 0) { dmcvalue = 0; } dmcshiftregister >>= 1; --dmcbitsleft; } } } private void dmcfillbuffer() { if (dmcsamplesleft > 0) { dmcbuffer = cpuram.read(dmcaddr++); dmcBufferEmpty = false; cpu.stealcycles(4); //DPCM Does steal cpu cycles - this should actually vary between 1-4 //can't do this properly without a cycle accurate cpu/ppu if (dmcaddr > 0xffff) { dmcaddr = 0x8000; } --dmcsamplesleft; if (dmcsamplesleft == 0) { if (dmcloop) { restartdmc(); } else if (dmcirq && !statusdmcint) { //this is supposed to fire after we've just READ the //last byte, not when coming back AFTER reading the last byte //and finding that there are no more bytes left to read. //that meant all dmc timing was too long. ++cpu.interrupt; statusdmcint = true; //System.err.println("dmc irq fire"); } } } else { dmcsilence = true; } } private void restartdmc() { dmcaddr = dmcstartaddr; dmcsamplesleft = dmcsamplelength; dmcsilence = false; } private void setlength() { for (int i = 0; i < 4; ++i) { if (!lenctrHalt[i] && lengthctr[i] > 0) { --lengthctr[i]; if (lengthctr[i] == 0) { setvolumes(); } } } } private void setlinctr() { if (linctrflag) { linearctr = linctrreload; } else if (linearctr > 0) { --linearctr; } if (!lenctrHalt[2]) { linctrflag = false; } } private void setenvelope() { //System.err.println("envelope"); for (int i = 0; i < 4; ++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 setsweep() { //System.err.println("sweep"); for (int i = 0; i < 2; ++i) { sweepsilence[i] = false; if (sweepreload[i]) { sweepreload[i] = false; sweeppos[i] = sweepperiod[i]; } ++sweeppos[i]; final int rawperiod = (timers[i].getperiod() >> 1); int shiftedperiod = (rawperiod >> sweepshift[i]); if (sweepnegate[i]) { //invert bits of period //add 1 on second channel only shiftedperiod = -shiftedperiod + i; } shiftedperiod += rawperiod; if ((rawperiod < 8) || shiftedperiod > 0x7ff) { // silence channel sweepsilence[i] = true; } else if (sweepenable[i] && (sweepshift[i] != 0) && lengthctr[i] > 0 && sweeppos[i] > sweepperiod[i]) { sweeppos[i] = 0; timers[i].setperiod(shiftedperiod << 1); } } } }