Resampler.java

1
package com.rox.audio;
2
3
import java.util.OptionalDouble;
4
5
/**
6
 * Downsamples a per-input-cycle stream of audio samples (e.g. the APU's per-CPU-cycle mixed
7
 * output, ~1.79MHz) to a lower output rate (e.g. 44100Hz) by averaging each window of input
8
 * samples into one output sample (a box filter).
9
 *
10
 * The input:output ratio is rarely a whole number (1,789,773 / 44,100 ≈ 40.61), so a fixed
11
 * window size would drift the output rate over time. Instead this mirrors
12
 * {@link com.rox.clock.FPSClock#ticksThisFrame()}'s fractional-remainder accumulation: window
13
 * sizes are mostly {@code inputRate / outputRate} input samples, with an extra sample folded in
14
 * periodically so the long-run average window size is exactly {@code inputRate / outputRate}
15
 * (kept exact, not just close, the same way {@code FPSClock} keeps its tick rate exact).
16
 */
17
public class Resampler {
18
    private final long baseWindowSize;
19
    private final long outputRate;
20
    private final long windowSizeRemainderPerOutputSample;
21
22
    private long windowRemainderBuffer;
23
    private long samplesNeededForCurrentWindow;
24
25
    private double accumulatedSum;
26
    private long accumulatedSampleCount;
27
28
    public Resampler(final long inputRate, final long outputRate){
29 4 1. <init> : changed conditional boundary → SURVIVED
2. <init> : negated conditional → KILLED
3. <init> : negated conditional → KILLED
4. <init> : changed conditional boundary → KILLED
        if (inputRate <= 0 || outputRate <= 0){
30
            throw new IllegalArgumentException(
31
                    "inputRate and outputRate must both be positive, got inputRate=" + inputRate + ", outputRate=" + outputRate);
32
        }
33 2 1. <init> : negated conditional → KILLED
2. <init> : changed conditional boundary → KILLED
        if (inputRate < outputRate){
34
            throw new IllegalArgumentException(
35
                    "inputRate must be >= outputRate (this is a downsampler only), got inputRate=" + inputRate + ", outputRate=" + outputRate);
36
        }
37
        this.outputRate = outputRate;
38 1 1. <init> : Replaced long division with multiplication → KILLED
        this.baseWindowSize = inputRate / outputRate;
39 1 1. <init> : Replaced long modulus with multiplication → KILLED
        this.windowSizeRemainderPerOutputSample = inputRate % outputRate;
40
        this.samplesNeededForCurrentWindow = nextWindowSize();
41
    }
42
43
    /**
44
     * Accepts one input-rate sample. Returns the averaged output sample once a full window has
45
     * been accumulated, otherwise empty.
46
     */
47
    public OptionalDouble accept(final double sample){
48 1 1. accept : Replaced double addition with subtraction → KILLED
        accumulatedSum += sample;
49 1 1. accept : Replaced long addition with subtraction → KILLED
        accumulatedSampleCount++;
50
51 2 1. accept : changed conditional boundary → KILLED
2. accept : negated conditional → KILLED
        if (accumulatedSampleCount < samplesNeededForCurrentWindow){
52 1 1. accept : replaced return value with null for com/rox/audio/Resampler::accept → KILLED
            return OptionalDouble.empty();
53
        }
54
55 1 1. accept : Replaced double division with multiplication → KILLED
        final double average = accumulatedSum / accumulatedSampleCount;
56
        accumulatedSum = 0;
57
        accumulatedSampleCount = 0;
58
        samplesNeededForCurrentWindow = nextWindowSize();
59 1 1. accept : replaced return value with null for com/rox/audio/Resampler::accept → KILLED
        return OptionalDouble.of(average);
60
    }
61
62
    private long nextWindowSize(){
63
        long windowSize = baseWindowSize;
64 1 1. nextWindowSize : Replaced long addition with subtraction → KILLED
        windowRemainderBuffer += windowSizeRemainderPerOutputSample;
65 2 1. nextWindowSize : negated conditional → KILLED
2. nextWindowSize : changed conditional boundary → KILLED
        if (windowRemainderBuffer >= outputRate){
66 1 1. nextWindowSize : Replaced long addition with subtraction → KILLED
            windowSize++;
67 1 1. nextWindowSize : Replaced long subtraction with addition → KILLED
            windowRemainderBuffer -= outputRate;
68
        }
69 1 1. nextWindowSize : replaced long return with 0 for com/rox/audio/Resampler::nextWindowSize → KILLED
        return windowSize;
70
    }
71
}

Mutations

29

1.1
Location : <init>
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:outputIsTheAverageOfItsWindow()]
negated conditional → KILLED

2.2
Location : <init>
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:rejectsNegativeOutputRate()]
negated conditional → KILLED

3.3
Location : <init>
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:rejectsZeroOutputRate()]
changed conditional boundary → KILLED

4.4
Location : <init>
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

33

1.1
Location : <init>
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:rejectsInputRateBelowOutputRate()]
negated conditional → KILLED

2.2
Location : <init>
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:allowsInputRateEqualToOutputRate()]
changed conditional boundary → KILLED

38

1.1
Location : <init>
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:emptyUntilTheWindowIsFull()]
Replaced long division with multiplication → KILLED

39

1.1
Location : <init>
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:emptyUntilTheWindowIsFull()]
Replaced long modulus with multiplication → KILLED

48

1.1
Location : accept
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:outputIsTheAverageOfItsWindow()]
Replaced double addition with subtraction → KILLED

49

1.1
Location : accept
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:emptyUntilTheWindowIsFull()]
Replaced long addition with subtraction → KILLED

51

1.1
Location : accept
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:emptyUntilTheWindowIsFull()]
changed conditional boundary → KILLED

2.2
Location : accept
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:emptyUntilTheWindowIsFull()]
negated conditional → KILLED

52

1.1
Location : accept
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:emptyUntilTheWindowIsFull()]
replaced return value with null for com/rox/audio/Resampler::accept → KILLED

55

1.1
Location : accept
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:outputIsTheAverageOfItsWindow()]
Replaced double division with multiplication → KILLED

59

1.1
Location : accept
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:emptyUntilTheWindowIsFull()]
replaced return value with null for com/rox/audio/Resampler::accept → KILLED

64

1.1
Location : nextWindowSize
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:windowSizesSumExactlyToTheInputRateOverMultipleCycles()]
Replaced long addition with subtraction → KILLED

65

1.1
Location : nextWindowSize
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:emptyUntilTheWindowIsFull()]
negated conditional → KILLED

2.2
Location : nextWindowSize
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:windowSizesSumExactlyToTheInputRateOverMultipleCycles()]
changed conditional boundary → KILLED

66

1.1
Location : nextWindowSize
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:windowSizesSumExactlyToTheInputRateOverMultipleCycles()]
Replaced long addition with subtraction → KILLED

67

1.1
Location : nextWindowSize
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:windowSizesSumExactlyToTheInputRateOverMultipleCycles()]
Replaced long subtraction with addition → KILLED

69

1.1
Location : nextWindowSize
Killed by : com.rox.audio.ResamplerTest.[engine:junit-jupiter]/[class:com.rox.audio.ResamplerTest]/[method:emptyUntilTheWindowIsFull()]
replaced long return with 0 for com/rox/audio/Resampler::nextWindowSize → KILLED

Active mutators

Tests examined


Report generated by PIT 1.22.1