TriangleChannel.java

1
package com.rox.apu;
2
3
import com.rox.clock.ClockWatcher;
4
5
/**
6
 * NES triangle channel ($4008 / $400A / $400B). Composes a {@link LinearCounter} and
7
 * {@link LengthCounter} with an 11-bit timer and a 32-step sequencer that produces a stepped
8
 * triangle waveform.
9
 *
10
 * Unlike the pulse/noise timers, the triangle's timer is clocked every CPU cycle rather than every
11
 * APU cycle (no parity gating) - this is what gives the triangle its doubled effective frequency
12
 * range relative to pulse/noise for the same timer period.
13
 *
14
 * The sequencer only advances while both the length counter and linear counter are nonzero; the
15
 * output is always whatever the current sequence step holds regardless - real hardware doesn't
16
 * silence the channel outright when either counter hits 0, it just freezes the waveform at
17
 * whichever step it was on (a documented quirk, not a bug).
18
 */
19
public class TriangleChannel implements ClockWatcher {
20
    static final int[] SEQUENCE = {
21
            15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
22
            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
23
    };
24
    private static final int SEQUENCE_LENGTH = 32;
25
26
    private static final int CONTROL_FLAG_BIT = 0x80; //shared: linear-counter control / length-counter halt
27
28
    private static final int TIMER_LOW_MASK = 0xFF;
29
    private static final int TIMER_HIGH_MASK = 0x07;
30
    private static final int TIMER_HIGH_SHIFT = 8;
31
    private static final int LENGTH_LOAD_SHIFT = 3;
32
    private static final int LENGTH_LOAD_MASK = 0x1F;
33
34
    private final LinearCounter linearCounter;
35
    private final LengthCounter lengthCounter;
36
37
    private int timerPeriod;
38
    private int timerCounter;
39
    private int sequencePosition;
40
41
    public TriangleChannel(){
42
        this(new LinearCounter(), new LengthCounter());
43
    }
44
45
    TriangleChannel(final LinearCounter linearCounter, final LengthCounter lengthCounter){
46
        this.linearCounter = linearCounter;
47
        this.lengthCounter = lengthCounter;
48
    }
49
50
    /**
51
     * Handle a $4008 write.
52
     *
53
     * Register: CRRR RRRR
54
     * C (bit 7): linear-counter control flag / length-counter halt (shared bit)
55
     * R (bits 0-6): linear counter reload value
56
     */
57
    public void writeLinearCounterRegister(final int value){
58 3 1. writeLinearCounterRegister : negated conditional → KILLED
2. writeLinearCounterRegister : Replaced bitwise AND with OR → KILLED
3. writeLinearCounterRegister : removed call to com/rox/apu/LengthCounter::setHalt → KILLED
        lengthCounter.setHalt((value & CONTROL_FLAG_BIT) != 0);
59 1 1. writeLinearCounterRegister : removed call to com/rox/apu/LinearCounter::writeControlRegister → KILLED
        linearCounter.writeControlRegister(value);
60
    }
61
62
    /** Handle a $400A write: low 8 bits of the 11-bit timer period. */
63
    public void writeTimerLow(final int value){
64 3 1. writeTimerLow : Replaced bitwise AND with OR → KILLED
2. writeTimerLow : Replaced bitwise OR with AND → KILLED
3. writeTimerLow : Replaced bitwise AND with OR → KILLED
        timerPeriod = (timerPeriod & (TIMER_HIGH_MASK << TIMER_HIGH_SHIFT)) | (value & TIMER_LOW_MASK);
65
    }
66
67
    /**
68
     * Handle a $400B write.
69
     *
70
     * Register: LLLL LHHH
71
     * L (bits 3-7): length-counter table index
72
     * H (bits 0-2): high 3 bits of the 11-bit timer period
73
     */
74
    public void writeTimerHighAndLengthLoad(final int value){
75 4 1. writeTimerHighAndLengthLoad : Replaced Shift Left with Shift Right → KILLED
2. writeTimerHighAndLengthLoad : Replaced bitwise AND with OR → KILLED
3. writeTimerHighAndLengthLoad : Replaced bitwise OR with AND → KILLED
4. writeTimerHighAndLengthLoad : Replaced bitwise AND with OR → KILLED
        timerPeriod = (timerPeriod & TIMER_LOW_MASK) | ((value & TIMER_HIGH_MASK) << TIMER_HIGH_SHIFT);
76 3 1. writeTimerHighAndLengthLoad : removed call to com/rox/apu/LengthCounter::load → KILLED
2. writeTimerHighAndLengthLoad : Replaced Shift Right with Shift Left → KILLED
3. writeTimerHighAndLengthLoad : Replaced bitwise AND with OR → KILLED
        lengthCounter.load((value >> LENGTH_LOAD_SHIFT) & LENGTH_LOAD_MASK);
77 1 1. writeTimerHighAndLengthLoad : removed call to com/rox/apu/LinearCounter::requestReload → KILLED
        linearCounter.requestReload();
78
    }
79
80
    /** Handle a $4015 enable-bit change - see {@link LengthCounter#setEnabled}. */
81
    public void setEnabled(final boolean enabled){
82 1 1. setEnabled : removed call to com/rox/apu/LengthCounter::setEnabled → KILLED
        lengthCounter.setEnabled(enabled);
83
    }
84
85
    /** Whether the length counter is currently nonzero - for the $4015 status read. */
86
    public boolean isLengthCounterActive(){
87 2 1. isLengthCounterActive : replaced boolean return with true for com/rox/apu/TriangleChannel::isLengthCounterActive → KILLED
2. isLengthCounterActive : negated conditional → KILLED
        return !lengthCounter.isZero();
88
    }
89
90
    /** CPU-cycle clock: unlike pulse/noise, runs at full CPU rate (no APU-cycle parity gating). */
91
    @Override
92
    public void tick(){
93 1 1. tick : negated conditional → KILLED
        if (timerCounter == 0){
94
            timerCounter = timerPeriod;
95 2 1. tick : negated conditional → KILLED
2. tick : negated conditional → KILLED
            if (!lengthCounter.isZero() && !linearCounter.isZero()){
96 2 1. tick : Replaced integer modulus with multiplication → KILLED
2. tick : Replaced integer addition with subtraction → KILLED
                sequencePosition = (sequencePosition + 1) % SEQUENCE_LENGTH;
97
            }
98
        } else {
99 1 1. tick : Replaced integer subtraction with addition → KILLED
            timerCounter--;
100
        }
101
    }
102
103
    /** Quarter-frame clock: advances the linear counter. */
104
    public void quarterFrameTick(){
105 1 1. quarterFrameTick : removed call to com/rox/apu/LinearCounter::tick → KILLED
        linearCounter.tick();
106
    }
107
108
    /** Half-frame clock: advances the length counter. */
109
    public void halfFrameTick(){
110 1 1. halfFrameTick : removed call to com/rox/apu/LengthCounter::tick → KILLED
        lengthCounter.tick();
111
    }
112
113
    /** Current output: always the current sequence step (real hardware never silences the channel outright). */
114
    public int outputSample(){
115 1 1. outputSample : replaced int return with 0 for com/rox/apu/TriangleChannel::outputSample → KILLED
        return SEQUENCE[sequencePosition];
116
    }
117
118
    int currentSequencePosition(){
119 1 1. currentSequencePosition : replaced int return with 0 for com/rox/apu/TriangleChannel::currentSequencePosition → KILLED
        return sequencePosition;
120
    }
121
122
    int currentTimerPeriod(){
123 1 1. currentTimerPeriod : replaced int return with 0 for com/rox/apu/TriangleChannel::currentTimerPeriod → KILLED
        return timerPeriod;
124
    }
125
126
    /** Everything a {@link TriangleChannel} needs to carry on exactly where it was, sub-units included. */
127
    public record Snapshot(int timerPeriod, int timerCounter, int sequencePosition,
128
                           LinearCounter.Snapshot linearCounter, LengthCounter.Snapshot lengthCounter) {
129
    }
130
131
    public Snapshot snapshot(){
132 1 1. snapshot : replaced return value with null for com/rox/apu/TriangleChannel::snapshot → KILLED
        return new Snapshot(timerPeriod, timerCounter, sequencePosition, linearCounter.snapshot(),
133
                lengthCounter.snapshot());
134
    }
135
136
    public void restore(final Snapshot snapshot){
137
        timerPeriod = snapshot.timerPeriod();
138
        timerCounter = snapshot.timerCounter();
139
        sequencePosition = snapshot.sequencePosition();
140 1 1. restore : removed call to com/rox/apu/LinearCounter::restore → KILLED
        linearCounter.restore(snapshot.linearCounter());
141 1 1. restore : removed call to com/rox/apu/LengthCounter::restore → KILLED
        lengthCounter.restore(snapshot.lengthCounter());
142
    }
143
}

