| 1 | package com.rox.apu; | |
| 2 | ||
| 3 | import com.rox.clock.ClockWatcher; | |
| 4 | ||
| 5 | /** | |
| 6 | * NES pulse channel ($4000-$4003 / $4004-$4007). Composes an {@link Envelope}, {@link LengthCounter} | |
| 7 | * and {@link Sweep} with an 11-bit timer and an 8-step duty-cycle sequencer. | |
| 8 | * | |
| 9 | * Registered on the CPU clock directly via {@link #tick()} (timer/sequencer run at APU-cycle rate, | |
| 10 | * every other CPU cycle). {@link #quarterFrameTick()} and {@link #halfFrameTick()} are plain methods | |
| 11 | * rather than a shared listener interface - nothing needs to tell quarter and half frame clocks | |
| 12 | * apart, so a channel is registered with the frame sequencer as two separate method references. | |
| 13 | */ | |
| 14 | public class PulseChannel implements ClockWatcher { | |
| 15 | /** | |
| 16 | * The fraction of each waveform period the signal spends "high" versus "low,"<br/> | |
| 17 | * The NES lets you choose between four different duty cycle patterns per pulse channel. | |
| 18 | */ | |
| 19 | public static final int[][] DUTY_TABLES = { | |
| 20 | {0, 1, 0, 0, 0, 0, 0, 0}, //0: 12.5% | |
| 21 | {0, 1, 1, 0, 0, 0, 0, 0}, //1: 25% | |
| 22 | {0, 1, 1, 1, 1, 0, 0, 0}, //2: 50% | |
| 23 | {1, 0, 0, 1, 1, 1, 1, 1} //3: 75% (inverted 25%) | |
| 24 | }; | |
| 25 | private static final int SEQUENCE_LENGTH = 8; | |
| 26 | ||
| 27 | private static final int DUTY_SHIFT = 6; | |
| 28 | private static final int DUTY_MASK = 0x03; | |
| 29 | private static final int LENGTH_HALT_BIT = 0x20; | |
| 30 | ||
| 31 | private static final int TIMER_LOW_MASK = 0xFF; | |
| 32 | private static final int TIMER_HIGH_MASK = 0x07; | |
| 33 | private static final int TIMER_HIGH_SHIFT = 8; | |
| 34 | private static final int LENGTH_LOAD_SHIFT = 3; | |
| 35 | private static final int LENGTH_LOAD_MASK = 0x1F; | |
| 36 | ||
| 37 | private final Envelope envelope; | |
| 38 | private final LengthCounter lengthCounter; | |
| 39 | private final Sweep sweep; | |
| 40 | ||
| 41 | private int dutyCycle; | |
| 42 | private int sequencePosition; | |
| 43 | ||
| 44 | private final ParityCountdownFrequencyDivider frequencyDivider; | |
| 45 | ||
| 46 | public PulseChannel(final boolean onesComplementNegate){ | |
| 47 | this(new Envelope(), new LengthCounter(), new Sweep(onesComplementNegate)); | |
| 48 | } | |
| 49 | ||
| 50 | PulseChannel(final Envelope envelope, final LengthCounter lengthCounter, final Sweep sweep){ | |
| 51 | this.envelope = envelope; | |
| 52 | this.lengthCounter = lengthCounter; | |
| 53 | this.sweep = sweep; | |
| 54 | ||
| 55 | this.frequencyDivider = new ParityCountdownFrequencyDivider(this::advanceSequencePosition, false, 0); | |
| 56 | } | |
| 57 | ||
| 58 | /** | |
| 59 | * Handle a $4000/$4004 write. | |
| 60 | * | |
| 61 | * Register: DDLC VVVV | |
| 62 | * D (bits 6-7): duty cycle select | |
| 63 | * L (bit 5): length-counter halt / envelope loop (shared bit) | |
| 64 | * C (bit 4): constant volume | |
| 65 | * V (bits 0-3): volume / envelope period | |
| 66 | */ | |
| 67 | public void writeControlRegister(final int value){ | |
| 68 |
2
1. writeControlRegister : Replaced Shift Right with Shift Left → KILLED 2. writeControlRegister : Replaced bitwise AND with OR → KILLED |
dutyCycle = (value >> DUTY_SHIFT) & DUTY_MASK; |
| 69 |
3
1. writeControlRegister : negated conditional → KILLED 2. writeControlRegister : removed call to com/rox/apu/LengthCounter::setHalt → KILLED 3. writeControlRegister : Replaced bitwise AND with OR → KILLED |
lengthCounter.setHalt((value & LENGTH_HALT_BIT) != 0); |
| 70 |
1
1. writeControlRegister : removed call to com/rox/apu/Envelope::writeControlRegister → KILLED |
envelope.writeControlRegister(value); |
| 71 | } | |
| 72 | ||
| 73 | /** Handle a $4001/$4005 sweep-unit write. */ | |
| 74 | public void writeSweepRegister(final int value){ | |
| 75 |
1
1. writeSweepRegister : removed call to com/rox/apu/Sweep::writeControlRegister → KILLED |
sweep.writeControlRegister(value); |
| 76 | } | |
| 77 | ||
| 78 | /** Handle a $4002/$4006 write: low 8 bits of the 11-bit timer period. */ | |
| 79 | public void writeTimerLow(final int value){ | |
| 80 |
4
1. writeTimerLow : Replaced bitwise AND with OR → KILLED 2. writeTimerLow : removed call to com/rox/apu/ParityCountdownFrequencyDivider::setCounterPeriod → KILLED 3. writeTimerLow : Replaced bitwise OR with AND → KILLED 4. writeTimerLow : Replaced bitwise AND with OR → KILLED |
frequencyDivider.setCounterPeriod((frequencyDivider.getCounterPeriod() & (TIMER_HIGH_MASK << TIMER_HIGH_SHIFT)) | (value & TIMER_LOW_MASK)); |
| 81 | } | |
| 82 | ||
| 83 | /** | |
| 84 | * Handle a $4003/$4007 write. | |
| 85 | * | |
| 86 | * Register: LLLL LHHH | |
| 87 | * L (bits 3-7): length-counter table index | |
| 88 | * H (bits 0-2): high 3 bits of the 11-bit timer period | |
| 89 | */ | |
| 90 | public void writeTimerHighAndLengthLoad(final int value){ | |
| 91 |
5
1. writeTimerHighAndLengthLoad : Replaced bitwise AND with OR → KILLED 2. writeTimerHighAndLengthLoad : Replaced Shift Left with Shift Right → KILLED 3. writeTimerHighAndLengthLoad : removed call to com/rox/apu/ParityCountdownFrequencyDivider::setCounterPeriod → KILLED 4. writeTimerHighAndLengthLoad : Replaced bitwise AND with OR → KILLED 5. writeTimerHighAndLengthLoad : Replaced bitwise OR with AND → KILLED |
frequencyDivider.setCounterPeriod((frequencyDivider.getCounterPeriod() & TIMER_LOW_MASK) | ((value & TIMER_HIGH_MASK) << TIMER_HIGH_SHIFT)); |
| 92 |
3
1. writeTimerHighAndLengthLoad : Replaced bitwise AND with OR → KILLED 2. writeTimerHighAndLengthLoad : removed call to com/rox/apu/LengthCounter::load → KILLED 3. writeTimerHighAndLengthLoad : Replaced Shift Right with Shift Left → KILLED |
lengthCounter.load((value >> LENGTH_LOAD_SHIFT) & LENGTH_LOAD_MASK); |
| 93 | sequencePosition = 0; | |
| 94 |
1
1. writeTimerHighAndLengthLoad : removed call to com/rox/apu/Envelope::restart → KILLED |
envelope.restart(); |
| 95 | } | |
| 96 | ||
| 97 | /** Handle a $4015 enable-bit change - see {@link LengthCounter#setEnabled}. */ | |
| 98 | public void setEnabled(final boolean enabled){ | |
| 99 |
1
1. setEnabled : removed call to com/rox/apu/LengthCounter::setEnabled → KILLED |
lengthCounter.setEnabled(enabled); |
| 100 | } | |
| 101 | ||
| 102 | /** Whether the length counter is currently nonzero - for the $4015 status read. */ | |
| 103 | public boolean isLengthCounterActive(){ | |
| 104 |
2
1. isLengthCounterActive : replaced boolean return with true for com/rox/apu/PulseChannel::isLengthCounterActive → KILLED 2. isLengthCounterActive : negated conditional → KILLED |
return !lengthCounter.isZero(); |
| 105 | } | |
| 106 | ||
| 107 | /** CPU-cycle clock: the timer/sequencer run at half this rate (once per APU cycle). */ | |
| 108 | @Override | |
| 109 | public void tick(){ | |
| 110 |
1
1. tick : removed call to com/rox/apu/ParityCountdownFrequencyDivider::tick → KILLED |
frequencyDivider.tick(); |
| 111 | } | |
| 112 | ||
| 113 | private void advanceSequencePosition(){ | |
| 114 |
2
1. advanceSequencePosition : Replaced integer modulus with multiplication → KILLED 2. advanceSequencePosition : Replaced integer addition with subtraction → KILLED |
sequencePosition = (sequencePosition + 1) % SEQUENCE_LENGTH; |
| 115 | } | |
| 116 | ||
| 117 | /** Quarter-frame clock: advances the envelope. */ | |
| 118 | public void quarterFrameTick(){ | |
| 119 |
1
1. quarterFrameTick : removed call to com/rox/apu/Envelope::tick → KILLED |
envelope.tick(); |
| 120 | } | |
| 121 | ||
| 122 | /** Half-frame clock: advances the length counter and lets the sweep unit retune the timer period. */ | |
| 123 | public void halfFrameTick(){ | |
| 124 |
1
1. halfFrameTick : removed call to com/rox/apu/LengthCounter::tick → KILLED |
lengthCounter.tick(); |
| 125 |
1
1. halfFrameTick : removed call to com/rox/apu/ParityCountdownFrequencyDivider::setCounterPeriod → KILLED |
frequencyDivider.setCounterPeriod(sweep.clockHalfFrame(frequencyDivider.getCounterPeriod())); |
| 126 | } | |
| 127 | ||
| 128 | /** Current output: 0 if silenced by the length counter, the sweep unit, or the duty waveform, else the envelope volume. */ | |
| 129 | public int outputSample(){ | |
| 130 |
3
1. outputSample : negated conditional → KILLED 2. outputSample : negated conditional → KILLED 3. outputSample : negated conditional → KILLED |
if (lengthCounter.isZero() || sweep.isMuted(frequencyDivider.getCounterPeriod()) || DUTY_TABLES[dutyCycle][sequencePosition] == 0){ |
| 131 | return 0; | |
| 132 | } | |
| 133 |
1
1. outputSample : replaced int return with 0 for com/rox/apu/PulseChannel::outputSample → KILLED |
return envelope.volume(); |
| 134 | } | |
| 135 | ||
| 136 | int currentSequencePosition(){ | |
| 137 |
1
1. currentSequencePosition : replaced int return with 0 for com/rox/apu/PulseChannel::currentSequencePosition → KILLED |
return sequencePosition; |
| 138 | } | |
| 139 | ||
| 140 | /** Everything a {@link PulseChannel} needs to carry on exactly where it was, sub-units included. */ | |
| 141 | public record Snapshot(int dutyCycle, int sequencePosition, Envelope.Snapshot envelope, | |
| 142 | LengthCounter.Snapshot lengthCounter, Sweep.Snapshot sweep, | |
| 143 | ParityCountdownFrequencyDivider.Snapshot frequencyDivider) { | |
| 144 | } | |
| 145 | ||
| 146 | public Snapshot snapshot(){ | |
| 147 |
1
1. snapshot : replaced return value with null for com/rox/apu/PulseChannel::snapshot → KILLED |
return new Snapshot(dutyCycle, sequencePosition, envelope.snapshot(), lengthCounter.snapshot(), |
| 148 | sweep.snapshot(), frequencyDivider.snapshot()); | |
| 149 | } | |
| 150 | ||
| 151 | public void restore(final Snapshot snapshot){ | |
| 152 | dutyCycle = snapshot.dutyCycle(); | |
| 153 | sequencePosition = snapshot.sequencePosition(); | |
| 154 |
1
1. restore : removed call to com/rox/apu/Envelope::restore → KILLED |
envelope.restore(snapshot.envelope()); |
| 155 |
1
1. restore : removed call to com/rox/apu/LengthCounter::restore → KILLED |
lengthCounter.restore(snapshot.lengthCounter()); |
| 156 |
1
1. restore : removed call to com/rox/apu/Sweep::restore → KILLED |
sweep.restore(snapshot.sweep()); |
| 157 |
1
1. restore : removed call to com/rox/apu/ParityCountdownFrequencyDivider::restore → KILLED |
frequencyDivider.restore(snapshot.frequencyDivider()); |
| 158 | } | |
| 159 | } | |
Mutations | ||
| 68 |
1.1 2.2 |
|
| 69 |
1.1 2.2 3.3 |
|
| 70 |
1.1 |
|
| 75 |
1.1 |
|
| 80 |
1.1 2.2 3.3 4.4 |
|
| 91 |
1.1 2.2 3.3 4.4 5.5 |
|
| 92 |
1.1 2.2 3.3 |
|
| 94 |
1.1 |
|
| 99 |
1.1 |
|
| 104 |
1.1 2.2 |
|
| 110 |
1.1 |
|
| 114 |
1.1 2.2 |
|
| 119 |
1.1 |
|
| 124 |
1.1 |
|
| 125 |
1.1 |
|
| 130 |
1.1 2.2 3.3 |
|
| 133 |
1.1 |
|
| 137 |
1.1 |
|
| 147 |
1.1 |
|
| 154 |
1.1 |
|
| 155 |
1.1 |
|
| 156 |
1.1 |
|
| 157 |
1.1 |