blob: 6b9d4b2a6b0879b087d6b0afe7db2012b93b9870 [file] [log] [blame]
/*
* Copyright 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mobileer.oboetester;
import java.util.ArrayList;
/**
* Analyze a recording and extract edges for latency analysis.
*/
public class TapLatencyAnalyser {
public static final int TYPE_TAP = 0;
float[] mHighPassBuffer;
private float mDroop = 0.995f;
private static final float EDGE_THRESHOLD = 0.01f;
private static final float LOW_FRACTION = 0.5f;
public static class TapLatencyEvent {
public int type;
public int sampleIndex;
public TapLatencyEvent(int type, int sampleIndex) {
this.type = type;
this.sampleIndex = sampleIndex;
}
}
public TapLatencyEvent[] analyze(float[] buffer, int offset, int numSamples) {
// Use high pass filter to remove rumble from air conditioners.
mHighPassBuffer = new float[numSamples];
highPassFilter(buffer, offset, numSamples, mHighPassBuffer);
// Apply envelope follower.
float[] peakBuffer = new float[numSamples];
fillPeakBuffer(mHighPassBuffer, 0, numSamples, peakBuffer);
// Look for two attacks.
return scanForEdges(peakBuffer, numSamples);
}
public float[] getFilteredBuffer() {
return mHighPassBuffer;
}
// Based on https://en.wikipedia.org/wiki/High-pass_filter
private void highPassFilter(float[] buffer, int offset, int numSamples, float[] highPassBuffer) {
float xn1 = 0.0f;
float yn1 = 0.0f;
final float alpha = 0.8f;
for (int i = 0; i < numSamples; i++) {
float xn = buffer[i + offset];
float yn = alpha * (yn1 + xn - xn1);
highPassBuffer[i] = yn;
xn1 = xn;
yn1 = yn;
}
}
private TapLatencyEvent[] scanForEdges(float[] peakBuffer, int numSamples) {
ArrayList<TapLatencyEvent> events = new ArrayList<TapLatencyEvent>();
float slow = 0.0f;
float fast = 0.0f;
final float slowCoefficient = 0.01f;
final float fastCoefficient = 0.10f;
float lowThreshold = EDGE_THRESHOLD;
boolean armed = true;
int sampleIndex = 0;
for (float level : peakBuffer) {
slow = slow + (level - slow) * slowCoefficient; // low pass filter
fast = fast + (level - fast) * fastCoefficient; // low pass filter
if (armed && (fast > EDGE_THRESHOLD) && (fast > (2.0 * slow))) {
//System.out.println("edge at " + sampleIndex + ", slow " + slow + ", fast " + fast);
events.add(new TapLatencyEvent(TYPE_TAP, sampleIndex));
armed = false;
lowThreshold = fast * LOW_FRACTION;
}
// Use hysteresis when rearming.
if (!armed && (fast < lowThreshold)) {
armed = true;
// slow = fast; // This seems unnecessary.
//events.add(new TapLatencyEvent(TYPE_TAP, sampleIndex));
}
sampleIndex++;
}
return events.toArray(new TapLatencyEvent[0]);
}
/**
* Envelope follower that rides along the peaks of the waveforms
* and then decays exponentially.
*
* @param buffer
* @param offset
* @param numSamples
*/
private void fillPeakBuffer(float[] buffer, int offset, int numSamples, float[] peakBuffer) {
float previous = 0.0f;
for (int i = 0; i < numSamples; i++) {
float input = Math.abs(buffer[i + offset]);
float output = previous * mDroop;
if (input > output) {
output = input;
}
previous = output;
peakBuffer[i] = output;
}
}
}