Mutations

58

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

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

3.3
Location : writeLinearCounterRegister
Killed by : com.rox.apu.TriangleChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.TriangleChannelTest]/[method:writingLinearCounterRegisterWithControlBitClearSetsHaltFalse()]
removed call to com/rox/apu/LengthCounter::setHalt → KILLED

59

1.1
Location : writeLinearCounterRegister
Killed by : com.rox.apu.TriangleChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.TriangleChannelTest]/[method:writingLinearCounterRegisterSetsHaltAndDelegatesToLinearCounter()]
removed call to com/rox/apu/LinearCounter::writeControlRegister → KILLED

64

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

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

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

75

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

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

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

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

76

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

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

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

77

1.1
Location : writeTimerHighAndLengthLoad
Killed by : com.rox.apu.TriangleChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.TriangleChannelTest]/[method:writingTimerHighAndLengthLoadLoadsLengthCounterAndRequestsLinearReload()]
removed call to com/rox/apu/LinearCounter::requestReload → KILLED

82

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

87

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

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

93

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

95

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

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

96

1.1
Location : tick
Killed by : com.rox.apu.TriangleChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.TriangleChannelTest]/[method:sequencerAdvancesWhenBothCountersAreNonzero()]
Replaced integer modulus with multiplication → KILLED

