| 1 | package com.rox.apu; | |
| 2 | ||
| 3 | import com.rox.clock.ClockWatcher; | |
| 4 | import com.rox.mem.MemoryBus; | |
| 5 | ||
| 6 | /** | |
| 7 | * NES delta modulation channel (DMC, $4010-$4013). Plays back 1-bit delta-encoded PCM samples | |
| 8 | * fetched via DMA from CPU-addressable memory, driving a 7-bit output/delta counter that the | |
| 9 | * {@link Mixer} reads directly as this channel's level (0-127) - unlike the other four channels, | |
| 10 | * DMC has no envelope, sweep, or length counter, and isn't clocked by the {@link FrameSequencer} | |
| 11 | * at all; its only rhythm is its own timer. | |
| 12 | * | |
| 13 | * <p>The timer ({@link ParityCountdownFrequencyDivider}, same "once per APU cycle" gating as | |
| 14 | * pulse/noise) drives an 8-bit shift register that empties one bit at a time, LSB first: a 1 bit | |
| 15 | * nudges the delta counter up by 2 (clamped at 127), a 0 bit nudges it down by 2 (clamped at 0). | |
| 16 | * Every 8 shifts, the shift register is refilled by a memory fetch, provided sample bytes remain - | |
| 17 | * if none remain, the output freezes at its last delta value (real DMC behaviour, not a | |
| 18 | * simplification) until the channel is restarted. | |
| 19 | * | |
| 20 | * <p>Sample playback parameters: start address = $C000 + ($4012 * 64), length = ($4013 * 16) + 1 | |
| 21 | * bytes, fetch address wraps $FFFF back to $8000. On exhausting the sample, either the loop flag | |
| 22 | * ($4010 bit 6) restarts playback from the original address/length, or - if the loop flag is clear | |
| 23 | * and the IRQ-enable flag ($4010 bit 7) is set - an IRQ becomes pending. | |
| 24 | * | |
| 25 | * <p>Enable/disable ($4015 bit 4, via {@link #setEnabled(boolean)}) works differently from the | |
| 26 | * length-counter-based channels: disabling sets {@code bytesRemaining} to 0 directly (stops future | |
| 27 | * fetches - whatever's already latched in the shift register keeps playing out until the next | |
| 28 | * reload finds nothing left and silences); enabling only calls {@link #start()} if the channel was | |
| 29 | * idle ({@code bytesRemaining == 0}) - restarting a still-playing sample would be wrong. | |
| 30 | * | |
| 31 | * <p>Simplifications (Phase 6): | |
| 32 | * <ul> | |
| 33 | * <li>CPU cycle-stealing/stall on each DMA sample fetch is not modeled - real hardware stalls | |
| 34 | * the CPU 1-4 cycles per fetch; no stall mechanism exists in this codebase's Clock/ClockWatcher | |
| 35 | * model yet. Tracked as a separate, deferred follow-up.</li> | |
| 36 | * <li>Real hardware's memory-reader and output unit are two independent state machines linked | |
| 37 | * only by the one-byte sample buffer (the reader can prefetch ahead of the output unit | |
| 38 | * finishing its current byte); here they're combined into one synchronous fetch-on-refill path | |
| 39 | * - functionally equivalent for audio output, but not a basis for cycle-accurate CPU stalling.</li> | |
| 40 | * <li>{@link #start()} eagerly calls {@link #reloadShiftRegister()}, so starting playback of a | |
| 41 | * 1-byte sample synchronously consumes that byte - and can set {@link #isIrqPending()} or | |
| 42 | * perform the loop reload - before a single output clock has happened. The address/length are | |
| 43 | * only reloaded once {@code bytesRemaining} reaches 0, matching real hardware.</li> | |
| 44 | * </ul> | |
| 45 | */ | |
| 46 | public class DMCChannel implements ClockWatcher { | |
| 47 | /** NTSC DMC rates (in APU cycles), selected by the low nibble of $4010. */ | |
| 48 | static final int[] NTSC_DMC_RATES = { | |
| 49 | 428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54 | |
| 50 | }; | |
| 51 | ||
| 52 | private static final int IRQ_ENABLE_BIT = 0x80; | |
| 53 | private static final int LOOP_BIT = 0x40; | |
| 54 | private static final int RATE_INDEX_MASK = 0x0F; | |
| 55 | ||
| 56 | private static final int DIRECT_LOAD_MASK = 0x7F; | |
| 57 | ||
| 58 | private static final int SAMPLE_ADDRESS_BASE = 0xC000; | |
| 59 | private static final int SAMPLE_ADDRESS_MULTIPLIER = 64; | |
| 60 | ||
| 61 | private static final int SAMPLE_LENGTH_MULTIPLIER = 16; | |
| 62 | private static final int SAMPLE_LENGTH_OFFSET = 1; | |
| 63 | ||
| 64 | private static final int MAX_ADDRESS = 0xFFFF; | |
| 65 | private static final int ADDRESS_WRAP_TARGET = 0x8000; | |
| 66 | ||
| 67 | private static final int DELTA_MIN = 0; | |
| 68 | private static final int DELTA_MAX = 127; | |
| 69 | private static final int DELTA_STEP = 2; | |
| 70 | ||
| 71 | private static final int SHIFT_REGISTER_BITS = 8; | |
| 72 | ||
| 73 | private final MemoryBus memoryBus; | |
| 74 | private final ParityCountdownFrequencyDivider frequencyDivider; | |
| 75 | ||
| 76 | private boolean irqEnabled; | |
| 77 | private boolean loop; | |
| 78 | private boolean irqPending; | |
| 79 | ||
| 80 | private int sampleStartAddress; | |
| 81 | private int sampleLength; | |
| 82 | ||
| 83 | private int currentAddress; | |
| 84 | private int bytesRemaining; | |
| 85 | ||
| 86 | private int shiftRegister; | |
| 87 | private int bitsRemainingInShiftRegister; | |
| 88 | private boolean outputSilenced = true; | |
| 89 | ||
| 90 | private int deltaCounter; | |
| 91 | ||
| 92 | public DMCChannel(final MemoryBus memoryBus){ | |
| 93 | this.memoryBus = memoryBus; | |
| 94 | this.frequencyDivider = new ParityCountdownFrequencyDivider(this::clockOutputUnit, false, NTSC_DMC_RATES[0]); | |
| 95 | } | |
| 96 | ||
| 97 | private boolean channelIsIdle(){ | |
| 98 |
2
1. channelIsIdle : replaced boolean return with true for com/rox/apu/DMCChannel::channelIsIdle → KILLED 2. channelIsIdle : negated conditional → KILLED |
return bytesRemaining == 0; |
| 99 | } | |
| 100 | ||
| 101 | /** | |
| 102 | * Handle a $4010 write. | |
| 103 | * | |
| 104 | * Register: IL-- RRRR | |
| 105 | * I (bit 7): IRQ enable | |
| 106 | * L (bit 6): loop flag | |
| 107 | * R (bits 0-3): index into {@link #NTSC_DMC_RATES} | |
| 108 | */ | |
| 109 | public void writeControlRegister(final int value){ | |
| 110 |
2
1. writeControlRegister : Replaced bitwise AND with OR → KILLED 2. writeControlRegister : negated conditional → KILLED |
irqEnabled = (value & IRQ_ENABLE_BIT) != 0; |
| 111 |
2
1. writeControlRegister : Replaced bitwise AND with OR → KILLED 2. writeControlRegister : negated conditional → KILLED |
loop = (value & LOOP_BIT) != 0; |
| 112 |
1
1. writeControlRegister : negated conditional → KILLED |
if (!irqEnabled){ |
| 113 | irqPending = false; | |
| 114 | } | |
| 115 |
2
1. writeControlRegister : Replaced bitwise AND with OR → KILLED 2. writeControlRegister : removed call to com/rox/apu/ParityCountdownFrequencyDivider::setCounterPeriod → KILLED |
frequencyDivider.setCounterPeriod(NTSC_DMC_RATES[value & RATE_INDEX_MASK]); |
| 116 | } | |
| 117 | ||
| 118 | /** Register: -DDD DDDD - direct 7-bit load of the delta counter ($4011), bypassing the shift/delta logic. */ | |
| 119 | public void writeDirectLoad(final int value){ | |
| 120 |
1
1. writeDirectLoad : Replaced bitwise AND with OR → KILLED |
deltaCounter = value & DIRECT_LOAD_MASK; |
| 121 | } | |
| 122 | ||
| 123 | /** Register: AAAA AAAA - sample start address = $C000 + value*64 ($4012). */ | |
| 124 | public void writeSampleAddress(final int value){ | |
| 125 |
2
1. writeSampleAddress : Replaced integer addition with subtraction → KILLED 2. writeSampleAddress : Replaced integer multiplication with division → KILLED |
sampleStartAddress = SAMPLE_ADDRESS_BASE + value * SAMPLE_ADDRESS_MULTIPLIER; |
| 126 | } | |
| 127 | ||
| 128 | /** Register: LLLL LLLL - sample length in bytes = value*16 + 1 ($4013). */ | |
| 129 | public void writeSampleLength(final int value){ | |
| 130 |
2
1. writeSampleLength : Replaced integer multiplication with division → KILLED 2. writeSampleLength : Replaced integer addition with subtraction → KILLED |
sampleLength = value * SAMPLE_LENGTH_MULTIPLIER + SAMPLE_LENGTH_OFFSET; |
| 131 | } | |
| 132 | ||
| 133 | /** | |
| 134 | * Starts (or restarts) sample playback: reloads the current address and bytes-remaining | |
| 135 | * counter from the last-written $4012/$4013 values, then immediately primes the shift register | |
| 136 | * (fetching the first sample byte if one is available). Called by {@link #setEnabled(boolean)} | |
| 137 | * when a $4015 write enables this channel while it was idle. | |
| 138 | */ | |
| 139 | public void start(){ | |
| 140 | currentAddress = sampleStartAddress; | |
| 141 | bytesRemaining = sampleLength; | |
| 142 |
1
1. start : removed call to com/rox/apu/DMCChannel::reloadShiftRegister → KILLED |
reloadShiftRegister(); |
| 143 | } | |
| 144 | ||
| 145 | /** | |
| 146 | * Handle a $4015 bit 4 enable-bit change. Enabling only restarts playback if the channel was | |
| 147 | * idle ({@code bytesRemaining == 0}) - a still-playing sample is left alone. Disabling stops | |
| 148 | * future fetches by zeroing {@code bytesRemaining}; whatever's already latched in the shift | |
| 149 | * register keeps playing out until the next reload finds nothing left and silences. | |
| 150 | */ | |
| 151 | public void setEnabled(final boolean enabled){ | |
| 152 |
1
1. setEnabled : negated conditional → KILLED |
if (!enabled){ |
| 153 | bytesRemaining = 0; | |
| 154 |
1
1. setEnabled : negated conditional → KILLED |
} else if (channelIsIdle()){ |
| 155 |
1
1. setEnabled : removed call to com/rox/apu/DMCChannel::start → KILLED |
start(); |
| 156 | } | |
| 157 | } | |
| 158 | ||
| 159 | /** Whether the DMA reader still has sample bytes left to fetch - for the $4015 status read. */ | |
| 160 | public boolean isActive(){ | |
| 161 |
3
1. isActive : replaced boolean return with true for com/rox/apu/DMCChannel::isActive → KILLED 2. isActive : changed conditional boundary → KILLED 3. isActive : negated conditional → KILLED |
return bytesRemaining > 0; |
| 162 | } | |
| 163 | ||
| 164 | /** CPU-cycle clock: the timer runs at half this rate (once per APU cycle), same as pulse/noise. */ | |
| 165 | @Override | |
| 166 | public void tick(){ | |
| 167 |
1
1. tick : removed call to com/rox/apu/ParityCountdownFrequencyDivider::tick → KILLED |
frequencyDivider.tick(); |
| 168 | } | |
| 169 | ||
| 170 | private void clockOutputUnit(){ | |
| 171 |
1
1. clockOutputUnit : negated conditional → KILLED |
if (!outputSilenced){ |
| 172 |
2
1. clockOutputUnit : negated conditional → KILLED 2. clockOutputUnit : Replaced bitwise AND with OR → KILLED |
if ((shiftRegister & 1) != 0){ |
| 173 |
1
1. clockOutputUnit : Replaced integer addition with subtraction → KILLED |
deltaCounter = Math.min(DELTA_MAX, deltaCounter + DELTA_STEP); |
| 174 | } else { | |
| 175 |
1
1. clockOutputUnit : Replaced integer subtraction with addition → KILLED |
deltaCounter = Math.max(DELTA_MIN, deltaCounter - DELTA_STEP); |
| 176 | } | |
| 177 | } | |
| 178 |
1
1. clockOutputUnit : Replaced Shift Right with Shift Left → KILLED |
shiftRegister >>= 1; |
| 179 |
1
1. clockOutputUnit : Replaced integer subtraction with addition → KILLED |
bitsRemainingInShiftRegister--; |
| 180 |
2
1. clockOutputUnit : changed conditional boundary → KILLED 2. clockOutputUnit : negated conditional → KILLED |
if (bitsRemainingInShiftRegister <= 0){ |
| 181 |
1
1. clockOutputUnit : removed call to com/rox/apu/DMCChannel::reloadShiftRegister → KILLED |
reloadShiftRegister(); |
| 182 | } | |
| 183 | } | |
| 184 | ||
| 185 | /** | |
| 186 | * Refills the 8-bit shift register for the next 8 output clocks. On real hardware this is fed | |
| 187 | * by an independent one-byte sample buffer that can be prefetched ahead of the output unit | |
| 188 | * needing it; here the fetch happens synchronously in the same call (see the class Javadoc's | |
| 189 | * simplifications), so there's no separate buffer state to model. | |
| 190 | */ | |
| 191 | private void reloadShiftRegister(){ | |
| 192 | bitsRemainingInShiftRegister = SHIFT_REGISTER_BITS; | |
| 193 |
1
1. reloadShiftRegister : negated conditional → KILLED |
if (channelIsIdle()){ |
| 194 | outputSilenced = true; | |
| 195 | return; | |
| 196 | } | |
| 197 | shiftRegister = fetchNextSampleByte(); | |
| 198 | outputSilenced = false; | |
| 199 | } | |
| 200 | ||
| 201 | /** Fetches the next sample byte, advancing (and wrapping) the address and handling exhaustion. */ | |
| 202 | private int fetchNextSampleByte(){ | |
| 203 | final int value = memoryBus.read(currentAddress); | |
| 204 |
2
1. fetchNextSampleByte : Replaced integer addition with subtraction → KILLED 2. fetchNextSampleByte : negated conditional → KILLED |
currentAddress = currentAddress == MAX_ADDRESS ? ADDRESS_WRAP_TARGET : currentAddress + 1; |
| 205 |
1
1. fetchNextSampleByte : Replaced integer subtraction with addition → KILLED |
bytesRemaining--; |
| 206 |
1
1. fetchNextSampleByte : negated conditional → KILLED |
if (channelIsIdle()){ |
| 207 |
1
1. fetchNextSampleByte : negated conditional → KILLED |
if (loop){ |
| 208 | currentAddress = sampleStartAddress; | |
| 209 | bytesRemaining = sampleLength; | |
| 210 |
1
1. fetchNextSampleByte : negated conditional → KILLED |
} else if (irqEnabled){ |
| 211 | irqPending = true; | |
| 212 | } | |
| 213 | } | |
| 214 |
1
1. fetchNextSampleByte : replaced int return with 0 for com/rox/apu/DMCChannel::fetchNextSampleByte → KILLED |
return value; |
| 215 | } | |
| 216 | ||
| 217 | /** Current output: the 7-bit delta counter (0-127), fed directly into {@link Mixer#mix}. */ | |
| 218 | public int outputSample(){ | |
| 219 |
1
1. outputSample : replaced int return with 0 for com/rox/apu/DMCChannel::outputSample → KILLED |
return deltaCounter; |
| 220 | } | |
| 221 | ||
| 222 | public boolean isIrqPending(){ | |
| 223 |
2
1. isIrqPending : replaced boolean return with false for com/rox/apu/DMCChannel::isIrqPending → KILLED 2. isIrqPending : replaced boolean return with true for com/rox/apu/DMCChannel::isIrqPending → KILLED |
return irqPending; |
| 224 | } | |
| 225 | ||
| 226 | public void clearIrq(){ | |
| 227 | irqPending = false; | |
| 228 | } | |
| 229 | ||
| 230 | int sampleStartAddress(){ | |
| 231 |
1
1. sampleStartAddress : replaced int return with 0 for com/rox/apu/DMCChannel::sampleStartAddress → KILLED |
return sampleStartAddress; |
| 232 | } | |
| 233 | ||
| 234 | int sampleLength(){ | |
| 235 |
1
1. sampleLength : replaced int return with 0 for com/rox/apu/DMCChannel::sampleLength → KILLED |
return sampleLength; |
| 236 | } | |
| 237 | ||
| 238 | int currentAddress(){ | |
| 239 |
1
1. currentAddress : replaced int return with 0 for com/rox/apu/DMCChannel::currentAddress → KILLED |
return currentAddress; |
| 240 | } | |
| 241 | ||
| 242 | int bytesRemaining(){ | |
| 243 |
1
1. bytesRemaining : replaced int return with 0 for com/rox/apu/DMCChannel::bytesRemaining → KILLED |
return bytesRemaining; |
| 244 | } | |
| 245 | ||
| 246 | int currentDeltaCounter(){ | |
| 247 |
1
1. currentDeltaCounter : replaced int return with 0 for com/rox/apu/DMCChannel::currentDeltaCounter → KILLED |
return deltaCounter; |
| 248 | } | |
| 249 | ||
| 250 | int currentShiftRegister(){ | |
| 251 |
1
1. currentShiftRegister : replaced int return with 0 for com/rox/apu/DMCChannel::currentShiftRegister → KILLED |
return shiftRegister; |
| 252 | } | |
| 253 | ||
| 254 | int bitsRemainingInShiftRegister(){ | |
| 255 |
1
1. bitsRemainingInShiftRegister : replaced int return with 0 for com/rox/apu/DMCChannel::bitsRemainingInShiftRegister → KILLED |
return bitsRemainingInShiftRegister; |
| 256 | } | |
| 257 | ||
| 258 | boolean isOutputSilenced(){ | |
| 259 |
2
1. isOutputSilenced : replaced boolean return with true for com/rox/apu/DMCChannel::isOutputSilenced → KILLED 2. isOutputSilenced : replaced boolean return with false for com/rox/apu/DMCChannel::isOutputSilenced → KILLED |
return outputSilenced; |
| 260 | } | |
| 261 | ||
| 262 | int currentTimerPeriod(){ | |
| 263 |
1
1. currentTimerPeriod : replaced int return with 0 for com/rox/apu/DMCChannel::currentTimerPeriod → KILLED |
return frequencyDivider.getCounterPeriod(); |
| 264 | } | |
| 265 | ||
| 266 | boolean isLoopFlagSet(){ | |
| 267 |
2
1. isLoopFlagSet : replaced boolean return with true for com/rox/apu/DMCChannel::isLoopFlagSet → KILLED 2. isLoopFlagSet : replaced boolean return with false for com/rox/apu/DMCChannel::isLoopFlagSet → KILLED |
return loop; |
| 268 | } | |
| 269 | ||
| 270 | /** Everything a {@link DMCChannel} needs to carry on exactly where it was (sample bytes live in the cartridge, not here). */ | |
| 271 | public record Snapshot(boolean irqEnabled, boolean loop, boolean irqPending, int sampleStartAddress, | |
| 272 | int sampleLength, int currentAddress, int bytesRemaining, int shiftRegister, | |
| 273 | int bitsRemainingInShiftRegister, boolean outputSilenced, int deltaCounter, | |
| 274 | ParityCountdownFrequencyDivider.Snapshot frequencyDivider) { | |
| 275 | } | |
| 276 | ||
| 277 | public Snapshot snapshot(){ | |
| 278 |
1
1. snapshot : replaced return value with null for com/rox/apu/DMCChannel::snapshot → KILLED |
return new Snapshot(irqEnabled, loop, irqPending, sampleStartAddress, sampleLength, currentAddress, |
| 279 | bytesRemaining, shiftRegister, bitsRemainingInShiftRegister, outputSilenced, deltaCounter, | |
| 280 | frequencyDivider.snapshot()); | |
| 281 | } | |
| 282 | ||
| 283 | public void restore(final Snapshot snapshot){ | |
| 284 | irqEnabled = snapshot.irqEnabled(); | |
| 285 | loop = snapshot.loop(); | |
| 286 | irqPending = snapshot.irqPending(); | |
| 287 | sampleStartAddress = snapshot.sampleStartAddress(); | |
| 288 | sampleLength = snapshot.sampleLength(); | |
| 289 | currentAddress = snapshot.currentAddress(); | |
| 290 | bytesRemaining = snapshot.bytesRemaining(); | |
| 291 | shiftRegister = snapshot.shiftRegister(); | |
| 292 | bitsRemainingInShiftRegister = snapshot.bitsRemainingInShiftRegister(); | |
| 293 | outputSilenced = snapshot.outputSilenced(); | |
| 294 | deltaCounter = snapshot.deltaCounter(); | |
| 295 |
1
1. restore : removed call to com/rox/apu/ParityCountdownFrequencyDivider::restore → KILLED |
frequencyDivider.restore(snapshot.frequencyDivider()); |
| 296 | } | |
| 297 | } | |
Mutations | ||
| 98 |
1.1 2.2 |
|
| 110 |
1.1 2.2 |
|
| 111 |
1.1 2.2 |
|
| 112 |
1.1 |
|
| 115 |
1.1 2.2 |
|
| 120 |
1.1 |
|
| 125 |
1.1 2.2 |
|
| 130 |
1.1 2.2 |
|
| 142 |
1.1 |
|
| 152 |
1.1 |
|
| 154 |
1.1 |
|
| 155 |
1.1 |
|
| 161 |
1.1 2.2 3.3 |
|
| 167 |
1.1 |
|
| 171 |
1.1 |
|
| 172 |
1.1 2.2 |
|
| 173 |
1.1 |
|
| 175 |
1.1 |
|
| 178 |
1.1 |
|
| 179 |
1.1 |
|
| 180 |
1.1 2.2 |
|
| 181 |
1.1 |
|
| 193 |
1.1 |
|
| 204 |
1.1 2.2 |
|
| 205 |
1.1 |
|
| 206 |
1.1 |
|
| 207 |
1.1 |
|
| 210 |
1.1 |
|
| 214 |
1.1 |
|
| 219 |
1.1 |
|
| 223 |
1.1 2.2 |
|
| 231 |
1.1 |
|
| 235 |
1.1 |
|
| 239 |
1.1 |
|
| 243 |
1.1 |
|
| 247 |
1.1 |
|
| 251 |
1.1 |
|
| 255 |
1.1 |
|
| 259 |
1.1 2.2 |
|
| 263 |
1.1 |
|
| 267 |
1.1 2.2 |
|
| 278 |
1.1 |
|
| 295 |
1.1 |