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