| 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 |
|
| 39 |
1.1 2.2 |
|
| 40 |
1.1 2.2 |
|
| 51 |
1.1 |
|
| 63 |
1.1 |
|
| 65 |
1.1 2.2 |
|
| 66 |
1.1 |
|
| 67 |
1.1 |
|
| 71 |
1.1 |
|
| 77 |
1.1 2.2 |
|
| 85 |
1.1 |