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