1 package com.android.cts.verifier.audio; 2 3 import org.apache.commons.math.complex.Complex; 4 5 import java.nio.ByteBuffer; 6 import java.nio.ByteOrder; 7 8 /** 9 * Class contains the analysis to calculate frequency response. 10 */ 11 public class WavAnalyzer { 12 private final Listener listener; 13 private final int sampleRate; // Recording sampling rate. 14 private double[] data; // Whole recording data. 15 private double[] dB; // Average response 16 private double[][] power; // power of each trial 17 private double[] noiseDB; // background noise 18 private double[][] noisePower; 19 private double threshold; // threshold of passing, drop off compared to 2000 kHz 20 private boolean result = false; // result of the test 21 22 /** 23 * Constructor of WavAnalyzer. 24 */ WavAnalyzer(byte[] byteData, int sampleRate, Listener listener)25 public WavAnalyzer(byte[] byteData, int sampleRate, Listener listener) { 26 this.listener = listener; 27 this.sampleRate = sampleRate; 28 29 short[] shortData = new short[byteData.length >> 1]; 30 ByteBuffer.wrap(byteData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shortData); 31 this.data = Util.toDouble(shortData); 32 for (int i = 0; i < data.length; i++) { 33 data[i] = data[i] / Short.MAX_VALUE; 34 } 35 } 36 37 /** 38 * Do the analysis. Returns true if passing, false if failing. 39 */ doWork()40 public boolean doWork() { 41 if (isClipped()) { 42 return false; 43 } 44 // Calculating the pip strength. 45 listener.sendMessage("Calculating... Please wait...\n"); 46 try { 47 dB = measurePipStrength(); 48 } catch (IndexOutOfBoundsException e) { 49 listener.sendMessage("WARNING: May have missed the prefix." 50 + " Turn up the volume of the playback device or move to a quieter location.\n"); 51 return false; 52 } 53 if (!isConsistent()) { 54 return false; 55 } 56 result = responsePassesHifiTest(dB); 57 return result; 58 } 59 60 /** 61 * Check if the recording is clipped. 62 */ isClipped()63 boolean isClipped() { 64 for (int i = 1; i < data.length; i++) { 65 if ((Math.abs(data[i]) >= Short.MAX_VALUE) && (Math.abs(data[i - 1]) >= Short.MAX_VALUE)) { 66 listener.sendMessage("WARNING: Data is clipped." 67 + " Turn down the volume of the playback device and redo the procedure.\n"); 68 return true; 69 } 70 } 71 return false; 72 } 73 74 /** 75 * Check if the result is consistant across trials. 76 */ isConsistent()77 boolean isConsistent() { 78 double[] coeffOfVar = new double[Common.PIP_NUM]; 79 for (int i = 0; i < Common.PIP_NUM; i++) { 80 double[] powerAtFreq = new double[Common.REPETITIONS]; 81 for (int j = 0; j < Common.REPETITIONS; j++) { 82 powerAtFreq[j] = power[i][j]; 83 } 84 coeffOfVar[i] = Util.std(powerAtFreq) / Util.mean(powerAtFreq); 85 } 86 if (Util.mean(coeffOfVar) > 1.0) { 87 listener.sendMessage("WARNING: Inconsistent result across trials." 88 + " Turn up the volume of the playback device or move to a quieter location.\n"); 89 return false; 90 } 91 return true; 92 } 93 94 /** 95 * Determine test pass/fail using the frequency response. Package visible for unit testing. 96 */ responsePassesHifiTest(double[] dB)97 boolean responsePassesHifiTest(double[] dB) { 98 for (int i = 0; i < dB.length; i++) { 99 // Precautionary; NaN should not happen. 100 if (Double.isNaN(dB[i])) { 101 listener.sendMessage( 102 "WARNING: Unexpected NaN in result. Redo the test.\n"); 103 return false; 104 } 105 } 106 107 if (Util.mean(dB) - Util.mean(noiseDB) < Common.SIGNAL_MIN_STRENGTH_DB_ABOVE_NOISE) { 108 listener.sendMessage("WARNING: Signal is too weak or background noise is too strong." 109 + " Turn up the volume of the playback device or move to a quieter location.\n"); 110 return false; 111 } 112 113 int indexOf2000Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 2000.0); 114 threshold = dB[indexOf2000Hz] + Common.PASSING_THRESHOLD_DB; 115 int indexOf18500Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 18500.0); 116 int indexOf20000Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 20000.0); 117 double[] responseInRange = new double[indexOf20000Hz - indexOf18500Hz]; 118 System.arraycopy(dB, indexOf18500Hz, responseInRange, 0, responseInRange.length); 119 if (Util.mean(responseInRange) < threshold) { 120 listener.sendMessage( 121 "WARNING: Failed. Retry with different orientations or report failed.\n"); 122 return false; 123 } 124 return true; 125 } 126 127 /** 128 * Calculate the Fourier Coefficient at the pip frequency to calculate the frequency response. 129 * Package visible for unit testing. 130 */ measurePipStrength()131 double[] measurePipStrength() { 132 listener.sendMessage("Aligning data... Please wait...\n"); 133 final int dataStartI = alignData(); 134 final int prefixTotalLength = dataStartI 135 + Util.toLength(Common.PREFIX_LENGTH_S + Common.PAUSE_AFTER_PREFIX_DURATION_S, sampleRate); 136 listener.sendMessage("Done.\n"); 137 listener.sendMessage("Prefix starts at " + (double) dataStartI / sampleRate + " s \n"); 138 if (dataStartI > Math.round(sampleRate * (Common.PREFIX_LENGTH_S 139 + Common.PAUSE_BEFORE_PREFIX_DURATION_S + Common.PAUSE_AFTER_PREFIX_DURATION_S))) { 140 listener.sendMessage("WARNING: Unexpected prefix start time. May have missed the prefix.\n" 141 + "PLAY button should be pressed on the playback device within one second" 142 + " after RECORD is pressed on the recording device.\n" 143 + "If this happens repeatedly," 144 + " turn up the volume of the playback device or move to a quieter location.\n"); 145 } 146 147 listener.sendMessage("Analyzing noise strength... Please wait...\n"); 148 noisePower = new double[Common.PIP_NUM][Common.NOISE_SAMPLES]; 149 noiseDB = new double[Common.PIP_NUM]; 150 for (int s = 0; s < Common.NOISE_SAMPLES; s++) { 151 double[] noisePoints = new double[Common.WINDOW_FOR_RECORDER.length]; 152 System.arraycopy(data, dataStartI - (s + 1) * noisePoints.length - 1, 153 noisePoints, 0, noisePoints.length); 154 for (int j = 0; j < noisePoints.length; j++) { 155 noisePoints[j] = noisePoints[j] * Common.WINDOW_FOR_RECORDER[j]; 156 } 157 for (int i = 0; i < Common.PIP_NUM; i++) { 158 double freq = Common.FREQUENCIES_ORIGINAL[i]; 159 Complex fourierCoeff = new Complex(0, 0); 160 final Complex rotator = new Complex(0, 161 -2.0 * Math.PI * freq / sampleRate).exp(); 162 Complex phasor = new Complex(1, 0); 163 for (int j = 0; j < noisePoints.length; j++) { 164 fourierCoeff = fourierCoeff.add(phasor.multiply(noisePoints[j])); 165 phasor = phasor.multiply(rotator); 166 } 167 fourierCoeff = fourierCoeff.multiply(1.0 / noisePoints.length); 168 noisePower[i][s] = fourierCoeff.multiply(fourierCoeff.conjugate()).abs(); 169 } 170 } 171 for (int i = 0; i < Common.PIP_NUM; i++) { 172 double meanNoisePower = 0; 173 for (int j = 0; j < Common.NOISE_SAMPLES; j++) { 174 meanNoisePower += noisePower[i][j]; 175 } 176 meanNoisePower /= Common.NOISE_SAMPLES; 177 noiseDB[i] = 10 * Math.log10(meanNoisePower); 178 } 179 180 listener.sendMessage("Analyzing pips... Please wait...\n"); 181 power = new double[Common.PIP_NUM][Common.REPETITIONS]; 182 for (int i = 0; i < Common.PIP_NUM * Common.REPETITIONS; i++) { 183 if (i % Common.PIP_NUM == 0) { 184 listener.sendMessage("#" + (i / Common.PIP_NUM + 1) + "\n"); 185 } 186 187 int pipExpectedStartI; 188 pipExpectedStartI = prefixTotalLength 189 + Util.toLength(i * (Common.PIP_DURATION_S + Common.PAUSE_DURATION_S), sampleRate); 190 // Cut out the data points for the current pip. 191 double[] pipPoints = new double[Common.WINDOW_FOR_RECORDER.length]; 192 System.arraycopy(data, pipExpectedStartI, pipPoints, 0, pipPoints.length); 193 for (int j = 0; j < Common.WINDOW_FOR_RECORDER.length; j++) { 194 pipPoints[j] = pipPoints[j] * Common.WINDOW_FOR_RECORDER[j]; 195 } 196 Complex fourierCoeff = new Complex(0, 0); 197 final Complex rotator = new Complex(0, 198 -2.0 * Math.PI * Common.FREQUENCIES[i] / sampleRate).exp(); 199 Complex phasor = new Complex(1, 0); 200 for (int j = 0; j < pipPoints.length; j++) { 201 fourierCoeff = fourierCoeff.add(phasor.multiply(pipPoints[j])); 202 phasor = phasor.multiply(rotator); 203 } 204 fourierCoeff = fourierCoeff.multiply(1.0 / pipPoints.length); 205 int j = Common.ORDER[i]; 206 power[j % Common.PIP_NUM][j / Common.PIP_NUM] = 207 fourierCoeff.multiply(fourierCoeff.conjugate()).abs(); 208 } 209 210 // Calculate median of trials. 211 double[] dB = new double[Common.PIP_NUM]; 212 for (int i = 0; i < Common.PIP_NUM; i++) { 213 dB[i] = 10 * Math.log10(Util.median(power[i])); 214 } 215 return dB; 216 } 217 218 /** 219 * Align data using prefix. Package visible for unit testing. 220 */ alignData()221 int alignData() { 222 // Zeropadding samples to add in the correlation to avoid FFT wraparound. 223 final int zeroPad = Util.toLength(Common.PREFIX_LENGTH_S, Common.RECORDING_SAMPLE_RATE_HZ) - 1; 224 int fftSize = Util.nextPowerOfTwo((int) Math.round(sampleRate * (Common.PREFIX_LENGTH_S 225 + Common.PAUSE_BEFORE_PREFIX_DURATION_S + Common.PAUSE_AFTER_PREFIX_DURATION_S + 0.5)) 226 + zeroPad); 227 228 double[] dataCut = new double[fftSize - zeroPad]; 229 System.arraycopy(data, 0, dataCut, 0, fftSize - zeroPad); 230 double[] xCorrDataPrefix = Util.computeCrossCorrelation( 231 Util.padZeros(Util.toComplex(dataCut), fftSize), 232 Util.padZeros(Util.toComplex(Common.PREFIX_FOR_RECORDER), fftSize)); 233 return Util.findMaxIndex(xCorrDataPrefix); 234 } 235 getDB()236 double[] getDB() { 237 return dB; 238 } 239 getPower()240 double[][] getPower() { 241 return power; 242 } 243 getNoiseDB()244 double[] getNoiseDB() { 245 return noiseDB; 246 } 247 getThreshold()248 double getThreshold() { 249 return threshold; 250 } 251 getResult()252 boolean getResult() { 253 return result; 254 } 255 256 /** 257 * An interface for listening a message publishing the progress of the analyzer. 258 */ 259 public interface Listener { 260 sendMessage(String message)261 void sendMessage(String message); 262 } 263 } 264