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.audiolib;
17 
18 import java.util.ArrayList;
19 
20 /**
21  * Analyzes a block of samples to find a trigger "tick" (presumably from a screen tap) and the
22  * resulting "blip" tone and compute the latency between those events.
23  */
24 public class TapLatencyAnalyser {
25     public static final int TYPE_TAP = 0;
26     float[] mHighPassBuffer;
27 
28     private float mDroop = 0.995f;
29     private static final float LOW_THRESHOLD = 0.01f;
30     private static final float HIGH_THRESHOLD = 0.03f;
31 
32     /**
33      * A class to hold "events" discovered by the TapLatencyAnalyser
34      */
35     public static class TapLatencyEvent {
36         public int type;
37         public int sampleIndex;
TapLatencyEvent(int type, int sampleIndex)38         public TapLatencyEvent(int type, int sampleIndex) {
39             this.type = type;
40             this.sampleIndex = sampleIndex;
41         }
42     }
43 
44     /**
45      * Analyzes the provided audio data to find audio event "edges"
46      * @param buffer    Audio samples to analyze.
47      * @param offset    Offset within the provide buffer to start analysis.
48      * @param numSamples    Number of samples to analyze.
49      * @return
50      */
analyze(float[] buffer, int offset, int numSamples)51     public TapLatencyEvent[] analyze(float[] buffer, int offset, int numSamples) {
52         // Use high pass filter to remove rumble from air conditioners.
53         mHighPassBuffer = new float[numSamples];
54         highPassFilter(buffer, offset, numSamples, mHighPassBuffer);
55         float[] peakBuffer = new float[numSamples];
56         fillPeakBuffer(mHighPassBuffer, 0, numSamples, peakBuffer);
57         return scanForEdges(peakBuffer, numSamples);
58     }
59 
60     /**
61      * @return The filtered samples on which the analysis was performed.
62      *   High-pass filtered to emphasize high-frequency events such as edges.
63      */
getFilteredBuffer()64     public float[] getFilteredBuffer() {
65         return mHighPassBuffer;
66     }
67 
highPassFilter(float[] buffer, int offset, int numSamples, float[] highPassBuffer)68     private void highPassFilter(float[] buffer, int offset, int numSamples,
69                                 float[] highPassBuffer) {
70         float xn1 = 0.0f;
71         float yn1 = 0.0f;
72         final float alpha = 0.05f;
73         for (int i = 0; i < numSamples; i++) {
74             float xn = buffer[i + offset];
75             float yn = alpha * yn1 + ((1.0f - alpha) * (xn - xn1));
76             highPassBuffer[i] = yn;
77             xn1 = xn;
78             yn1 = yn;
79         }
80     }
81 
scanForEdges(float[] peakBuffer, int numSamples)82     private TapLatencyEvent[] scanForEdges(float[] peakBuffer, int numSamples) {
83         ArrayList<TapLatencyEvent> events = new ArrayList<TapLatencyEvent>();
84         float slow = 0.0f;
85         float fast = 0.0f;
86         float slowCoefficient = 0.01f;
87         float fastCoefficient = 0.10f;
88         boolean armed = true;
89         int sampleIndex = 0;
90         for (float level : peakBuffer) {
91             slow = slow + (level - slow) * slowCoefficient; // low pass filter
92             fast = fast + (level - fast) * fastCoefficient;
93             if (armed && (fast > HIGH_THRESHOLD) && (fast > (2.0 * slow))) {
94                 events.add(new TapLatencyEvent(TYPE_TAP, sampleIndex));
95                 armed = false;
96             }
97             // Use hysteresis when rearming.
98             if (!armed && (fast < LOW_THRESHOLD)) {
99                 armed = true;
100             }
101             sampleIndex++;
102         }
103         return events.toArray(new TapLatencyEvent[0]);
104     }
105 
fillPeakBuffer(float[] buffer, int offset, int numSamples, float[] peakBuffer)106     private void fillPeakBuffer(float[] buffer, int offset, int numSamples, float[] peakBuffer) {
107         float previous = 0.0f;
108         for (int i = 0; i < numSamples; i++) {
109             float input = buffer[i + offset];
110             float output = previous * mDroop;
111             if (input > output) {
112                 output = input;
113             }
114             previous = output;
115             peakBuffer[i] = output;
116         }
117     }
118 }
119