Envelope.java

1
package com.rox.apu;
2
3
import com.rox.clock.ClockWatcher;
4
5
/**
6
 * NES pulse/noise envelope generator ($4000/$4004/$400C). Produces a 4-bit volume that either
7
 * decays from 15 to 0 over time or holds a constant value, clocked once per quarter-frame.
8
 *
9
 * The loop flag (bit5 of the control byte) is the same physical bit as the owning channel's
10
 * length-counter halt flag - a hardware quirk, not a coincidence, so callers read both meanings
11
 * from the one write.
12
 */
13
public class Envelope implements ClockWatcher {
14
    private static final int RELOAD_VALUE_MASK = 0x0F;
15
    private static final int CONSTANT_VOLUME_BIT = 0x10;
16
    private static final int LOOP_BIT = 0x20;
17
    private static final int MAX_DECAY_LEVEL = 15;
18
19
    private int reloadValue;
20
    private boolean volumeIsConstant;
21
    private boolean loopEnabled;
22
23
    private boolean restartRequested;
24
    /** The countdown from {@link #reloadValue} and represents the speed of decay, 0 is fastest */
25
    private int divider;
26
    /** The countdown from {@link MAX_DECAY_LEVEL} */
27
    private int decayLevel; //the decay countdown from
28
29
    /**
30
     * Handle a $4000/$4004/$400C write: sets reload value V, constant-volume flag, loop flag.
31
     *
32
     * Register: --LC VVVV
33
     * V (bits 0-3): a 4-bit value with a dual purpose (fixed volume level or decay speed)
34
     * C (bit 4): the constant volume flag
35
     * L (bit 5): the loop flag
36
     */
37
    public void writeControlRegister(final int value){
38 1 1. writeControlRegister : Replaced bitwise AND with OR → KILLED
        reloadValue = value & RELOAD_VALUE_MASK;
39 2 1. writeControlRegister : negated conditional → KILLED
2. writeControlRegister : Replaced bitwise AND with OR → KILLED
        volumeIsConstant = (value & CONSTANT_VOLUME_BIT) != 0;
40 2 1. writeControlRegister : negated conditional → KILLED
2. writeControlRegister : Replaced bitwise AND with OR → KILLED
        loopEnabled = (value & LOOP_BIT) != 0;
41
    }
42
43
    /** Requests a restart on the next {@link #tick()} - triggered by a channel's high-byte timer write. */
44
    public void restart(){
45
        restartRequested = true;
46
    }
47
48
    /** Quarter-frame clock: advances the decay envelope or services a pending restart. */
49
    @Override
50
    public void tick(){
51 1 1. tick : negated conditional → KILLED
        if (restartRequested){
52
            restartRequested = false;
53
            decayLevel = MAX_DECAY_LEVEL;
54
            divider = reloadValue;
55
            return;
56
        }
57
        /**
58
         * XXX this could probably be expressed better:
59
         * Each clock: reduce the {@link #divider},
60
         *             if it reaches zero, reset it and reduce {@link #decayLevel}
61
         *                                 if {@link #loopEnabled} then reset decay level when it reaches 0
62
         */
63 1 1. tick : negated conditional → KILLED
        if (divider == 0){
64
            divider = reloadValue;
65 2 1. tick : negated conditional → KILLED
2. tick : changed conditional boundary → KILLED
            if (decayLevel > 0){
66 1 1. tick : Replaced integer subtraction with addition → KILLED
                decayLevel--;
67 1 1. tick : negated conditional → KILLED
            } else if (loopEnabled){
68
                decayLevel = MAX_DECAY_LEVEL;
69
            }
70
        } else {
71 1 1. tick : Replaced integer subtraction with addition → KILLED
            divider--;
72
        }
73
    }
74
75
    /** Current output volume: the constant reload value if constant-volume is set, else the decay level. */
76
    public int volume(){
77 2 1. volume : negated conditional → KILLED
2. volume : replaced int return with 0 for com/rox/apu/Envelope::volume → KILLED
        return volumeIsConstant ? reloadValue : decayLevel;
78
    }
79
80
    /** Everything an {@link Envelope} needs to carry on exactly where it was. */
81
    public record Snapshot(int reloadValue, boolean volumeIsConstant, boolean loopEnabled, boolean restartRequested, int divider, int decayLevel) {
82
    }
83
84
    public Snapshot snapshot(){
85 1 1. snapshot : replaced return value with null for com/rox/apu/Envelope::snapshot → KILLED
        return new Snapshot(reloadValue, volumeIsConstant, loopEnabled, restartRequested, divider, decayLevel);
86
    }
87
88
    public void restore(final Snapshot snapshot){
89
        reloadValue = snapshot.reloadValue();
90
        volumeIsConstant = snapshot.volumeIsConstant();
91
        loopEnabled = snapshot.loopEnabled();
92
        restartRequested = snapshot.restartRequested();
93
        divider = snapshot.divider();
94
        decayLevel = snapshot.decayLevel();
95
    }
96
}

Mutations

38

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

39

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

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

40

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

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

51

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

63

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

65

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

2.2
Location : tick
Killed by : com.rox.apu.EnvelopeTest.[engine:junit-jupiter]/[class:com.rox.apu.EnvelopeTest]/[method:decayWrapsToFifteenWhenLoopSetAfterReachingZero()]
changed conditional boundary → KILLED

66

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

67

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

71

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

77

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

2.2
Location : volume
Killed by : com.rox.apu.EnvelopeTest.[engine:junit-jupiter]/[class:com.rox.apu.EnvelopeTest]/[method:restartThenClockSetsDecayToFifteen()]
replaced int return with 0 for com/rox/apu/Envelope::volume → KILLED

85

1.1
Location : snapshot
Killed by : com.rox.apu.ApuSnapshotTest.[engine:jqwik]/[class:com.rox.apu.ApuSnapshotTest]/[property:envelopeRoundTrips(int, boolean, boolean, boolean, int, int)]
replaced return value with null for com/rox/apu/Envelope::snapshot → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1