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