PulseChannel.java

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
Location : writeControlRegister
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:audibleOutputReturnsEnvelopeVolumeWhenNothingSilences()]
Replaced Shift Right with Shift Left → KILLED

2.2
Location : writeControlRegister
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:dutyBitZeroSilencesOutputWhenLengthAndSweepAllowSound()]
Replaced bitwise AND with OR → KILLED

69

1.1
Location : writeControlRegister
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:writingControlRegisterWithHaltBitClearSetsHaltFalse()]
negated conditional → KILLED

2.2
Location : writeControlRegister
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:writingControlRegisterWithHaltBitClearSetsHaltFalse()]
removed call to com/rox/apu/LengthCounter::setHalt → KILLED

3.3
Location : writeControlRegister
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:writingControlRegisterWithHaltBitClearSetsHaltFalse()]
Replaced bitwise AND with OR → KILLED

70

1.1
Location : writeControlRegister
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:writingControlRegisterSetsLengthHaltAndDelegatesToEnvelope()]
removed call to com/rox/apu/Envelope::writeControlRegister → KILLED

75

1.1
Location : writeSweepRegister
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:writingSweepRegisterDelegatesToSweep()]
removed call to com/rox/apu/Sweep::writeControlRegister → KILLED

80

1.1
Location : writeTimerLow
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:timerHighWritePreservesPreviouslyWrittenLowByte()]
Replaced bitwise AND with OR → KILLED

2.2
Location : writeTimerLow
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:timerHighWritePreservesPreviouslyWrittenLowByte()]
removed call to com/rox/apu/ParityCountdownFrequencyDivider::setCounterPeriod → KILLED

3.3
Location : writeTimerLow
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:timerHighWritePreservesPreviouslyWrittenLowByte()]
Replaced bitwise OR with AND → KILLED

4.4
Location : writeTimerLow
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:timerHighWritePreservesPreviouslyWrittenLowByte()]
Replaced bitwise AND with OR → KILLED

91

1.1
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:timerHighWritePreservesPreviouslyWrittenLowByte()]
Replaced bitwise AND with OR → KILLED

2.2
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:timerHighWritePreservesPreviouslyWrittenLowByte()]
Replaced Shift Left with Shift Right → KILLED

3.3
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:timerHighWritePreservesPreviouslyWrittenLowByte()]
removed call to com/rox/apu/ParityCountdownFrequencyDivider::setCounterPeriod → KILLED

4.4
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:timerPeriodAssemblesLowThenHighByteWithoutCrossContamination()]
Replaced bitwise AND with OR → KILLED

5.5
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:timerHighWritePreservesPreviouslyWrittenLowByte()]
Replaced bitwise OR with AND → KILLED

92

1.1
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:writingTimerHighAndLengthLoadResetsSequencePositionAndRestartsEnvelope()]
Replaced bitwise AND with OR → KILLED

2.2
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:writingTimerHighAndLengthLoadResetsSequencePositionAndRestartsEnvelope()]
removed call to com/rox/apu/LengthCounter::load → KILLED

3.3
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:writingTimerHighAndLengthLoadResetsSequencePositionAndRestartsEnvelope()]
Replaced Shift Right with Shift Left → KILLED

94

1.1
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:writingTimerHighAndLengthLoadResetsSequencePositionAndRestartsEnvelope()]
removed call to com/rox/apu/Envelope::restart → KILLED

99

1.1
Location : setEnabled
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:setEnabledPassesThroughToTheLengthCounter()]
removed call to com/rox/apu/LengthCounter::setEnabled → KILLED

104

1.1
Location : isLengthCounterActive
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:isLengthCounterActiveDelegatesToLengthCounter()]
replaced boolean return with true for com/rox/apu/PulseChannel::isLengthCounterActive → KILLED

2.2
Location : isLengthCounterActive
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:isLengthCounterActiveDelegatesToLengthCounter()]
negated conditional → KILLED

110

1.1
Location : tick
Killed by : com.rox.apu.PulseChannelTest.[engine:jqwik]/[class:com.rox.apu.PulseChannelTest]/[property:sequencePositionAdvancesExactlyOncePerFullTimerPeriod(int)]
removed call to com/rox/apu/ParityCountdownFrequencyDivider::tick → KILLED