2.2
Location : tick
Killed by : com.rox.apu.TriangleChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.TriangleChannelTest]/[method:sequencerAdvancesWhenBothCountersAreNonzero()]
Replaced integer addition with subtraction → KILLED

99

1.1
Location : tick
Killed by : com.rox.apu.TriangleChannelTest.[engine:jqwik]/[class:com.rox.apu.TriangleChannelTest]/[property:sequencePositionAdvancesExactlyOncePerFullTimerPeriodWhenUnfrozen(int)]
Replaced integer subtraction with addition → KILLED

105

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

110

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

115

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

119

1.1
Location : currentSequencePosition
Killed by : com.rox.apu.TriangleChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.TriangleChannelTest]/[method:sequencerAdvancesWhenBothCountersAreNonzero()]
replaced int return with 0 for com/rox/apu/TriangleChannel::currentSequencePosition → KILLED

123

1.1
Location : currentTimerPeriod
Killed by : com.rox.apu.TriangleChannelTest.[engine:junit-jupiter]/[class:com.rox.apu.TriangleChannelTest]/[method:timerHighWritePreservesPreviouslyWrittenLowByte()]
replaced int return with 0 for com/rox/apu/TriangleChannel::currentTimerPeriod → KILLED

132

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/TriangleChannel::snapshot → KILLED

140

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/LinearCounter::restore → KILLED

141

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

Active mutators

Tests examined


Report generated by PIT 1.22.1