Mixer.java

1
package com.rox.apu;
2
3
/**
4
 * Combines the five NES APU channel outputs into a single sample (~0.0-1.0).
5
 *
6
 * <p>Real NES hardware doesn't mix its 5 channels with a simple average, and it doesn't mix them
7
 * all together in one place either. Two separate analog resistor networks on the chip each sum a
8
 * subset of the channels, and each network's output voltage is a <em>non-linear</em> function of
9
 * its inputs (a consequence of the physical circuit, not a deliberate audio design choice):
10
 * <ul>
11
 *     <li>one network sums {@code pulse1} + {@code pulse2} ({@link #pulseOut});</li>
12
 *     <li>the other sums {@code triangle} + {@code noise} + {@code dmc} - commonly abbreviated
13
 *     "TND" in NES documentation and emulator source - ({@link #tndOut}).</li>
14
 * </ul>
15
 * The two formulas below are curve-fits (reverse-engineered from real hardware measurements) for
16
 * those two circuits' voltage output, not something derived from first principles - which is why
17
 * their constants (95.88, 8128, 8227, 12241, 22638, ...) don't individually mean anything; they're
18
 * just the coefficients that reproduce the measured curves. {@code output} is simply the sum of
19
 * both circuits' voltages, since on real hardware that's the final combined analog signal.
20
 *
21
 * <p>{@code pulse1}/{@code pulse2}/{@code triangle}/{@code noise} are 0-15, {@code dmc} is 0-127.
22
 *
23
 * <p>Deliberately not precomputing a single lookup table for {@link #tndOut} keyed by a combined
24
 * index like {@code 3*triangle + 2*noise + dmc} (a common optimisation in other NES emulators):
25
 * that collapses three independently-weighted terms (divisors 8227/12241/22638 - not proportional
26
 * to 3:2:1) into one dimension, which is only an approximation of the real circuit's curve, not an
27
 * exact optimisation of it. The direct computation below is exact and cheap enough per call that
28
 * the accuracy trade-off isn't worth making.
29
 */
30
public final class Mixer {
31
    /** Curve-fit coefficients for the pulse1+pulse2 summing circuit's output voltage. */
32
    private static final double PULSE_CIRCUIT_NUMERATOR = 95.88;
33
    private static final double PULSE_CIRCUIT_DIVISOR_CONSTANT = 8128.0;
34
    private static final double PULSE_CIRCUIT_OFFSET = 100.0;
35
36
    /** Curve-fit coefficients for the triangle+noise+dmc ("TND") summing circuit's output voltage. */
37
    private static final double TND_CIRCUIT_NUMERATOR = 159.79;
38
    private static final double TRIANGLE_CONTRIBUTION_DIVISOR = 8227.0;
39
    private static final double NOISE_CONTRIBUTION_DIVISOR = 12241.0;
40
    private static final double DMC_CONTRIBUTION_DIVISOR = 22638.0;
41
    private static final double TND_CIRCUIT_OFFSET = 100.0;
42
43
    private Mixer(){
44
        /**
45
         * XXX Code coverage fails us here. This constructors only job is to make `new Mixer()` a compile error,
46
         * since a utility class with only static methods being instantiable is meaningless. If I remove it,
47
         * Java auto-generates a public no-arg constructor, so `new Mixer()` becomes legal (harmless, but there's
48
         * no longer anything stopping it).
49
         */
50
    }
51
52
    /** The combined analog output voltage of both summing circuits, as they'd appear on real hardware. */
53
    public static double mix(final int pulse1,
54
                             final int pulse2,
55
                             final int triangle,
56
                             final int noise,
57
                             final int dmc){
58 2 1. mix : Replaced double addition with subtraction → KILLED
2. mix : replaced double return with 0.0d for com/rox/apu/Mixer::mix → KILLED
        return pulseOut(pulse1, pulse2) + tndOut(triangle, noise, dmc);
59
    }
60
61
    /** The pulse1+pulse2 summing circuit's output voltage (0 when both channels are silent). */
62
    private static double pulseOut(final int pulse1,
63
                                   final int pulse2){
64 1 1. pulseOut : Replaced integer addition with subtraction → KILLED
        final int sum = pulse1 + pulse2;
65 1 1. pulseOut : negated conditional → KILLED
        if (sum == 0){
66
            return 0;
67
        }
68 4 1. pulseOut : replaced double return with 0.0d for com/rox/apu/Mixer::pulseOut → KILLED
2. pulseOut : Replaced double division with multiplication → KILLED
3. pulseOut : Replaced double addition with subtraction → KILLED
4. pulseOut : Replaced double division with multiplication → KILLED
        return PULSE_CIRCUIT_NUMERATOR / (PULSE_CIRCUIT_DIVISOR_CONSTANT / sum + PULSE_CIRCUIT_OFFSET);
69
    }
70
71
    /** The triangle+noise+dmc ("TND") summing circuit's output voltage (0 when all three are silent). */
72
    private static double tndOut(final int triangle, final int noise, final int dmc){
73 3 1. tndOut : negated conditional → SURVIVED
2. tndOut : negated conditional → KILLED
3. tndOut : negated conditional → KILLED
        if (triangle == 0 && noise == 0 && dmc == 0){
74
            return 0;
75
        }
76 5 1. tndOut : Replaced double division with multiplication → KILLED
2. tndOut : Replaced double addition with subtraction → KILLED
3. tndOut : Replaced double addition with subtraction → KILLED
4. tndOut : Replaced double division with multiplication → KILLED
5. tndOut : Replaced double division with multiplication → KILLED
        final double combinedContribution = triangle / TRIANGLE_CONTRIBUTION_DIVISOR
77
                + noise / NOISE_CONTRIBUTION_DIVISOR
78
                + dmc / DMC_CONTRIBUTION_DIVISOR;
79 4 1. tndOut : Replaced double division with multiplication → KILLED
2. tndOut : Replaced double addition with subtraction → KILLED
3. tndOut : Replaced double division with multiplication → KILLED
4. tndOut : replaced double return with 0.0d for com/rox/apu/Mixer::tndOut → KILLED
        return TND_CIRCUIT_NUMERATOR / (1.0 / combinedContribution + TND_CIRCUIT_OFFSET);
80
    }
81
}

