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); } }