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