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