114

1.1
Location : advanceSequencePosition
Killed by : com.rox.apu.PulseChannelTest.[engine:jqwik]/[class:com.rox.apu.PulseChannelTest]/[property:sequencePositionAdvancesExactlyOncePerFullTimerPeriod(int)]
Replaced integer modulus with multiplication → KILLED

2.2
Location : advanceSequencePosition
Killed by : com.rox.apu.PulseChannelTest.[engine:jqwik]/[class:com.rox.apu.PulseChannelTest]/[property:sequencePositionAdvancesExactlyOncePerFullTimerPeriod(int)]
Replaced integer addition with subtraction → KILLED

119

1.1
Location : quarterFrameTick
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:quarterFrameTickClocksOnlyTheEnvelope()]
removed call to com/rox/apu/Envelope::tick → KILLED

124

1.1
Location : halfFrameTick
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:halfFrameTickClocksLengthCounterAndAppliesSweep()]
removed call to com/rox/apu/LengthCounter::tick → KILLED

125

1.1
Location : halfFrameTick
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:halfFrameTickClocksLengthCounterAndAppliesSweep()]
removed call to com/rox/apu/ParityCountdownFrequencyDivider::setCounterPeriod → KILLED

130

1.1
Location : outputSample
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:audibleOutputReturnsEnvelopeVolumeWhenNothingSilences()]
negated conditional → KILLED

2.2
Location : outputSample
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:lengthCounterZeroSilencesOutputRegardlessOfSweepOrDuty()]
negated conditional → KILLED

3.3
Location : outputSample
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:audibleOutputReturnsEnvelopeVolumeWhenNothingSilences()]
negated conditional → KILLED

133

1.1
Location : outputSample
Killed by : com.rox.apu.PulseChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.PulseChannelTest]/[method:audibleOutputReturnsEnvelopeVolumeWhenNothingSilences()]
replaced int return with 0 for com/rox/apu/PulseChannel::outputSample → KILLED

137

1.1
Location : currentSequencePosition
Killed by : com.rox.apu.PulseChannelTest.[engine:jqwik]/[class:com.rox.apu.PulseChannelTest]/[property:sequencePositionAdvancesExactlyOncePerFullTimerPeriod(int)]
replaced int return with 0 for com/rox/apu/PulseChannel::currentSequencePosition → KILLED

147

1.1
Location : snapshot
Killed by : com.rox.save.SaveFileFormatTest.[engine:junit-jupiter]/[class:com.rox.save.SaveFileFormatTest]/[method:readLiveSnapshotIsEmptyForASnapshotPayloadInAFileTaggedAsAnotherSaveType(java.nio.file.Path)]
replaced return value with null for com/rox/apu/PulseChannel::snapshot → KILLED

154

1.1
Location : restore
Killed by : com.rox.apu.ApuSnapshotTest.[engine:junit-jupiter]/[class:com.rox.apu.ApuSnapshotTest]/[method:restoredMidSampleRunsInLockstep()]
removed call to com/rox/apu/Envelope::restore → KILLED

155

1.1
Location : restore
Killed by : com.rox.apu.ApuSnapshotTest.[engine:junit-jupiter]/[class:com.rox.apu.ApuSnapshotTest]/[method:restoredMidSampleRunsInLockstep()]
removed call to com/rox/apu/LengthCounter::restore → KILLED

156

1.1
Location : restore
Killed by : com.rox.apu.ApuSnapshotTest.[engine:junit-jupiter]/[class:com.rox.apu.ApuSnapshotTest]/[method:restoredMidSampleRunsInLockstep()]
removed call to com/rox/apu/Sweep::restore → KILLED

157

1.1
Location : restore
Killed by : com.rox.apu.ApuSnapshotTest.[engine:junit-jupiter]/[class:com.rox.apu.ApuSnapshotTest]/[method:restoredMidSampleRunsInLockstep()]
removed call to com/rox/apu/ParityCountdownFrequencyDivider::restore → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1