• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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