blob: 6fdccb4040e236ba351506afb7ad0ccda69e437d [file] [log] [blame]
/*
* Copyright (C) 2021 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.android.cts.verifier.audio.wavlib;
import com.android.cts.verifier.audio.audiolib.AudioCommon;
import com.android.cts.verifier.audio.Util;
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.
*/
public 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.
*/
public boolean isConsistent() {
double[] coeffOfVar = new double[AudioCommon.PIP_NUM];
for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
double[] powerAtFreq = new double[AudioCommon.REPETITIONS];
for (int j = 0; j < AudioCommon.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.
*/
public 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) < AudioCommon.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(AudioCommon.FREQUENCIES_ORIGINAL, 2000.0);
threshold = dB[indexOf2000Hz] + AudioCommon.PASSING_THRESHOLD_DB;
int indexOf18500Hz = Util.findClosest(AudioCommon.FREQUENCIES_ORIGINAL, 18500.0);
int indexOf20000Hz = Util.findClosest(AudioCommon.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.
*/
public double[] measurePipStrength() {
listener.sendMessage("Aligning data... Please wait...\n");
final int dataStartI = alignData();
final int prefixTotalLength = dataStartI
+ Util.toLength(AudioCommon.PREFIX_LENGTH_S + AudioCommon.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 * (AudioCommon.PREFIX_LENGTH_S
+ AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S + AudioCommon.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[AudioCommon.PIP_NUM][AudioCommon.NOISE_SAMPLES];
noiseDB = new double[AudioCommon.PIP_NUM];
for (int s = 0; s < AudioCommon.NOISE_SAMPLES; s++) {
double[] noisePoints = new double[AudioCommon.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] * AudioCommon.WINDOW_FOR_RECORDER[j];
}
for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
double freq = AudioCommon.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 < AudioCommon.PIP_NUM; i++) {
double meanNoisePower = 0;
for (int j = 0; j < AudioCommon.NOISE_SAMPLES; j++) {
meanNoisePower += noisePower[i][j];
}
meanNoisePower /= AudioCommon.NOISE_SAMPLES;
noiseDB[i] = 10 * Math.log10(meanNoisePower);
}
listener.sendMessage("Analyzing pips... Please wait...\n");
power = new double[AudioCommon.PIP_NUM][AudioCommon.REPETITIONS];
for (int i = 0; i < AudioCommon.PIP_NUM * AudioCommon.REPETITIONS; i++) {
if (i % AudioCommon.PIP_NUM == 0) {
listener.sendMessage("#" + (i / AudioCommon.PIP_NUM + 1) + "\n");
}
int pipExpectedStartI;
pipExpectedStartI = prefixTotalLength
+ Util.toLength(i * (AudioCommon.PIP_DURATION_S + AudioCommon.PAUSE_DURATION_S), sampleRate);
// Cut out the data points for the current pip.
double[] pipPoints = new double[AudioCommon.WINDOW_FOR_RECORDER.length];
System.arraycopy(data, pipExpectedStartI, pipPoints, 0, pipPoints.length);
for (int j = 0; j < AudioCommon.WINDOW_FOR_RECORDER.length; j++) {
pipPoints[j] = pipPoints[j] * AudioCommon.WINDOW_FOR_RECORDER[j];
}
Complex fourierCoeff = new Complex(0, 0);
final Complex rotator = new Complex(0,
-2.0 * Math.PI * AudioCommon.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 = AudioCommon.ORDER[i];
power[j % AudioCommon.PIP_NUM][j / AudioCommon.PIP_NUM] =
fourierCoeff.multiply(fourierCoeff.conjugate()).abs();
}
// Calculate median of trials.
double[] dB = new double[AudioCommon.PIP_NUM];
for (int i = 0; i < AudioCommon.PIP_NUM; i++) {
dB[i] = 10 * Math.log10(Util.median(power[i]));
}
return dB;
}
/**
* Align data using prefix. Package visible for unit testing.
*/
public int alignData() {
// Zeropadding samples to add in the correlation to avoid FFT wraparound.
final int zeroPad =
Util.toLength(AudioCommon.PREFIX_LENGTH_S, AudioCommon.RECORDING_SAMPLE_RATE_HZ) - 1;
int fftSize = Util.nextPowerOfTwo((int) Math.round(sampleRate * (AudioCommon.PREFIX_LENGTH_S
+ AudioCommon.PAUSE_BEFORE_PREFIX_DURATION_S
+ AudioCommon.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(AudioCommon.PREFIX_FOR_RECORDER), fftSize));
return Util.findMaxIndex(xCorrDataPrefix);
}
public double[] getDB() {
return dB;
}
public double[][] getPower() {
return power;
}
public double[] getNoiseDB() {
return noiseDB;
}
public double getThreshold() {
return threshold;
}
public boolean getResult() {
return result;
}
/**
* An interface for listening a message publishing the progress of the analyzer.
*/
public interface Listener {
void sendMessage(String message);
}
}