blob: b75c40b4ab91b0f46d8d34b4e725892f56277989 [file] [log] [blame]
package com.android.cts.verifier.audio;
import org.apache.commons.math.complex.Complex;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
/**
* Class contains the analysis to calculate frequency response.
*/
public class WavAnalyzer {
private final Listener listener;
private final int sampleRate; // Recording sampling rate.
private double[] data; // Whole recording data.
private double[] dB; // Average response
private double[][] power; // power of each trial
private double[] noiseDB; // background noise
private double[][] noisePower;
private double threshold; // threshold of passing, drop off compared to 2000 kHz
private boolean result = false; // result of the test
/**
* Constructor of WavAnalyzer.
*/
public WavAnalyzer(byte[] byteData, int sampleRate, Listener listener) {
this.listener = listener;
this.sampleRate = sampleRate;
short[] shortData = new short[byteData.length >> 1];
ByteBuffer.wrap(byteData).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shortData);
this.data = Util.toDouble(shortData);
for (int i = 0; i < data.length; i++) {
data[i] = data[i] / Short.MAX_VALUE;
}
}
/**
* Do the analysis. Returns true if passing, false if failing.
*/
public boolean doWork() {
if (isClipped()) {
return false;
}
// Calculating the pip strength.
listener.sendMessage("Calculating... Please wait...\n");
try {
dB = measurePipStrength();
} catch (IndexOutOfBoundsException e) {
listener.sendMessage("WARNING: May have missed the prefix."
+ " Turn up the volume of the playback device or move to a quieter location.\n");
return false;
}
if (!isConsistent()) {
return false;
}
result = responsePassesHifiTest(dB);
return result;
}
/**
* Check if the recording is clipped.
*/
boolean isClipped() {
for (int i = 1; i < data.length; i++) {
if ((Math.abs(data[i]) >= Short.MAX_VALUE) && (Math.abs(data[i - 1]) >= Short.MAX_VALUE)) {
listener.sendMessage("WARNING: Data is clipped."
+ " Turn down the volume of the playback device and redo the procedure.\n");
return true;
}
}
return false;
}
/**
* Check if the result is consistant across trials.
*/
boolean isConsistent() {
double[] coeffOfVar = new double[Common.PIP_NUM];
for (int i = 0; i < Common.PIP_NUM; i++) {
double[] powerAtFreq = new double[Common.REPETITIONS];
for (int j = 0; j < Common.REPETITIONS; j++) {
powerAtFreq[j] = power[i][j];
}
coeffOfVar[i] = Util.std(powerAtFreq) / Util.mean(powerAtFreq);
}
if (Util.mean(coeffOfVar) > 1.0) {
listener.sendMessage("WARNING: Inconsistent result across trials."
+ " Turn up the volume of the playback device or move to a quieter location.\n");
return false;
}
return true;
}
/**
* Determine test pass/fail using the frequency response. Package visible for unit testing.
*/
boolean responsePassesHifiTest(double[] dB) {
for (int i = 0; i < dB.length; i++) {
// Precautionary; NaN should not happen.
if (Double.isNaN(dB[i])) {
listener.sendMessage(
"WARNING: Unexpected NaN in result. Redo the test.\n");
return false;
}
}
if (Util.mean(dB) - Util.mean(noiseDB) < Common.SIGNAL_MIN_STRENGTH_DB_ABOVE_NOISE) {
listener.sendMessage("WARNING: Signal is too weak or background noise is too strong."
+ " Turn up the volume of the playback device or move to a quieter location.\n");
return false;
}
int indexOf2000Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 2000.0);
threshold = dB[indexOf2000Hz] + Common.PASSING_THRESHOLD_DB;
int indexOf18500Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 18500.0);
int indexOf20000Hz = Util.findClosest(Common.FREQUENCIES_ORIGINAL, 20000.0);
double[] responseInRange = new double[indexOf20000Hz - indexOf18500Hz];
System.arraycopy(dB, indexOf18500Hz, responseInRange, 0, responseInRange.length);
if (Util.mean(responseInRange) < threshold) {
listener.sendMessage(
"WARNING: Failed. Retry with different orientations or report failed.\n");
return false;
}
return true;
}
/**
* Calculate the Fourier Coefficient at the pip frequency to calculate the frequency response.
* Package visible for unit testing.
*/
double[] measurePipStrength() {
listener.sendMessage("Aligning data... Please wait...\n");
final int dataStartI = alignData();
final int prefixTotalLength = dataStartI
+ Util.toLength(Common.PREFIX_LENGTH_S + Common.PAUSE_AFTER_PREFIX_DURATION_S, sampleRate);
listener.sendMessage("Done.\n");
listener.sendMessage("Prefix starts at " + (double) dataStartI / sampleRate + " s \n");
if (dataStartI > Math.round(sampleRate * (Common.PREFIX_LENGTH_S
+ Common.PAUSE_BEFORE_PREFIX_DURATION_S + Common.PAUSE_AFTER_PREFIX_DURATION_S))) {
listener.sendMessage("WARNING: Unexpected prefix start time. May have missed the prefix.\n"
+ "PLAY button should be pressed on the playback device within one second"
+ " after RECORD is pressed on the recording device.\n"
+ "If this happens repeatedly,"
+ " turn up the volume of the playback device or move to a quieter location.\n");
}
listener.sendMessage("Analyzing noise strength... Please wait...\n");
noisePower = new double[Common.PIP_NUM][Common.NOISE_SAMPLES];
noiseDB = new double[Common.PIP_NUM];
for (int s = 0; s < Common.NOISE_SAMPLES; s++) {
double[] noisePoints = new double[Common.WINDOW_FOR_RECORDER.length];
System.arraycopy(data, dataStartI - (s + 1) * noisePoints.length - 1,
noisePoints, 0, noisePoints.length);
for (int j = 0; j < noisePoints.length; j++) {
noisePoints[j] = noisePoints[j] * Common.WINDOW_FOR_RECORDER[j];
}
for (int i = 0; i < Common.PIP_NUM; i++) {
double freq = Common.FREQUENCIES_ORIGINAL[i];
Complex fourierCoeff = new Complex(0, 0);
final Complex rotator = new Complex(0,
-2.0 * Math.PI * freq / sampleRate).exp();
Complex phasor = new Complex(1, 0);
for (int j = 0; j < noisePoints.length; j++) {
fourierCoeff = fourierCoeff.add(phasor.multiply(noisePoints[j]));
phasor = phasor.multiply(rotator);
}
fourierCoeff = fourierCoeff.multiply(1.0 / noisePoints.length);
noisePower[i][s] = fourierCoeff.multiply(fourierCoeff.conjugate()).abs();
}
}
for (int i = 0; i < Common.PIP_NUM; i++) {
double meanNoisePower = 0;
for (int j = 0; j < Common.NOISE_SAMPLES; j++) {
meanNoisePower += noisePower[i][j];
}
meanNoisePower /= Common.NOISE_SAMPLES;
noiseDB[i] = 10 * Math.log10(meanNoisePower);
}
listener.sendMessage("Analyzing pips... Please wait...\n");
power = new double[Common.PIP_NUM][Common.REPETITIONS];
for (int i = 0; i < Common.PIP_NUM * Common.REPETITIONS; i++) {
if (i % Common.PIP_NUM == 0) {
listener.sendMessage("#" + (i / Common.PIP_NUM + 1) + "\n");
}
int pipExpectedStartI;
pipExpectedStartI = prefixTotalLength
+ Util.toLength(i * (Common.PIP_DURATION_S + Common.PAUSE_DURATION_S), sampleRate);
// Cut out the data points for the current pip.
double[] pipPoints = new double[Common.WINDOW_FOR_RECORDER.length];
System.arraycopy(data, pipExpectedStartI, pipPoints, 0, pipPoints.length);
for (int j = 0; j < Common.WINDOW_FOR_RECORDER.length; j++) {
pipPoints[j] = pipPoints[j] * Common.WINDOW_FOR_RECORDER[j];
}
Complex fourierCoeff = new Complex(0, 0);
final Complex rotator = new Complex(0,
-2.0 * Math.PI * Common.FREQUENCIES[i] / sampleRate).exp();
Complex phasor = new Complex(1, 0);
for (int j = 0; j < pipPoints.length; j++) {
fourierCoeff = fourierCoeff.add(phasor.multiply(pipPoints[j]));
phasor = phasor.multiply(rotator);
}
fourierCoeff = fourierCoeff.multiply(1.0 / pipPoints.length);
int j = Common.ORDER[i];
power[j % Common.PIP_NUM][j / Common.PIP_NUM] =
fourierCoeff.multiply(fourierCoeff.conjugate()).abs();
}
// Calculate median of trials.
double[] dB = new double[Common.PIP_NUM];
for (int i = 0; i < Common.PIP_NUM; i++) {
dB[i] = 10 * Math.log10(Util.median(power[i]));
}
return dB;
}
/**
* Align data using prefix. Package visible for unit testing.
*/
int alignData() {
// Zeropadding samples to add in the correlation to avoid FFT wraparound.
final int zeroPad = Util.toLength(Common.PREFIX_LENGTH_S, Common.RECORDING_SAMPLE_RATE_HZ) - 1;
int fftSize = Util.nextPowerOfTwo((int) Math.round(sampleRate * (Common.PREFIX_LENGTH_S
+ Common.PAUSE_BEFORE_PREFIX_DURATION_S + Common.PAUSE_AFTER_PREFIX_DURATION_S + 0.5))
+ zeroPad);
double[] dataCut = new double[fftSize - zeroPad];
System.arraycopy(data, 0, dataCut, 0, fftSize - zeroPad);
double[] xCorrDataPrefix = Util.computeCrossCorrelation(
Util.padZeros(Util.toComplex(dataCut), fftSize),
Util.padZeros(Util.toComplex(Common.PREFIX_FOR_RECORDER), fftSize));
return Util.findMaxIndex(xCorrDataPrefix);
}
double[] getDB() {
return dB;
}
double[][] getPower() {
return power;
}
double[] getNoiseDB() {
return noiseDB;
}
double getThreshold() {
return threshold;
}
boolean getResult() {
return result;
}
/**
* An interface for listening a message publishing the progress of the analyzer.
*/
public interface Listener {
void sendMessage(String message);
}
}