Mutations

58

1.1
Location : mix
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:dmcAloneAtMaxMatchesGoldenValue()]
Replaced double addition with subtraction → KILLED

2.2
Location : mix
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:dmcAloneAtMaxMatchesGoldenValue()]
replaced double return with 0.0d for com/rox/apu/Mixer::mix → KILLED

64

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

65

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

68

1.1
Location : pulseOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:zeroTndWithNonZeroPulseIsNotSilencedByTheTndZeroCase()]
replaced double return with 0.0d for com/rox/apu/Mixer::pulseOut → KILLED

2.2
Location : pulseOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:mixedChannelsMatchGoldenValue()]
Replaced double division with multiplication → KILLED

3.3
Location : pulseOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:mixedChannelsMatchGoldenValue()]
Replaced double addition with subtraction → KILLED

4.4
Location : pulseOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:mixedChannelsMatchGoldenValue()]
Replaced double division with multiplication → KILLED

73

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

2.2
Location : tndOut
Killed by : none
negated conditional → SURVIVED
Covering tests

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

76

1.1
Location : tndOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:maxTriangleNoiseAndDmcOnlyMatchesGoldenValue()]
Replaced double division with multiplication → KILLED

2.2
Location : tndOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:maxTriangleNoiseAndDmcOnlyMatchesGoldenValue()]
Replaced double addition with subtraction → KILLED

3.3
Location : tndOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:dmcAloneAtMaxMatchesGoldenValue()]
Replaced double addition with subtraction → KILLED

4.4
Location : tndOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:dmcAloneAtMaxMatchesGoldenValue()]
Replaced double division with multiplication → KILLED

5.5
Location : tndOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:maxTriangleNoiseAndDmcOnlyMatchesGoldenValue()]
Replaced double division with multiplication → KILLED

79

1.1
Location : tndOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:dmcAloneAtMaxMatchesGoldenValue()]
Replaced double division with multiplication → KILLED

2.2
Location : tndOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:dmcAloneAtMaxMatchesGoldenValue()]
Replaced double addition with subtraction → KILLED

3.3
Location : tndOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:dmcAloneAtMaxMatchesGoldenValue()]
Replaced double division with multiplication → KILLED

4.4
Location : tndOut
Killed by : com.rox.apu.MixerTest.[engine:junit-jupiter]/[class:com.rox.apu.MixerTest]/[method:dmcAloneAtMaxMatchesGoldenValue()]
replaced double return with 0.0d for com/rox/apu/Mixer::tndOut → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1