aaudio_loopback: port latency analyzer from OboeTester
Integrate with aaudio_loopback.
Eventually this could be used in the CTS Verifier for latency testing.
Pass std:string back from analyze().
General cleanup.
Test: adb shell aaudio_loopback
Test: check the latency.msec value
Change-Id: Ibf7128d78d47d91e0bf314344ca450d7f70b3ceb
diff --git a/media/libaaudio/examples/loopback/src/LoopbackAnalyzer.h b/media/libaaudio/examples/loopback/src/LoopbackAnalyzer.h
deleted file mode 100644
index 8eb70b1..0000000
--- a/media/libaaudio/examples/loopback/src/LoopbackAnalyzer.h
+++ /dev/null
@@ -1,1114 +0,0 @@
-/*
- * Copyright (C) 2017 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.
- */
-
-/**
- * Tools for measuring latency and for detecting glitches.
- * These classes are pure math and can be used with any audio system.
- */
-
-#ifndef AAUDIO_EXAMPLES_LOOPBACK_ANALYSER_H
-#define AAUDIO_EXAMPLES_LOOPBACK_ANALYSER_H
-
-#include <algorithm>
-#include <assert.h>
-#include <cctype>
-#include <math.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <unistd.h>
-
-#include <audio_utils/sndfile.h>
-
-// Tag for machine readable results as property = value pairs
-#define LOOPBACK_RESULT_TAG "RESULT: "
-
-constexpr int32_t kDefaultSampleRate = 48000;
-constexpr int32_t kMillisPerSecond = 1000;
-constexpr int32_t kMinLatencyMillis = 4; // arbitrary and very low
-constexpr int32_t kMaxLatencyMillis = 400; // arbitrary and generous
-constexpr double kMaxEchoGain = 10.0; // based on experiments, otherwise too noisy
-constexpr double kMinimumConfidence = 0.5;
-
-static void printAudioScope(float sample) {
- const int maxStars = 80; // arbitrary, fits on one line
- char c = '*';
- if (sample < -1.0) {
- sample = -1.0;
- c = '$';
- } else if (sample > 1.0) {
- sample = 1.0;
- c = '$';
- }
- int numSpaces = (int) (((sample + 1.0) * 0.5) * maxStars);
- for (int i = 0; i < numSpaces; i++) {
- putchar(' ');
- }
- printf("%c\n", c);
-}
-
-/*
-
-FIR filter designed with
-http://t-filter.appspot.com
-
-sampling frequency: 48000 Hz
-
-* 0 Hz - 8000 Hz
- gain = 1.2
- desired ripple = 5 dB
- actual ripple = 5.595266169703693 dB
-
-* 12000 Hz - 20000 Hz
- gain = 0
- desired attenuation = -40 dB
- actual attenuation = -37.58691566571914 dB
-
-*/
-
-#define FILTER_TAP_NUM 11
-
-static const float sFilterTaps8000[FILTER_TAP_NUM] = {
- -0.05944219353343189f,
- -0.07303434839503208f,
- -0.037690487672689066f,
- 0.1870480506596512f,
- 0.3910337357836833f,
- 0.5333672385425637f,
- 0.3910337357836833f,
- 0.1870480506596512f,
- -0.037690487672689066f,
- -0.07303434839503208f,
- -0.05944219353343189f
-};
-
-class LowPassFilter {
-public:
-
- /*
- * Filter one input sample.
- * @return filtered output
- */
- float filter(float input) {
- float output = 0.0f;
- mX[mCursor] = input;
- // Index backwards over x.
- int xIndex = mCursor + FILTER_TAP_NUM;
- // Write twice so we avoid having to wrap in the middle of the convolution.
- mX[xIndex] = input;
- for (int i = 0; i < FILTER_TAP_NUM; i++) {
- output += sFilterTaps8000[i] * mX[xIndex--];
- }
- if (++mCursor >= FILTER_TAP_NUM) {
- mCursor = 0;
- }
- return output;
- }
-
- /**
- * @return true if PASSED
- */
- bool test() {
- // Measure the impulse of the filter at different phases so we exercise
- // all the wraparound cases in the FIR.
- for (int offset = 0; offset < (FILTER_TAP_NUM * 2); offset++ ) {
- // printf("LowPassFilter: cursor = %d\n", mCursor);
- // Offset by one each time.
- if (filter(0.0f) != 0.0f) {
- printf("ERROR: filter should return 0.0 before impulse response\n");
- return false;
- }
- for (int i = 0; i < FILTER_TAP_NUM; i++) {
- float output = filter((i == 0) ? 1.0f : 0.0f); // impulse
- if (output != sFilterTaps8000[i]) {
- printf("ERROR: filter should return impulse response\n");
- return false;
- }
- }
- for (int i = 0; i < FILTER_TAP_NUM; i++) {
- if (filter(0.0f) != 0.0f) {
- printf("ERROR: filter should return 0.0 after impulse response\n");
- return false;
- }
- }
- }
- return true;
- }
-
-private:
- float mX[FILTER_TAP_NUM * 2]{}; // twice as big as needed to avoid wrapping
- int32_t mCursor = 0;
-};
-
-// A narrow impulse seems to have better immunity against over estimating the
-// latency due to detecting subharmonics by the auto-correlator.
-static const float s_Impulse[] = {
- 0.0f, 0.0f, 0.0f, 0.0f, 0.3f, // silence on each side of the impulse
- 0.99f, 0.0f, -0.99f, // bipolar with one zero crossing in middle
- -0.3f, 0.0f, 0.0f, 0.0f, 0.0f
-};
-
-constexpr int32_t kImpulseSizeInFrames = (int32_t)(sizeof(s_Impulse) / sizeof(s_Impulse[0]));
-
-class PseudoRandom {
-public:
- PseudoRandom() {}
- PseudoRandom(int64_t seed)
- : mSeed(seed)
- {}
-
- /**
- * Returns the next random double from -1.0 to 1.0
- *
- * @return value from -1.0 to 1.0
- */
- double nextRandomDouble() {
- return nextRandomInteger() * (0.5 / (((int32_t)1) << 30));
- }
-
- /** Calculate random 32 bit number using linear-congruential method. */
- int32_t nextRandomInteger() {
- // Use values for 64-bit sequence from MMIX by Donald Knuth.
- mSeed = (mSeed * (int64_t)6364136223846793005) + (int64_t)1442695040888963407;
- return (int32_t) (mSeed >> 32); // The higher bits have a longer sequence.
- }
-
-private:
- int64_t mSeed = 99887766;
-};
-
-
-typedef struct LatencyReport_s {
- double latencyInFrames;
- double confidence;
-} LatencyReport;
-
-static double calculateCorrelation(const float *a,
- const float *b,
- int windowSize)
-{
- double correlation = 0.0;
- double sumProducts = 0.0;
- double sumSquares = 0.0;
-
- // Correlate a against b.
- for (int i = 0; i < windowSize; i++) {
- float s1 = a[i];
- float s2 = b[i];
- // Use a normalized cross-correlation.
- sumProducts += s1 * s2;
- sumSquares += ((s1 * s1) + (s2 * s2));
- }
-
- if (sumSquares >= 0.00000001) {
- correlation = (float) (2.0 * sumProducts / sumSquares);
- }
- return correlation;
-}
-
-static int measureLatencyFromEchos(const float *data,
- int32_t numFloats,
- int32_t sampleRate,
- LatencyReport *report) {
- // Allocate results array
- const int minReasonableLatencyFrames = sampleRate * kMinLatencyMillis / kMillisPerSecond;
- const int maxReasonableLatencyFrames = sampleRate * kMaxLatencyMillis / kMillisPerSecond;
- int32_t maxCorrelationSize = maxReasonableLatencyFrames * 3;
- int numCorrelations = std::min(numFloats, maxCorrelationSize);
- float *correlations = new float[numCorrelations]{};
- float *harmonicSums = new float[numCorrelations]{};
-
- // Perform sliding auto-correlation.
- // Skip first frames to avoid huge peak at zero offset.
- for (int i = minReasonableLatencyFrames; i < numCorrelations; i++) {
- int32_t remaining = numFloats - i;
- float correlation = (float) calculateCorrelation(&data[i], data, remaining);
- correlations[i] = correlation;
- // printf("correlation[%d] = %f\n", ic, correlation);
- }
-
- // Apply a technique similar to Harmonic Product Spectrum Analysis to find echo fundamental.
- // Add higher harmonics mapped onto lower harmonics. This reinforces the "fundamental" echo.
- const int numEchoes = 8;
- for (int partial = 1; partial < numEchoes; partial++) {
- for (int i = minReasonableLatencyFrames; i < numCorrelations; i++) {
- harmonicSums[i / partial] += correlations[i] / partial;
- }
- }
-
- // Find highest peak in correlation array.
- float maxCorrelation = 0.0;
- int peakIndex = 0;
- for (int i = 0; i < numCorrelations; i++) {
- if (harmonicSums[i] > maxCorrelation) {
- maxCorrelation = harmonicSums[i];
- peakIndex = i;
- // printf("maxCorrelation = %f at %d\n", maxCorrelation, peakIndex);
- }
- }
- report->latencyInFrames = peakIndex;
-/*
- {
- int32_t topPeak = peakIndex * 7 / 2;
- for (int i = 0; i < topPeak; i++) {
- float sample = harmonicSums[i];
- printf("%4d: %7.5f ", i, sample);
- printAudioScope(sample);
- }
- }
-*/
-
- // Calculate confidence.
- if (maxCorrelation < 0.001) {
- report->confidence = 0.0;
- } else {
- // Compare peak to average value around peak.
- int32_t numSamples = std::min(numCorrelations, peakIndex * 2);
- if (numSamples <= 0) {
- report->confidence = 0.0;
- } else {
- double sum = 0.0;
- for (int i = 0; i < numSamples; i++) {
- sum += harmonicSums[i];
- }
- const double average = sum / numSamples;
- const double ratio = average / maxCorrelation; // will be < 1.0
- report->confidence = 1.0 - sqrt(ratio);
- }
- }
-
- delete[] correlations;
- delete[] harmonicSums;
- return 0;
-}
-
-class AudioRecording
-{
-public:
- AudioRecording() {
- }
- ~AudioRecording() {
- delete[] mData;
- }
-
- void allocate(int maxFrames) {
- delete[] mData;
- mData = new float[maxFrames];
- mMaxFrames = maxFrames;
- }
-
- // Write SHORT data from the first channel.
- int32_t write(int16_t *inputData, int32_t inputChannelCount, int32_t numFrames) {
- // stop at end of buffer
- if ((mFrameCounter + numFrames) > mMaxFrames) {
- numFrames = mMaxFrames - mFrameCounter;
- }
- for (int i = 0; i < numFrames; i++) {
- mData[mFrameCounter++] = inputData[i * inputChannelCount] * (1.0f / 32768);
- }
- return numFrames;
- }
-
- // Write FLOAT data from the first channel.
- int32_t write(float *inputData, int32_t inputChannelCount, int32_t numFrames) {
- // stop at end of buffer
- if ((mFrameCounter + numFrames) > mMaxFrames) {
- numFrames = mMaxFrames - mFrameCounter;
- }
- for (int i = 0; i < numFrames; i++) {
- mData[mFrameCounter++] = inputData[i * inputChannelCount];
- }
- return numFrames;
- }
-
- int32_t size() {
- return mFrameCounter;
- }
-
- float *getData() {
- return mData;
- }
-
- void setSampleRate(int32_t sampleRate) {
- mSampleRate = sampleRate;
- }
-
- int32_t getSampleRate() {
- return mSampleRate;
- }
-
- int save(const char *fileName, bool writeShorts = true) {
- SNDFILE *sndFile = nullptr;
- int written = 0;
- SF_INFO info = {
- .frames = mFrameCounter,
- .samplerate = mSampleRate,
- .channels = 1,
- .format = SF_FORMAT_WAV | (writeShorts ? SF_FORMAT_PCM_16 : SF_FORMAT_FLOAT)
- };
-
- sndFile = sf_open(fileName, SFM_WRITE, &info);
- if (sndFile == nullptr) {
- printf("AudioRecording::save(%s) failed to open file\n", fileName);
- return -errno;
- }
-
- written = sf_writef_float(sndFile, mData, mFrameCounter);
-
- sf_close(sndFile);
- return written;
- }
-
- int load(const char *fileName) {
- SNDFILE *sndFile = nullptr;
- SF_INFO info;
-
- sndFile = sf_open(fileName, SFM_READ, &info);
- if (sndFile == nullptr) {
- printf("AudioRecording::load(%s) failed to open file\n", fileName);
- return -errno;
- }
-
- assert(info.channels == 1);
- assert(info.format == SF_FORMAT_FLOAT);
-
- setSampleRate(info.samplerate);
- allocate(info.frames);
- mFrameCounter = sf_readf_float(sndFile, mData, info.frames);
-
- sf_close(sndFile);
- return mFrameCounter;
- }
-
- /**
- * Square the samples so they are all positive and so the peaks are emphasized.
- */
- void square() {
- for (int i = 0; i < mFrameCounter; i++) {
- const float sample = mData[i];
- mData[i] = sample * sample;
- }
- }
-
- /**
- * Low pass filter the recording using a simple FIR filter.
- * Note that the lowpass filter cutoff tracks the sample rate.
- * That is OK because the impulse width is a fixed number of samples.
- */
- void lowPassFilter() {
- for (int i = 0; i < mFrameCounter; i++) {
- mData[i] = mLowPassFilter.filter(mData[i]);
- }
- }
-
- /**
- * Remove DC offset using a one-pole one-zero IIR filter.
- */
- void dcBlocker() {
- const float R = 0.996; // narrow notch at zero Hz
- float x1 = 0.0;
- float y1 = 0.0;
- for (int i = 0; i < mFrameCounter; i++) {
- const float x = mData[i];
- const float y = x - x1 + (R * y1);
- mData[i] = y;
- y1 = y;
- x1 = x;
- }
- }
-
-private:
- float *mData = nullptr;
- int32_t mFrameCounter = 0;
- int32_t mMaxFrames = 0;
- int32_t mSampleRate = kDefaultSampleRate; // common default
- LowPassFilter mLowPassFilter;
-};
-
-// ====================================================================================
-class LoopbackProcessor {
-public:
- virtual ~LoopbackProcessor() = default;
-
-
- enum process_result {
- PROCESS_RESULT_OK,
- PROCESS_RESULT_GLITCH
- };
-
- virtual void reset() {}
-
- virtual process_result process(float *inputData, int inputChannelCount,
- float *outputData, int outputChannelCount,
- int numFrames) = 0;
-
-
- virtual void report() = 0;
-
- virtual void printStatus() {};
-
- int32_t getResult() {
- return mResult;
- }
-
- void setResult(int32_t result) {
- mResult = result;
- }
-
- virtual bool isDone() {
- return false;
- }
-
- virtual int save(const char *fileName) {
- (void) fileName;
- return AAUDIO_ERROR_UNIMPLEMENTED;
- }
-
- virtual int load(const char *fileName) {
- (void) fileName;
- return AAUDIO_ERROR_UNIMPLEMENTED;
- }
-
- virtual void setSampleRate(int32_t sampleRate) {
- mSampleRate = sampleRate;
- }
-
- int32_t getSampleRate() {
- return mSampleRate;
- }
-
- // Measure peak amplitude of buffer.
- static float measurePeakAmplitude(float *inputData, int inputChannelCount, int numFrames) {
- float peak = 0.0f;
- for (int i = 0; i < numFrames; i++) {
- const float pos = fabs(*inputData);
- if (pos > peak) {
- peak = pos;
- }
- inputData += inputChannelCount;
- }
- return peak;
- }
-
-
-private:
- int32_t mSampleRate = kDefaultSampleRate;
- int32_t mResult = 0;
-};
-
-class PeakDetector {
-public:
- float process(float input) {
- float output = mPrevious * mDecay;
- if (input > output) {
- output = input;
- }
- mPrevious = output;
- return output;
- }
-
-private:
- float mDecay = 0.99f;
- float mPrevious = 0.0f;
-};
-
-// ====================================================================================
-/**
- * Measure latency given a loopback stream data.
- * Uses a state machine to cycle through various stages including:
- *
- */
-class EchoAnalyzer : public LoopbackProcessor {
-public:
-
- EchoAnalyzer() : LoopbackProcessor() {
- mAudioRecording.allocate(2 * getSampleRate());
- mAudioRecording.setSampleRate(getSampleRate());
- }
-
- void setSampleRate(int32_t sampleRate) override {
- LoopbackProcessor::setSampleRate(sampleRate);
- mAudioRecording.setSampleRate(sampleRate);
- }
-
- void reset() override {
- mDownCounter = getSampleRate() / 2;
- mLoopCounter = 0;
- mMeasuredLoopGain = 0.0f;
- mEchoGain = 1.0f;
- mState = STATE_INITIAL_SILENCE;
- }
-
- virtual bool isDone() {
- return mState == STATE_DONE || mState == STATE_FAILED;
- }
-
- void setGain(float gain) {
- mEchoGain = gain;
- }
-
- float getGain() {
- return mEchoGain;
- }
-
- bool testLowPassFilter() {
- LowPassFilter filter;
- return filter.test();
- }
-
- void report() override {
- printf("EchoAnalyzer ---------------\n");
- if (getResult() != 0) {
- printf(LOOPBACK_RESULT_TAG "result = %d\n", getResult());
- return;
- }
-
- // printf("LowPassFilter test %s\n", testLowPassFilter() ? "PASSED" : "FAILED");
-
- printf(LOOPBACK_RESULT_TAG "measured.gain = %8f\n", mMeasuredLoopGain);
- printf(LOOPBACK_RESULT_TAG "echo.gain = %8f\n", mEchoGain);
- printf(LOOPBACK_RESULT_TAG "test.state = %8d\n", mState);
- printf(LOOPBACK_RESULT_TAG "test.state.name = %8s\n", convertStateToText(mState));
-
- if (mState == STATE_WAITING_FOR_SILENCE) {
- printf("WARNING - Stuck waiting for silence. Input may be too noisy!\n");
- setResult(ERROR_NOISY);
- } else if (mMeasuredLoopGain >= 0.9999) {
- printf(" ERROR - clipping, turn down volume slightly\n");
- setResult(ERROR_CLIPPING);
- } else if (mState != STATE_DONE && mState != STATE_GATHERING_ECHOS) {
- printf("WARNING - Bad state. Check volume on device.\n");
- setResult(ERROR_INVALID_STATE);
- } else {
- // Cleanup the signal to improve the auto-correlation.
- mAudioRecording.dcBlocker();
- mAudioRecording.square();
- mAudioRecording.lowPassFilter();
-
- printf("Please wait several seconds for auto-correlation to complete.\n");
- measureLatencyFromEchos(mAudioRecording.getData(),
- mAudioRecording.size(),
- getSampleRate(),
- &mLatencyReport);
-
- double latencyMillis = kMillisPerSecond * (double) mLatencyReport.latencyInFrames
- / getSampleRate();
- printf(LOOPBACK_RESULT_TAG "latency.frames = %8.2f\n",
- mLatencyReport.latencyInFrames);
- printf(LOOPBACK_RESULT_TAG "latency.msec = %8.2f\n",
- latencyMillis);
- printf(LOOPBACK_RESULT_TAG "latency.confidence = %8.6f\n",
- mLatencyReport.confidence);
- if (mLatencyReport.confidence < kMinimumConfidence) {
- printf(" ERROR - confidence too low!\n");
- setResult(ERROR_CONFIDENCE);
- }
- }
- }
-
- void printStatus() override {
- printf("st = %d, echo gain = %f ", mState, mEchoGain);
- }
-
- void sendImpulses(float *outputData, int outputChannelCount, int numFrames) {
- while (numFrames-- > 0) {
- float sample = s_Impulse[mSampleIndex++];
- if (mSampleIndex >= kImpulseSizeInFrames) {
- mSampleIndex = 0;
- }
-
- *outputData = sample;
- outputData += outputChannelCount;
- }
- }
-
- void sendOneImpulse(float *outputData, int outputChannelCount) {
- mSampleIndex = 0;
- sendImpulses(outputData, outputChannelCount, kImpulseSizeInFrames);
- }
-
- // @return number of frames for a typical block of processing
- int32_t getBlockFrames() {
- return getSampleRate() / 8;
- }
-
- process_result process(float *inputData, int inputChannelCount,
- float *outputData, int outputChannelCount,
- int numFrames) override {
- int channelsValid = std::min(inputChannelCount, outputChannelCount);
- float peak = 0.0f;
- int numWritten;
- int numSamples;
-
- echo_state nextState = mState;
-
- switch (mState) {
- case STATE_INITIAL_SILENCE:
- // Output silence at the beginning.
- numSamples = numFrames * outputChannelCount;
- for (int i = 0; i < numSamples; i++) {
- outputData[i] = 0;
- }
- mDownCounter -= numFrames;
- if (mDownCounter <= 0) {
- nextState = STATE_MEASURING_GAIN;
- //printf("%5d: switch to STATE_MEASURING_GAIN\n", mLoopCounter);
- mDownCounter = getBlockFrames() * 2;
- }
- break;
-
- case STATE_MEASURING_GAIN:
- sendImpulses(outputData, outputChannelCount, numFrames);
- peak = measurePeakAmplitude(inputData, inputChannelCount, numFrames);
- // If we get several in a row then go to next state.
- if (peak > mPulseThreshold) {
- mDownCounter -= numFrames;
- if (mDownCounter <= 0) {
- //printf("%5d: switch to STATE_WAITING_FOR_SILENCE, measured peak = %f\n",
- // mLoopCounter, peak);
- mDownCounter = getBlockFrames();
- mMeasuredLoopGain = peak; // assumes original pulse amplitude is one
- mSilenceThreshold = peak * 0.1; // scale silence to measured pulse
- // Calculate gain that will give us a nice decaying echo.
- mEchoGain = mDesiredEchoGain / mMeasuredLoopGain;
- if (mEchoGain > kMaxEchoGain) {
- printf("ERROR - loop gain too low. Increase the volume.\n");
- nextState = STATE_FAILED;
- } else {
- nextState = STATE_WAITING_FOR_SILENCE;
- }
- }
- } else if (numFrames > kImpulseSizeInFrames){ // ignore short callbacks
- mDownCounter = getBlockFrames();
- }
- break;
-
- case STATE_WAITING_FOR_SILENCE:
- // Output silence and wait for the echos to die down.
- numSamples = numFrames * outputChannelCount;
- for (int i = 0; i < numSamples; i++) {
- outputData[i] = 0;
- }
- peak = measurePeakAmplitude(inputData, inputChannelCount, numFrames);
- // If we get several in a row then go to next state.
- if (peak < mSilenceThreshold) {
- mDownCounter -= numFrames;
- if (mDownCounter <= 0) {
- nextState = STATE_SENDING_PULSE;
- //printf("%5d: switch to STATE_SENDING_PULSE\n", mLoopCounter);
- mDownCounter = getBlockFrames();
- }
- } else {
- mDownCounter = getBlockFrames();
- }
- break;
-
- case STATE_SENDING_PULSE:
- mAudioRecording.write(inputData, inputChannelCount, numFrames);
- sendOneImpulse(outputData, outputChannelCount);
- nextState = STATE_GATHERING_ECHOS;
- //printf("%5d: switch to STATE_GATHERING_ECHOS\n", mLoopCounter);
- break;
-
- case STATE_GATHERING_ECHOS:
- numWritten = mAudioRecording.write(inputData, inputChannelCount, numFrames);
- peak = measurePeakAmplitude(inputData, inputChannelCount, numFrames);
- if (peak > mMeasuredLoopGain) {
- mMeasuredLoopGain = peak; // AGC might be raising gain so adjust it on the fly.
- // Recalculate gain that will give us a nice decaying echo.
- mEchoGain = mDesiredEchoGain / mMeasuredLoopGain;
- }
- // Echo input to output.
- for (int i = 0; i < numFrames; i++) {
- int ic;
- for (ic = 0; ic < channelsValid; ic++) {
- outputData[ic] = inputData[ic] * mEchoGain;
- }
- for (; ic < outputChannelCount; ic++) {
- outputData[ic] = 0;
- }
- inputData += inputChannelCount;
- outputData += outputChannelCount;
- }
- if (numWritten < numFrames) {
- nextState = STATE_DONE;
- }
- break;
-
- case STATE_DONE:
- case STATE_FAILED:
- default:
- break;
- }
-
- mState = nextState;
- mLoopCounter++;
- return PROCESS_RESULT_OK;
- }
-
- int save(const char *fileName) override {
- return mAudioRecording.save(fileName);
- }
-
- int load(const char *fileName) override {
- int result = mAudioRecording.load(fileName);
- setSampleRate(mAudioRecording.getSampleRate());
- mState = STATE_DONE;
- return result;
- }
-
-private:
-
- enum error_code {
- ERROR_OK = 0,
- ERROR_NOISY = -99,
- ERROR_CLIPPING,
- ERROR_CONFIDENCE,
- ERROR_INVALID_STATE
- };
-
- enum echo_state {
- STATE_INITIAL_SILENCE,
- STATE_MEASURING_GAIN,
- STATE_WAITING_FOR_SILENCE,
- STATE_SENDING_PULSE,
- STATE_GATHERING_ECHOS,
- STATE_DONE,
- STATE_FAILED
- };
-
- const char *convertStateToText(echo_state state) {
- const char *result = "Unknown";
- switch(state) {
- case STATE_INITIAL_SILENCE:
- result = "INIT";
- break;
- case STATE_MEASURING_GAIN:
- result = "GAIN";
- break;
- case STATE_WAITING_FOR_SILENCE:
- result = "SILENCE";
- break;
- case STATE_SENDING_PULSE:
- result = "PULSE";
- break;
- case STATE_GATHERING_ECHOS:
- result = "ECHOS";
- break;
- case STATE_DONE:
- result = "DONE";
- break;
- case STATE_FAILED:
- result = "FAILED";
- break;
- }
- return result;
- }
-
-
- int32_t mDownCounter = 500;
- int32_t mLoopCounter = 0;
- int32_t mSampleIndex = 0;
- float mPulseThreshold = 0.02f;
- float mSilenceThreshold = 0.002f;
- float mMeasuredLoopGain = 0.0f;
- float mDesiredEchoGain = 0.95f;
- float mEchoGain = 1.0f;
- echo_state mState = STATE_INITIAL_SILENCE;
-
- AudioRecording mAudioRecording; // contains only the input after the gain detection burst
- LatencyReport mLatencyReport;
- // PeakDetector mPeakDetector;
-};
-
-
-// ====================================================================================
-/**
- * Output a steady sinewave and analyze the return signal.
- *
- * Use a cosine transform to measure the predicted magnitude and relative phase of the
- * looped back sine wave. Then generate a predicted signal and compare with the actual signal.
- */
-class SineAnalyzer : public LoopbackProcessor {
-public:
-
- void report() override {
- printf("SineAnalyzer ------------------\n");
- printf(LOOPBACK_RESULT_TAG "peak.amplitude = %8f\n", mPeakAmplitude);
- printf(LOOPBACK_RESULT_TAG "sine.magnitude = %8f\n", mMagnitude);
- printf(LOOPBACK_RESULT_TAG "peak.noise = %8f\n", mPeakNoise);
- printf(LOOPBACK_RESULT_TAG "rms.noise = %8f\n", mRootMeanSquareNoise);
- float amplitudeRatio = mMagnitude / mPeakNoise;
- float signalToNoise = amplitudeRatio * amplitudeRatio;
- printf(LOOPBACK_RESULT_TAG "signal.to.noise = %8.2f\n", signalToNoise);
- float signalToNoiseDB = 10.0 * log(signalToNoise);
- printf(LOOPBACK_RESULT_TAG "signal.to.noise.db = %8.2f\n", signalToNoiseDB);
- if (signalToNoiseDB < MIN_SNRATIO_DB) {
- printf("ERROR - signal to noise ratio is too low! < %d dB. Adjust volume.\n", MIN_SNRATIO_DB);
- setResult(ERROR_NOISY);
- }
- printf(LOOPBACK_RESULT_TAG "frames.accumulated = %8d\n", mFramesAccumulated);
- printf(LOOPBACK_RESULT_TAG "sine.period = %8d\n", mSinePeriod);
- printf(LOOPBACK_RESULT_TAG "test.state = %8d\n", mState);
- printf(LOOPBACK_RESULT_TAG "frame.count = %8d\n", mFrameCounter);
- // Did we ever get a lock?
- bool gotLock = (mState == STATE_LOCKED) || (mGlitchCount > 0);
- if (!gotLock) {
- printf("ERROR - failed to lock on reference sine tone\n");
- setResult(ERROR_NO_LOCK);
- } else {
- // Only print if meaningful.
- printf(LOOPBACK_RESULT_TAG "glitch.count = %8d\n", mGlitchCount);
- printf(LOOPBACK_RESULT_TAG "max.glitch = %8f\n", mMaxGlitchDelta);
- if (mGlitchCount > 0) {
- printf("ERROR - number of glitches > 0\n");
- setResult(ERROR_GLITCHES);
- }
- }
- }
-
- void printStatus() override {
- printf("st = %d, #gl = %3d,", mState, mGlitchCount);
- }
-
- double calculateMagnitude(double *phasePtr = NULL) {
- if (mFramesAccumulated == 0) {
- return 0.0;
- }
- double sinMean = mSinAccumulator / mFramesAccumulated;
- double cosMean = mCosAccumulator / mFramesAccumulated;
- double magnitude = 2.0 * sqrt( (sinMean * sinMean) + (cosMean * cosMean ));
- if( phasePtr != NULL )
- {
- double phase = M_PI_2 - atan2( sinMean, cosMean );
- *phasePtr = phase;
- }
- return magnitude;
- }
-
- /**
- * @param inputData contains microphone data with sine signal feedback
- * @param outputData contains the reference sine wave
- */
- process_result process(float *inputData, int inputChannelCount,
- float *outputData, int outputChannelCount,
- int numFrames) override {
- process_result result = PROCESS_RESULT_OK;
- mProcessCount++;
-
- float peak = measurePeakAmplitude(inputData, inputChannelCount, numFrames);
- if (peak > mPeakAmplitude) {
- mPeakAmplitude = peak;
- }
-
- for (int i = 0; i < numFrames; i++) {
- bool sineEnabled = true;
- float sample = inputData[i * inputChannelCount];
-
- float sinOut = sinf(mPhase);
-
- switch (mState) {
- case STATE_IDLE:
- sineEnabled = false;
- mDownCounter--;
- if (mDownCounter <= 0) {
- mState = STATE_MEASURE_NOISE;
- mDownCounter = NOISE_FRAME_COUNT;
- }
- break;
- case STATE_MEASURE_NOISE:
- sineEnabled = false;
- mPeakNoise = std::max(abs(sample), mPeakNoise);
- mNoiseSumSquared += sample * sample;
- mDownCounter--;
- if (mDownCounter <= 0) {
- mState = STATE_WAITING_FOR_SIGNAL;
- mRootMeanSquareNoise = sqrt(mNoiseSumSquared / NOISE_FRAME_COUNT);
- mTolerance = std::max(MIN_TOLERANCE, mPeakNoise * 2.0f);
- mPhase = 0.0; // prevent spike at start
- }
- break;
-
- case STATE_IMMUNE:
- mDownCounter--;
- if (mDownCounter <= 0) {
- mState = STATE_WAITING_FOR_SIGNAL;
- }
- break;
-
- case STATE_WAITING_FOR_SIGNAL:
- if (peak > mThreshold) {
- mState = STATE_WAITING_FOR_LOCK;
- //printf("%5d: switch to STATE_WAITING_FOR_LOCK\n", mFrameCounter);
- resetAccumulator();
- }
- break;
-
- case STATE_WAITING_FOR_LOCK:
- mSinAccumulator += sample * sinOut;
- mCosAccumulator += sample * cosf(mPhase);
- mFramesAccumulated++;
- // Must be a multiple of the period or the calculation will not be accurate.
- if (mFramesAccumulated == mSinePeriod * PERIODS_NEEDED_FOR_LOCK) {
- mPhaseOffset = 0.0;
- mMagnitude = calculateMagnitude(&mPhaseOffset);
- if (mMagnitude > mThreshold) {
- if (fabs(mPreviousPhaseOffset - mPhaseOffset) < 0.001) {
- mState = STATE_LOCKED;
- //printf("%5d: switch to STATE_LOCKED\n", mFrameCounter);
- }
- mPreviousPhaseOffset = mPhaseOffset;
- }
- resetAccumulator();
- }
- break;
-
- case STATE_LOCKED: {
- // Predict next sine value
- float predicted = sinf(mPhase + mPhaseOffset) * mMagnitude;
- // printf(" predicted = %f, actual = %f\n", predicted, sample);
-
- float diff = predicted - sample;
- float absDiff = fabs(diff);
- mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff);
- if (absDiff > mTolerance) {
- mGlitchCount++;
- result = PROCESS_RESULT_GLITCH;
- //printf("%5d: Got a glitch # %d, predicted = %f, actual = %f\n",
- // mFrameCounter, mGlitchCount, predicted, sample);
- mState = STATE_IMMUNE;
- mDownCounter = mSinePeriod * PERIODS_IMMUNE;
- }
-
- // Track incoming signal and slowly adjust magnitude to account
- // for drift in the DRC or AGC.
- mSinAccumulator += sample * sinOut;
- mCosAccumulator += sample * cosf(mPhase);
- mFramesAccumulated++;
- // Must be a multiple of the period or the calculation will not be accurate.
- if (mFramesAccumulated == mSinePeriod) {
- const double coefficient = 0.1;
- double phaseOffset = 0.0;
- double magnitude = calculateMagnitude(&phaseOffset);
- // One pole averaging filter.
- mMagnitude = (mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient);
- resetAccumulator();
- }
- } break;
- }
-
- float output = 0.0f;
- // Output sine wave so we can measure it.
- if (sineEnabled) {
- output = (sinOut * mOutputAmplitude)
- + (mWhiteNoise.nextRandomDouble() * mNoiseAmplitude);
- // printf("%5d: sin(%f) = %f, %f\n", i, mPhase, sinOut, mPhaseIncrement);
- // advance and wrap phase
- mPhase += mPhaseIncrement;
- if (mPhase > M_PI) {
- mPhase -= (2.0 * M_PI);
- }
- }
- outputData[i * outputChannelCount] = output;
-
-
- mFrameCounter++;
- }
- return result;
- }
-
- void resetAccumulator() {
- mFramesAccumulated = 0;
- mSinAccumulator = 0.0;
- mCosAccumulator = 0.0;
- }
-
- void reset() override {
- mGlitchCount = 0;
- mState = STATE_IDLE;
- mDownCounter = IDLE_FRAME_COUNT;
- mPhaseIncrement = 2.0 * M_PI / mSinePeriod;
- printf("phaseInc = %f for period %d\n", mPhaseIncrement, mSinePeriod);
- resetAccumulator();
- mProcessCount = 0;
- mPeakNoise = 0.0f;
- mNoiseSumSquared = 0.0;
- mRootMeanSquareNoise = 0.0;
- mPhase = 0.0f;
- mMaxGlitchDelta = 0.0;
- }
-
-private:
-
- enum error_code {
- OK,
- ERROR_NO_LOCK = -80,
- ERROR_GLITCHES,
- ERROR_NOISY
- };
-
- enum sine_state_t {
- STATE_IDLE,
- STATE_MEASURE_NOISE,
- STATE_IMMUNE,
- STATE_WAITING_FOR_SIGNAL,
- STATE_WAITING_FOR_LOCK,
- STATE_LOCKED
- };
-
- enum constants {
- // Arbitrary durations, assuming 48000 Hz
- IDLE_FRAME_COUNT = 48 * 100,
- NOISE_FRAME_COUNT = 48 * 600,
- PERIODS_NEEDED_FOR_LOCK = 8,
- PERIODS_IMMUNE = 2,
- MIN_SNRATIO_DB = 65
- };
-
- static constexpr float MIN_TOLERANCE = 0.01;
-
- int mSinePeriod = 79;
- double mPhaseIncrement = 0.0;
- double mPhase = 0.0;
- double mPhaseOffset = 0.0;
- double mPreviousPhaseOffset = 0.0;
- double mMagnitude = 0.0;
- double mThreshold = 0.005;
- double mTolerance = MIN_TOLERANCE;
- int32_t mFramesAccumulated = 0;
- int32_t mProcessCount = 0;
- double mSinAccumulator = 0.0;
- double mCosAccumulator = 0.0;
- float mMaxGlitchDelta = 0.0f;
- int32_t mGlitchCount = 0;
- double mPeakAmplitude = 0.0;
- int mDownCounter = IDLE_FRAME_COUNT;
- int32_t mFrameCounter = 0;
- float mOutputAmplitude = 0.75;
-
- // measure background noise
- float mPeakNoise = 0.0f;
- double mNoiseSumSquared = 0.0;
- double mRootMeanSquareNoise = 0.0;
-
- PseudoRandom mWhiteNoise;
- float mNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC.
-
- sine_state_t mState = STATE_IDLE;
-};
-
-#undef LOOPBACK_RESULT_TAG
-
-#endif /* AAUDIO_EXAMPLES_LOOPBACK_ANALYSER_H */
diff --git a/media/libaaudio/examples/loopback/src/analyzer/GlitchAnalyzer.h b/media/libaaudio/examples/loopback/src/analyzer/GlitchAnalyzer.h
new file mode 100644
index 0000000..04435d1
--- /dev/null
+++ b/media/libaaudio/examples/loopback/src/analyzer/GlitchAnalyzer.h
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+#ifndef ANALYZER_GLITCH_ANALYZER_H
+#define ANALYZER_GLITCH_ANALYZER_H
+
+#include <algorithm>
+#include <cctype>
+#include <iomanip>
+#include <iostream>
+
+#include "LatencyAnalyzer.h"
+#include "PseudoRandom.h"
+
+/**
+ * Output a steady sine wave and analyze the return signal.
+ *
+ * Use a cosine transform to measure the predicted magnitude and relative phase of the
+ * looped back sine wave. Then generate a predicted signal and compare with the actual signal.
+ */
+class GlitchAnalyzer : public LoopbackProcessor {
+public:
+
+ int32_t getState() const {
+ return mState;
+ }
+
+ double getPeakAmplitude() const {
+ return mPeakFollower.getLevel();
+ }
+
+ double getTolerance() {
+ return mTolerance;
+ }
+
+ void setTolerance(double tolerance) {
+ mTolerance = tolerance;
+ mScaledTolerance = mMagnitude * mTolerance;
+ }
+
+ void setMagnitude(double magnitude) {
+ mMagnitude = magnitude;
+ mScaledTolerance = mMagnitude * mTolerance;
+ }
+
+ int32_t getGlitchCount() const {
+ return mGlitchCount;
+ }
+
+ int32_t getStateFrameCount(int state) const {
+ return mStateFrameCounters[state];
+ }
+
+ double getSignalToNoiseDB() {
+ static const double threshold = 1.0e-14;
+ if (mMeanSquareSignal < threshold || mMeanSquareNoise < threshold) {
+ return 0.0;
+ } else {
+ double signalToNoise = mMeanSquareSignal / mMeanSquareNoise; // power ratio
+ double signalToNoiseDB = 10.0 * log(signalToNoise);
+ if (signalToNoiseDB < MIN_SNR_DB) {
+ ALOGD("ERROR - signal to noise ratio is too low! < %d dB. Adjust volume.",
+ MIN_SNR_DB);
+ setResult(ERROR_VOLUME_TOO_LOW);
+ }
+ return signalToNoiseDB;
+ }
+ }
+
+ std::string analyze() override {
+ std::stringstream report;
+ report << "GlitchAnalyzer ------------------\n";
+ report << LOOPBACK_RESULT_TAG "peak.amplitude = " << std::setw(8)
+ << getPeakAmplitude() << "\n";
+ report << LOOPBACK_RESULT_TAG "sine.magnitude = " << std::setw(8)
+ << mMagnitude << "\n";
+ report << LOOPBACK_RESULT_TAG "rms.noise = " << std::setw(8)
+ << mMeanSquareNoise << "\n";
+ report << LOOPBACK_RESULT_TAG "signal.to.noise.db = " << std::setw(8)
+ << getSignalToNoiseDB() << "\n";
+ report << LOOPBACK_RESULT_TAG "frames.accumulated = " << std::setw(8)
+ << mFramesAccumulated << "\n";
+ report << LOOPBACK_RESULT_TAG "sine.period = " << std::setw(8)
+ << mSinePeriod << "\n";
+ report << LOOPBACK_RESULT_TAG "test.state = " << std::setw(8)
+ << mState << "\n";
+ report << LOOPBACK_RESULT_TAG "frame.count = " << std::setw(8)
+ << mFrameCounter << "\n";
+ // Did we ever get a lock?
+ bool gotLock = (mState == STATE_LOCKED) || (mGlitchCount > 0);
+ if (!gotLock) {
+ report << "ERROR - failed to lock on reference sine tone.\n";
+ setResult(ERROR_NO_LOCK);
+ } else {
+ // Only print if meaningful.
+ report << LOOPBACK_RESULT_TAG "glitch.count = " << std::setw(8)
+ << mGlitchCount << "\n";
+ report << LOOPBACK_RESULT_TAG "max.glitch = " << std::setw(8)
+ << mMaxGlitchDelta << "\n";
+ if (mGlitchCount > 0) {
+ report << "ERROR - number of glitches > 0\n";
+ setResult(ERROR_GLITCHES);
+ }
+ }
+ return report.str();
+ }
+
+ void printStatus() override {
+ ALOGD("st = %d, #gl = %3d,", mState, mGlitchCount);
+ }
+ /**
+ * Calculate the magnitude of the component of the input signal
+ * that matches the analysis frequency.
+ * Also calculate the phase that we can use to create a
+ * signal that matches that component.
+ * The phase will be between -PI and +PI.
+ */
+ double calculateMagnitude(double *phasePtr = nullptr) {
+ if (mFramesAccumulated == 0) {
+ return 0.0;
+ }
+ double sinMean = mSinAccumulator / mFramesAccumulated;
+ double cosMean = mCosAccumulator / mFramesAccumulated;
+ double magnitude = 2.0 * sqrt((sinMean * sinMean) + (cosMean * cosMean));
+ if (phasePtr != nullptr) {
+ double phase = M_PI_2 - atan2(sinMean, cosMean);
+ *phasePtr = phase;
+ }
+ return magnitude;
+ }
+
+ /**
+ * @param frameData contains microphone data with sine signal feedback
+ * @param channelCount
+ */
+ result_code processInputFrame(float *frameData, int /* channelCount */) override {
+ result_code result = RESULT_OK;
+
+ float sample = frameData[0];
+ float peak = mPeakFollower.process(sample);
+
+ // Force a periodic glitch to test the detector!
+ if (mForceGlitchDuration > 0) {
+ if (mForceGlitchCounter == 0) {
+ ALOGE("%s: force a glitch!!", __func__);
+ mForceGlitchCounter = getSampleRate();
+ } else if (mForceGlitchCounter <= mForceGlitchDuration) {
+ // Force an abrupt offset.
+ sample += (sample > 0.0) ? -0.5f : 0.5f;
+ }
+ --mForceGlitchCounter;
+ }
+
+ mStateFrameCounters[mState]++; // count how many frames we are in each state
+
+ switch (mState) {
+ case STATE_IDLE:
+ mDownCounter--;
+ if (mDownCounter <= 0) {
+ mState = STATE_IMMUNE;
+ mDownCounter = IMMUNE_FRAME_COUNT;
+ mInputPhase = 0.0; // prevent spike at start
+ mOutputPhase = 0.0;
+ }
+ break;
+
+ case STATE_IMMUNE:
+ mDownCounter--;
+ if (mDownCounter <= 0) {
+ mState = STATE_WAITING_FOR_SIGNAL;
+ }
+ break;
+
+ case STATE_WAITING_FOR_SIGNAL:
+ if (peak > mThreshold) {
+ mState = STATE_WAITING_FOR_LOCK;
+ //ALOGD("%5d: switch to STATE_WAITING_FOR_LOCK", mFrameCounter);
+ resetAccumulator();
+ }
+ break;
+
+ case STATE_WAITING_FOR_LOCK:
+ mSinAccumulator += sample * sinf(mInputPhase);
+ mCosAccumulator += sample * cosf(mInputPhase);
+ mFramesAccumulated++;
+ // Must be a multiple of the period or the calculation will not be accurate.
+ if (mFramesAccumulated == mSinePeriod * PERIODS_NEEDED_FOR_LOCK) {
+ double phaseOffset = 0.0;
+ setMagnitude(calculateMagnitude(&phaseOffset));
+// ALOGD("%s() mag = %f, offset = %f, prev = %f",
+// __func__, mMagnitude, mPhaseOffset, mPreviousPhaseOffset);
+ if (mMagnitude > mThreshold) {
+ if (abs(phaseOffset) < kMaxPhaseError) {
+ mState = STATE_LOCKED;
+// ALOGD("%5d: switch to STATE_LOCKED", mFrameCounter);
+ }
+ // Adjust mInputPhase to match measured phase
+ mInputPhase += phaseOffset;
+ }
+ resetAccumulator();
+ }
+ incrementInputPhase();
+ break;
+
+ case STATE_LOCKED: {
+ // Predict next sine value
+ double predicted = sinf(mInputPhase) * mMagnitude;
+ double diff = predicted - sample;
+ double absDiff = fabs(diff);
+ mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff);
+ if (absDiff > mScaledTolerance) {
+ result = ERROR_GLITCHES;
+ onGlitchStart();
+// LOGI("diff glitch detected, absDiff = %g", absDiff);
+ } else {
+ mSumSquareSignal += predicted * predicted;
+ mSumSquareNoise += diff * diff;
+ // Track incoming signal and slowly adjust magnitude to account
+ // for drift in the DRC or AGC.
+ mSinAccumulator += sample * sinf(mInputPhase);
+ mCosAccumulator += sample * cosf(mInputPhase);
+ mFramesAccumulated++;
+ // Must be a multiple of the period or the calculation will not be accurate.
+ if (mFramesAccumulated == mSinePeriod) {
+ const double coefficient = 0.1;
+ double phaseOffset = 0.0;
+ double magnitude = calculateMagnitude(&phaseOffset);
+ // One pole averaging filter.
+ setMagnitude((mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient));
+
+ mMeanSquareNoise = mSumSquareNoise * mInverseSinePeriod;
+ mMeanSquareSignal = mSumSquareSignal * mInverseSinePeriod;
+ resetAccumulator();
+
+ if (abs(phaseOffset) > kMaxPhaseError) {
+ result = ERROR_GLITCHES;
+ onGlitchStart();
+ ALOGD("phase glitch detected, phaseOffset = %g", phaseOffset);
+ } else if (mMagnitude < mThreshold) {
+ result = ERROR_GLITCHES;
+ onGlitchStart();
+ ALOGD("magnitude glitch detected, mMagnitude = %g", mMagnitude);
+ }
+ }
+ }
+ incrementInputPhase();
+ } break;
+
+ case STATE_GLITCHING: {
+ // Predict next sine value
+ mGlitchLength++;
+ double predicted = sinf(mInputPhase) * mMagnitude;
+ double diff = predicted - sample;
+ double absDiff = fabs(diff);
+ mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff);
+ if (absDiff < mScaledTolerance) { // close enough?
+ // If we get a full sine period of non-glitch samples in a row then consider the glitch over.
+ // We don't want to just consider a zero crossing the end of a glitch.
+ if (mNonGlitchCount++ > mSinePeriod) {
+ onGlitchEnd();
+ }
+ } else {
+ mNonGlitchCount = 0;
+ if (mGlitchLength > (4 * mSinePeriod)) {
+ relock();
+ }
+ }
+ incrementInputPhase();
+ } break;
+
+ case NUM_STATES: // not a real state
+ break;
+ }
+
+ mFrameCounter++;
+
+ return result;
+ }
+
+ // advance and wrap phase
+ void incrementInputPhase() {
+ mInputPhase += mPhaseIncrement;
+ if (mInputPhase > M_PI) {
+ mInputPhase -= (2.0 * M_PI);
+ }
+ }
+
+ // advance and wrap phase
+ void incrementOutputPhase() {
+ mOutputPhase += mPhaseIncrement;
+ if (mOutputPhase > M_PI) {
+ mOutputPhase -= (2.0 * M_PI);
+ }
+ }
+
+ /**
+ * @param frameData upon return, contains the reference sine wave
+ * @param channelCount
+ */
+ result_code processOutputFrame(float *frameData, int channelCount) override {
+ float output = 0.0f;
+ // Output sine wave so we can measure it.
+ if (mState != STATE_IDLE) {
+ float sinOut = sinf(mOutputPhase);
+ incrementOutputPhase();
+ output = (sinOut * mOutputAmplitude)
+ + (mWhiteNoise.nextRandomDouble() * kNoiseAmplitude);
+ // ALOGD("sin(%f) = %f, %f\n", mOutputPhase, sinOut, mPhaseIncrement);
+ }
+ frameData[0] = output;
+ for (int i = 1; i < channelCount; i++) {
+ frameData[i] = 0.0f;
+ }
+ return RESULT_OK;
+ }
+
+ void onGlitchStart() {
+ mGlitchCount++;
+// ALOGD("%5d: STARTED a glitch # %d", mFrameCounter, mGlitchCount);
+ mState = STATE_GLITCHING;
+ mGlitchLength = 1;
+ mNonGlitchCount = 0;
+ }
+
+ void onGlitchEnd() {
+// ALOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength);
+ mState = STATE_LOCKED;
+ resetAccumulator();
+ }
+
+ // reset the sine wave detector
+ void resetAccumulator() {
+ mFramesAccumulated = 0;
+ mSinAccumulator = 0.0;
+ mCosAccumulator = 0.0;
+ mSumSquareSignal = 0.0;
+ mSumSquareNoise = 0.0;
+ }
+
+ void relock() {
+// ALOGD("relock: %d because of a very long %d glitch", mFrameCounter, mGlitchLength);
+ mState = STATE_WAITING_FOR_LOCK;
+ resetAccumulator();
+ }
+
+ void reset() override {
+ LoopbackProcessor::reset();
+ mState = STATE_IDLE;
+ mDownCounter = IDLE_FRAME_COUNT;
+ resetAccumulator();
+ }
+
+ void prepareToTest() override {
+ LoopbackProcessor::prepareToTest();
+ mSinePeriod = getSampleRate() / kTargetGlitchFrequency;
+ mOutputPhase = 0.0f;
+ mInverseSinePeriod = 1.0 / mSinePeriod;
+ mPhaseIncrement = 2.0 * M_PI * mInverseSinePeriod;
+ mGlitchCount = 0;
+ mMaxGlitchDelta = 0.0;
+ for (int i = 0; i < NUM_STATES; i++) {
+ mStateFrameCounters[i] = 0;
+ }
+ }
+
+private:
+
+ // These must match the values in GlitchActivity.java
+ enum sine_state_t {
+ STATE_IDLE, // beginning
+ STATE_IMMUNE, // ignoring input, waiting fo HW to settle
+ STATE_WAITING_FOR_SIGNAL, // looking for a loud signal
+ STATE_WAITING_FOR_LOCK, // trying to lock onto the phase of the sine
+ STATE_LOCKED, // locked on the sine wave, looking for glitches
+ STATE_GLITCHING, // locked on the sine wave but glitching
+ NUM_STATES
+ };
+
+ enum constants {
+ // Arbitrary durations, assuming 48000 Hz
+ IDLE_FRAME_COUNT = 48 * 100,
+ IMMUNE_FRAME_COUNT = 48 * 100,
+ PERIODS_NEEDED_FOR_LOCK = 8,
+ MIN_SNR_DB = 65
+ };
+
+ static constexpr float kNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC.
+ static constexpr int kTargetGlitchFrequency = 607;
+ static constexpr double kMaxPhaseError = M_PI * 0.05;
+
+ float mTolerance = 0.10; // scaled from 0.0 to 1.0
+ double mThreshold = 0.005;
+ int mSinePeriod = 1; // this will be set before use
+ double mInverseSinePeriod = 1.0;
+
+ int32_t mStateFrameCounters[NUM_STATES];
+
+ double mPhaseIncrement = 0.0;
+ double mInputPhase = 0.0;
+ double mOutputPhase = 0.0;
+ double mMagnitude = 0.0;
+ int32_t mFramesAccumulated = 0;
+ double mSinAccumulator = 0.0;
+ double mCosAccumulator = 0.0;
+ double mMaxGlitchDelta = 0.0;
+ int32_t mGlitchCount = 0;
+ int32_t mNonGlitchCount = 0;
+ int32_t mGlitchLength = 0;
+ // This is used for processing every frame so we cache it here.
+ double mScaledTolerance = 0.0;
+ int mDownCounter = IDLE_FRAME_COUNT;
+ int32_t mFrameCounter = 0;
+ double mOutputAmplitude = 0.75;
+
+ int32_t mForceGlitchDuration = 0; // if > 0 then force a glitch for debugging
+ int32_t mForceGlitchCounter = 4 * 48000; // count down and trigger at zero
+
+ // measure background noise continuously as a deviation from the expected signal
+ double mSumSquareSignal = 0.0;
+ double mSumSquareNoise = 0.0;
+ double mMeanSquareSignal = 0.0;
+ double mMeanSquareNoise = 0.0;
+
+ PeakDetector mPeakFollower;
+
+ PseudoRandom mWhiteNoise;
+
+ sine_state_t mState = STATE_IDLE;
+};
+
+
+#endif //ANALYZER_GLITCH_ANALYZER_H
diff --git a/media/libaaudio/examples/loopback/src/analyzer/LatencyAnalyzer.h b/media/libaaudio/examples/loopback/src/analyzer/LatencyAnalyzer.h
new file mode 100644
index 0000000..e506791
--- /dev/null
+++ b/media/libaaudio/examples/loopback/src/analyzer/LatencyAnalyzer.h
@@ -0,0 +1,606 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+/**
+ * Tools for measuring latency and for detecting glitches.
+ * These classes are pure math and can be used with any audio system.
+ */
+
+#ifndef ANALYZER_LATENCY_ANALYZER_H
+#define ANALYZER_LATENCY_ANALYZER_H
+
+#include <algorithm>
+#include <assert.h>
+#include <cctype>
+#include <iomanip>
+#include <iostream>
+#include <math.h>
+#include <memory>
+#include <sstream>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <vector>
+
+#include "PeakDetector.h"
+#include "PseudoRandom.h"
+#include "RandomPulseGenerator.h"
+
+// This is used when the code is in Oboe.
+#ifndef ALOGD
+#define ALOGD printf
+#define ALOGE printf
+#define ALOGW printf
+#endif
+
+#define LOOPBACK_RESULT_TAG "RESULT: "
+
+static constexpr int32_t kDefaultSampleRate = 48000;
+static constexpr int32_t kMillisPerSecond = 1000;
+static constexpr int32_t kMaxLatencyMillis = 700; // arbitrary and generous
+static constexpr double kMinimumConfidence = 0.2;
+
+struct LatencyReport {
+ int32_t latencyInFrames = 0.0;
+ double confidence = 0.0;
+
+ void reset() {
+ latencyInFrames = 0;
+ confidence = 0.0;
+ }
+};
+
+// Calculate a normalized cross correlation.
+static double calculateNormalizedCorrelation(const float *a,
+ const float *b,
+ int windowSize) {
+ double correlation = 0.0;
+ double sumProducts = 0.0;
+ double sumSquares = 0.0;
+
+ // Correlate a against b.
+ for (int i = 0; i < windowSize; i++) {
+ float s1 = a[i];
+ float s2 = b[i];
+ // Use a normalized cross-correlation.
+ sumProducts += s1 * s2;
+ sumSquares += ((s1 * s1) + (s2 * s2));
+ }
+
+ if (sumSquares >= 1.0e-9) {
+ correlation = 2.0 * sumProducts / sumSquares;
+ }
+ return correlation;
+}
+
+static double calculateRootMeanSquare(float *data, int32_t numSamples) {
+ double sum = 0.0;
+ for (int32_t i = 0; i < numSamples; i++) {
+ float sample = data[i];
+ sum += sample * sample;
+ }
+ return sqrt(sum / numSamples);
+}
+
+/**
+ * Monophonic recording with processing.
+ */
+class AudioRecording
+{
+public:
+
+ void allocate(int maxFrames) {
+ mData = std::make_unique<float[]>(maxFrames);
+ mMaxFrames = maxFrames;
+ }
+
+ // Write SHORT data from the first channel.
+ int32_t write(int16_t *inputData, int32_t inputChannelCount, int32_t numFrames) {
+ // stop at end of buffer
+ if ((mFrameCounter + numFrames) > mMaxFrames) {
+ numFrames = mMaxFrames - mFrameCounter;
+ }
+ for (int i = 0; i < numFrames; i++) {
+ mData[mFrameCounter++] = inputData[i * inputChannelCount] * (1.0f / 32768);
+ }
+ return numFrames;
+ }
+
+ // Write FLOAT data from the first channel.
+ int32_t write(float *inputData, int32_t inputChannelCount, int32_t numFrames) {
+ // stop at end of buffer
+ if ((mFrameCounter + numFrames) > mMaxFrames) {
+ numFrames = mMaxFrames - mFrameCounter;
+ }
+ for (int i = 0; i < numFrames; i++) {
+ mData[mFrameCounter++] = inputData[i * inputChannelCount];
+ }
+ return numFrames;
+ }
+
+ // Write FLOAT data from the first channel.
+ int32_t write(float sample) {
+ // stop at end of buffer
+ if (mFrameCounter < mMaxFrames) {
+ mData[mFrameCounter++] = sample;
+ return 1;
+ }
+ return 0;
+ }
+
+ void clear() {
+ mFrameCounter = 0;
+ }
+ int32_t size() const {
+ return mFrameCounter;
+ }
+
+ bool isFull() const {
+ return mFrameCounter >= mMaxFrames;
+ }
+
+ float *getData() const {
+ return mData.get();
+ }
+
+ void setSampleRate(int32_t sampleRate) {
+ mSampleRate = sampleRate;
+ }
+
+ int32_t getSampleRate() const {
+ return mSampleRate;
+ }
+
+ /**
+ * Square the samples so they are all positive and so the peaks are emphasized.
+ */
+ void square() {
+ float *x = mData.get();
+ for (int i = 0; i < mFrameCounter; i++) {
+ x[i] *= x[i];
+ }
+ }
+
+ /**
+ * Amplify a signal so that the peak matches the specified target.
+ *
+ * @param target final max value
+ * @return gain applied to signal
+ */
+ float normalize(float target) {
+ float maxValue = 1.0e-9f;
+ for (int i = 0; i < mFrameCounter; i++) {
+ maxValue = std::max(maxValue, abs(mData[i]));
+ }
+ float gain = target / maxValue;
+ for (int i = 0; i < mFrameCounter; i++) {
+ mData[i] *= gain;
+ }
+ return gain;
+ }
+
+private:
+ std::unique_ptr<float[]> mData;
+ int32_t mFrameCounter = 0;
+ int32_t mMaxFrames = 0;
+ int32_t mSampleRate = kDefaultSampleRate; // common default
+};
+
+static int measureLatencyFromPulse(AudioRecording &recorded,
+ AudioRecording &pulse,
+ LatencyReport *report) {
+
+ report->latencyInFrames = 0;
+ report->confidence = 0.0;
+
+ int numCorrelations = recorded.size() - pulse.size();
+ if (numCorrelations < 10) {
+ ALOGE("%s() recording too small = %d frames\n", __func__, recorded.size());
+ return -1;
+ }
+ std::unique_ptr<float[]> correlations= std::make_unique<float[]>(numCorrelations);
+
+ // Correlate pulse against the recorded data.
+ for (int i = 0; i < numCorrelations; i++) {
+ float correlation = (float) calculateNormalizedCorrelation(&recorded.getData()[i],
+ &pulse.getData()[0],
+ pulse.size());
+ correlations[i] = correlation;
+ }
+
+ // Find highest peak in correlation array.
+ float peakCorrelation = 0.0;
+ int peakIndex = -1;
+ for (int i = 0; i < numCorrelations; i++) {
+ float value = abs(correlations[i]);
+ if (value > peakCorrelation) {
+ peakCorrelation = value;
+ peakIndex = i;
+ }
+ }
+ if (peakIndex < 0) {
+ ALOGE("%s() no signal for correlation\n", __func__);
+ return -2;
+ }
+
+ report->latencyInFrames = peakIndex;
+ report->confidence = peakCorrelation;
+
+ return 0;
+}
+
+// ====================================================================================
+class LoopbackProcessor {
+public:
+ virtual ~LoopbackProcessor() = default;
+
+ enum result_code {
+ RESULT_OK = 0,
+ ERROR_NOISY = -99,
+ ERROR_VOLUME_TOO_LOW,
+ ERROR_VOLUME_TOO_HIGH,
+ ERROR_CONFIDENCE,
+ ERROR_INVALID_STATE,
+ ERROR_GLITCHES,
+ ERROR_NO_LOCK
+ };
+
+ virtual void prepareToTest() {
+ reset();
+ }
+
+ virtual void reset() {
+ mResult = 0;
+ mResetCount++;
+ }
+
+ virtual result_code processInputFrame(float *frameData, int channelCount) = 0;
+ virtual result_code processOutputFrame(float *frameData, int channelCount) = 0;
+
+ void process(float *inputData, int inputChannelCount, int numInputFrames,
+ float *outputData, int outputChannelCount, int numOutputFrames) {
+ int numBoth = std::min(numInputFrames, numOutputFrames);
+ // Process one frame at a time.
+ for (int i = 0; i < numBoth; i++) {
+ processInputFrame(inputData, inputChannelCount);
+ inputData += inputChannelCount;
+ processOutputFrame(outputData, outputChannelCount);
+ outputData += outputChannelCount;
+ }
+ // If there is more input than output.
+ for (int i = numBoth; i < numInputFrames; i++) {
+ processInputFrame(inputData, inputChannelCount);
+ inputData += inputChannelCount;
+ }
+ // If there is more output than input.
+ for (int i = numBoth; i < numOutputFrames; i++) {
+ processOutputFrame(outputData, outputChannelCount);
+ outputData += outputChannelCount;
+ }
+ }
+
+ virtual std::string analyze() = 0;
+
+ virtual void printStatus() {};
+
+ int32_t getResult() {
+ return mResult;
+ }
+
+ void setResult(int32_t result) {
+ mResult = result;
+ }
+
+ virtual bool isDone() {
+ return false;
+ }
+
+ virtual int save(const char *fileName) {
+ (void) fileName;
+ return -1;
+ }
+
+ virtual int load(const char *fileName) {
+ (void) fileName;
+ return -1;
+ }
+
+ virtual void setSampleRate(int32_t sampleRate) {
+ mSampleRate = sampleRate;
+ }
+
+ int32_t getSampleRate() const {
+ return mSampleRate;
+ }
+
+ int32_t getResetCount() const {
+ return mResetCount;
+ }
+
+ /** Called when not enough input frames could be read after synchronization.
+ */
+ virtual void onInsufficientRead() {
+ reset();
+ }
+
+protected:
+ int32_t mResetCount = 0;
+
+private:
+ int32_t mSampleRate = kDefaultSampleRate;
+ int32_t mResult = 0;
+};
+
+class LatencyAnalyzer : public LoopbackProcessor {
+public:
+
+ LatencyAnalyzer() : LoopbackProcessor() {}
+ virtual ~LatencyAnalyzer() = default;
+
+ virtual int32_t getProgress() const = 0;
+
+ virtual int getState() = 0;
+
+ // @return latency in frames
+ virtual int32_t getMeasuredLatency() = 0;
+
+ virtual double getMeasuredConfidence() = 0;
+
+ virtual double getBackgroundRMS() = 0;
+
+ virtual double getSignalRMS() = 0;
+
+};
+
+// ====================================================================================
+/**
+ * Measure latency given a loopback stream data.
+ * Use an encoded bit train as the sound source because it
+ * has an unambiguous correlation value.
+ * Uses a state machine to cycle through various stages.
+ *
+ */
+class PulseLatencyAnalyzer : public LatencyAnalyzer {
+public:
+
+ PulseLatencyAnalyzer() : LatencyAnalyzer() {
+ int32_t maxLatencyFrames = getSampleRate() * kMaxLatencyMillis / kMillisPerSecond;
+ int32_t numPulseBits = getSampleRate() * kPulseLengthMillis
+ / (kFramesPerEncodedBit * kMillisPerSecond);
+ int32_t pulseLength = numPulseBits * kFramesPerEncodedBit;
+ mFramesToRecord = pulseLength + maxLatencyFrames;
+ mAudioRecording.allocate(mFramesToRecord);
+ mAudioRecording.setSampleRate(getSampleRate());
+ generateRandomPulse(pulseLength);
+ }
+
+ void generateRandomPulse(int32_t pulseLength) {
+ mPulse.allocate(pulseLength);
+ RandomPulseGenerator pulser(kFramesPerEncodedBit);
+ for (int i = 0; i < pulseLength; i++) {
+ mPulse.write(pulser.nextFloat());
+ }
+ }
+
+ int getState() override {
+ return mState;
+ }
+
+ void setSampleRate(int32_t sampleRate) override {
+ LoopbackProcessor::setSampleRate(sampleRate);
+ mAudioRecording.setSampleRate(sampleRate);
+ }
+
+ void reset() override {
+ LoopbackProcessor::reset();
+ mDownCounter = getSampleRate() / 2;
+ mLoopCounter = 0;
+
+ mPulseCursor = 0;
+ mBackgroundSumSquare = 0.0f;
+ mBackgroundSumCount = 0;
+ mBackgroundRMS = 0.0f;
+ mSignalRMS = 0.0f;
+
+ mState = STATE_MEASURE_BACKGROUND;
+ mAudioRecording.clear();
+ mLatencyReport.reset();
+ }
+
+ bool hasEnoughData() {
+ return mAudioRecording.isFull();
+ }
+
+ bool isDone() override {
+ return mState == STATE_DONE;
+ }
+
+ int32_t getProgress() const override {
+ return mAudioRecording.size();
+ }
+
+ std::string analyze() override {
+ std::stringstream report;
+ report << "PulseLatencyAnalyzer ---------------\n";
+ report << LOOPBACK_RESULT_TAG "test.state = "
+ << std::setw(8) << mState << "\n";
+ report << LOOPBACK_RESULT_TAG "test.state.name = "
+ << convertStateToText(mState) << "\n";
+ report << LOOPBACK_RESULT_TAG "background.rms = "
+ << std::setw(8) << mBackgroundRMS << "\n";
+
+ int32_t newResult = RESULT_OK;
+ if (mState != STATE_GOT_DATA) {
+ report << "WARNING - Bad state. Check volume on device.\n";
+ // setResult(ERROR_INVALID_STATE);
+ } else {
+ float gain = mAudioRecording.normalize(1.0f);
+ measureLatencyFromPulse(mAudioRecording,
+ mPulse,
+ &mLatencyReport);
+
+ if (mLatencyReport.confidence < kMinimumConfidence) {
+ report << " ERROR - confidence too low!";
+ newResult = ERROR_CONFIDENCE;
+ } else {
+ mSignalRMS = calculateRootMeanSquare(
+ &mAudioRecording.getData()[mLatencyReport.latencyInFrames], mPulse.size())
+ / gain;
+ }
+ double latencyMillis = kMillisPerSecond * (double) mLatencyReport.latencyInFrames
+ / getSampleRate();
+ report << LOOPBACK_RESULT_TAG "latency.frames = " << std::setw(8)
+ << mLatencyReport.latencyInFrames << "\n";
+ report << LOOPBACK_RESULT_TAG "latency.msec = " << std::setw(8)
+ << latencyMillis << "\n";
+ report << LOOPBACK_RESULT_TAG "latency.confidence = " << std::setw(8)
+ << mLatencyReport.confidence << "\n";
+ }
+ mState = STATE_DONE;
+ if (getResult() == RESULT_OK) {
+ setResult(newResult);
+ }
+
+ return report.str();
+ }
+
+ int32_t getMeasuredLatency() override {
+ return mLatencyReport.latencyInFrames;
+ }
+
+ double getMeasuredConfidence() override {
+ return mLatencyReport.confidence;
+ }
+
+ double getBackgroundRMS() override {
+ return mBackgroundRMS;
+ }
+
+ double getSignalRMS() override {
+ return mSignalRMS;
+ }
+
+ void printStatus() override {
+ ALOGD("st = %d", mState);
+ }
+
+ result_code processInputFrame(float *frameData, int channelCount) override {
+ echo_state nextState = mState;
+ mLoopCounter++;
+
+ switch (mState) {
+ case STATE_MEASURE_BACKGROUND:
+ // Measure background RMS on channel 0
+ mBackgroundSumSquare += frameData[0] * frameData[0];
+ mBackgroundSumCount++;
+ mDownCounter--;
+ if (mDownCounter <= 0) {
+ mBackgroundRMS = sqrtf(mBackgroundSumSquare / mBackgroundSumCount);
+ nextState = STATE_IN_PULSE;
+ mPulseCursor = 0;
+ }
+ break;
+
+ case STATE_IN_PULSE:
+ // Record input until the mAudioRecording is full.
+ mAudioRecording.write(frameData, channelCount, 1);
+ if (hasEnoughData()) {
+ nextState = STATE_GOT_DATA;
+ }
+ break;
+
+ case STATE_GOT_DATA:
+ case STATE_DONE:
+ default:
+ break;
+ }
+
+ mState = nextState;
+ return RESULT_OK;
+ }
+
+ result_code processOutputFrame(float *frameData, int channelCount) override {
+ switch (mState) {
+ case STATE_IN_PULSE:
+ if (mPulseCursor < mPulse.size()) {
+ float pulseSample = mPulse.getData()[mPulseCursor++];
+ for (int i = 0; i < channelCount; i++) {
+ frameData[i] = pulseSample;
+ }
+ } else {
+ for (int i = 0; i < channelCount; i++) {
+ frameData[i] = 0;
+ }
+ }
+ break;
+
+ case STATE_MEASURE_BACKGROUND:
+ case STATE_GOT_DATA:
+ case STATE_DONE:
+ default:
+ for (int i = 0; i < channelCount; i++) {
+ frameData[i] = 0.0f; // silence
+ }
+ break;
+ }
+
+ return RESULT_OK;
+ }
+
+private:
+
+ enum echo_state {
+ STATE_MEASURE_BACKGROUND,
+ STATE_IN_PULSE,
+ STATE_GOT_DATA, // must match RoundTripLatencyActivity.java
+ STATE_DONE,
+ };
+
+ const char *convertStateToText(echo_state state) {
+ switch (state) {
+ case STATE_MEASURE_BACKGROUND:
+ return "INIT";
+ case STATE_IN_PULSE:
+ return "PULSE";
+ case STATE_GOT_DATA:
+ return "GOT_DATA";
+ case STATE_DONE:
+ return "DONE";
+ }
+ return "UNKNOWN";
+ }
+
+ int32_t mDownCounter = 500;
+ int32_t mLoopCounter = 0;
+ echo_state mState = STATE_MEASURE_BACKGROUND;
+
+ static constexpr int32_t kFramesPerEncodedBit = 8; // multiple of 2
+ static constexpr int32_t kPulseLengthMillis = 500;
+
+ AudioRecording mPulse;
+ int32_t mPulseCursor = 0;
+
+ double mBackgroundSumSquare = 0.0;
+ int32_t mBackgroundSumCount = 0;
+ double mBackgroundRMS = 0.0;
+ double mSignalRMS = 0.0;
+ int32_t mFramesToRecord = 0;
+
+ AudioRecording mAudioRecording; // contains only the input after starting the pulse
+ LatencyReport mLatencyReport;
+};
+
+#endif // ANALYZER_LATENCY_ANALYZER_H
diff --git a/media/libaaudio/examples/loopback/src/analyzer/ManchesterEncoder.h b/media/libaaudio/examples/loopback/src/analyzer/ManchesterEncoder.h
new file mode 100644
index 0000000..0a4bd5b
--- /dev/null
+++ b/media/libaaudio/examples/loopback/src/analyzer/ManchesterEncoder.h
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2019 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.
+ */
+
+#ifndef ANALYZER_MANCHESTER_ENCODER_H
+#define ANALYZER_MANCHESTER_ENCODER_H
+
+#include <cstdint>
+
+/**
+ * Encode bytes using Manchester Coding scheme.
+ *
+ * Manchester Code is self clocking.
+ * There is a transition in the middle of every bit.
+ * Zero is high then low.
+ * One is low then high.
+ *
+ * This avoids having long DC sections that would droop when
+ * passed though analog circuits with AC coupling.
+ *
+ * IEEE 802.3 compatible.
+ */
+
+class ManchesterEncoder {
+public:
+ ManchesterEncoder(int samplesPerPulse)
+ : mSamplesPerPulse(samplesPerPulse)
+ , mSamplesPerPulseHalf(samplesPerPulse / 2)
+ , mCursor(samplesPerPulse) {
+ }
+
+ virtual ~ManchesterEncoder() = default;
+
+ /**
+ * This will be called when the next byte is needed.
+ * @return
+ */
+ virtual uint8_t onNextByte() = 0;
+
+ /**
+ * Generate the next floating point sample.
+ * @return
+ */
+ virtual float nextFloat() {
+ advanceSample();
+ if (mCurrentBit) {
+ return (mCursor < mSamplesPerPulseHalf) ? -1.0f : 1.0f; // one
+ } else {
+ return (mCursor < mSamplesPerPulseHalf) ? 1.0f : -1.0f; // zero
+ }
+ }
+
+protected:
+ /**
+ * This will be called when a new bit is ready to be encoded.
+ * It can be used to prepare the encoded samples.
+ * @param current
+ */
+ virtual void onNextBit(bool /* current */) {};
+
+ void advanceSample() {
+ // Are we ready for a new bit?
+ if (++mCursor >= mSamplesPerPulse) {
+ mCursor = 0;
+ if (mBitsLeft == 0) {
+ mCurrentByte = onNextByte();
+ mBitsLeft = 8;
+ }
+ --mBitsLeft;
+ mCurrentBit = (mCurrentByte >> mBitsLeft) & 1;
+ onNextBit(mCurrentBit);
+ }
+ }
+
+ bool getCurrentBit() {
+ return mCurrentBit;
+ }
+
+ const int mSamplesPerPulse;
+ const int mSamplesPerPulseHalf;
+ int mCursor;
+ int mBitsLeft = 0;
+ uint8_t mCurrentByte = 0;
+ bool mCurrentBit = false;
+};
+#endif //ANALYZER_MANCHESTER_ENCODER_H
diff --git a/media/libaaudio/examples/loopback/src/analyzer/PeakDetector.h b/media/libaaudio/examples/loopback/src/analyzer/PeakDetector.h
new file mode 100644
index 0000000..4b3b4e7
--- /dev/null
+++ b/media/libaaudio/examples/loopback/src/analyzer/PeakDetector.h
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+#ifndef ANALYZER_PEAK_DETECTOR_H
+#define ANALYZER_PEAK_DETECTOR_H
+
+#include <math.h>
+
+/**
+ * Measure a peak envelope by rising with the peaks,
+ * and decaying exponentially after each peak.
+ * The absolute value of the input signal is used.
+ */
+class PeakDetector {
+public:
+
+ void reset() {
+ mLevel = 0.0;
+ }
+
+ double process(double input) {
+ mLevel *= mDecay; // exponential decay
+ input = fabs(input);
+ // never fall below the input signal
+ if (input > mLevel) {
+ mLevel = input;
+ }
+ return mLevel;
+ }
+
+ double getLevel() const {
+ return mLevel;
+ }
+
+ double getDecay() const {
+ return mDecay;
+ }
+
+ /**
+ * Multiply the level by this amount on every iteration.
+ * This provides an exponential decay curve.
+ * A value just under 1.0 is best, for example, 0.99;
+ * @param decay scale level for each input
+ */
+ void setDecay(double decay) {
+ mDecay = decay;
+ }
+
+private:
+ static constexpr double kDefaultDecay = 0.99f;
+
+ double mLevel = 0.0;
+ double mDecay = kDefaultDecay;
+};
+#endif //ANALYZER_PEAK_DETECTOR_H
diff --git a/media/libaaudio/examples/loopback/src/analyzer/PseudoRandom.h b/media/libaaudio/examples/loopback/src/analyzer/PseudoRandom.h
new file mode 100644
index 0000000..1c4938c
--- /dev/null
+++ b/media/libaaudio/examples/loopback/src/analyzer/PseudoRandom.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2017 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.
+ */
+
+
+#ifndef ANALYZER_PSEUDORANDOM_H
+#define ANALYZER_PSEUDORANDOM_H
+
+#include <cctype>
+
+class PseudoRandom {
+public:
+ PseudoRandom(int64_t seed = 99887766)
+ : mSeed(seed)
+ {}
+
+ /**
+ * Returns the next random double from -1.0 to 1.0
+ *
+ * @return value from -1.0 to 1.0
+ */
+ double nextRandomDouble() {
+ return nextRandomInteger() * (0.5 / (((int32_t)1) << 30));
+ }
+
+ /** Calculate random 32 bit number using linear-congruential method
+ * with known real-time performance.
+ */
+ int32_t nextRandomInteger() {
+#if __has_builtin(__builtin_mul_overflow) && __has_builtin(__builtin_add_overflow)
+ int64_t prod;
+ // Use values for 64-bit sequence from MMIX by Donald Knuth.
+ __builtin_mul_overflow(mSeed, (int64_t)6364136223846793005, &prod);
+ __builtin_add_overflow(prod, (int64_t)1442695040888963407, &mSeed);
+#else
+ mSeed = (mSeed * (int64_t)6364136223846793005) + (int64_t)1442695040888963407;
+#endif
+ return (int32_t) (mSeed >> 32); // The higher bits have a longer sequence.
+ }
+
+private:
+ int64_t mSeed;
+};
+
+#endif //ANALYZER_PSEUDORANDOM_H
diff --git a/media/libaaudio/examples/loopback/src/analyzer/RandomPulseGenerator.h b/media/libaaudio/examples/loopback/src/analyzer/RandomPulseGenerator.h
new file mode 100644
index 0000000..030050b
--- /dev/null
+++ b/media/libaaudio/examples/loopback/src/analyzer/RandomPulseGenerator.h
@@ -0,0 +1,43 @@
+/*
+ * 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.
+ */
+
+#ifndef ANALYZER_RANDOM_PULSE_GENERATOR_H
+#define ANALYZER_RANDOM_PULSE_GENERATOR_H
+
+#include <stdlib.h>
+#include "RoundedManchesterEncoder.h"
+
+/**
+ * Encode random ones and zeros using Manchester Code per IEEE 802.3.
+ */
+class RandomPulseGenerator : public RoundedManchesterEncoder {
+public:
+ RandomPulseGenerator(int samplesPerPulse)
+ : RoundedManchesterEncoder(samplesPerPulse) {
+ }
+
+ virtual ~RandomPulseGenerator() = default;
+
+ /**
+ * This will be called when the next byte is needed.
+ * @return random byte
+ */
+ uint8_t onNextByte() override {
+ return static_cast<uint8_t>(rand());
+ }
+};
+
+#endif //ANALYZER_RANDOM_PULSE_GENERATOR_H
diff --git a/media/libaaudio/examples/loopback/src/analyzer/RoundedManchesterEncoder.h b/media/libaaudio/examples/loopback/src/analyzer/RoundedManchesterEncoder.h
new file mode 100644
index 0000000..f2eba84
--- /dev/null
+++ b/media/libaaudio/examples/loopback/src/analyzer/RoundedManchesterEncoder.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2019 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.
+ */
+
+#ifndef ANALYZER_ROUNDED_MANCHESTER_ENCODER_H
+#define ANALYZER_ROUNDED_MANCHESTER_ENCODER_H
+
+#include <math.h>
+#include <memory.h>
+#include <stdlib.h>
+#include "ManchesterEncoder.h"
+
+/**
+ * Encode bytes using Manchester Code.
+ * Round the edges using a half cosine to reduce ringing caused by a hard edge.
+ */
+
+class RoundedManchesterEncoder : public ManchesterEncoder {
+public:
+ RoundedManchesterEncoder(int samplesPerPulse)
+ : ManchesterEncoder(samplesPerPulse) {
+ int rampSize = samplesPerPulse / 4;
+ mZeroAfterZero = std::make_unique<float[]>(samplesPerPulse);
+ mZeroAfterOne = std::make_unique<float[]>(samplesPerPulse);
+
+ int sampleIndex = 0;
+ for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) {
+ float phase = (rampIndex + 1) * M_PI / rampSize;
+ float sample = -cosf(phase);
+ mZeroAfterZero[sampleIndex] = sample;
+ mZeroAfterOne[sampleIndex] = 1.0f;
+ sampleIndex++;
+ }
+ for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) {
+ mZeroAfterZero[sampleIndex] = 1.0f;
+ mZeroAfterOne[sampleIndex] = 1.0f;
+ sampleIndex++;
+ }
+ for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) {
+ float phase = (rampIndex + 1) * M_PI / rampSize;
+ float sample = cosf(phase);
+ mZeroAfterZero[sampleIndex] = sample;
+ mZeroAfterOne[sampleIndex] = sample;
+ sampleIndex++;
+ }
+ for (int rampIndex = 0; rampIndex < rampSize; rampIndex++) {
+ mZeroAfterZero[sampleIndex] = -1.0f;
+ mZeroAfterOne[sampleIndex] = -1.0f;
+ sampleIndex++;
+ }
+ }
+
+ void onNextBit(bool current) override {
+ // Do we need to use the rounded edge?
+ mCurrentSamples = (current ^ mPreviousBit)
+ ? mZeroAfterOne.get()
+ : mZeroAfterZero.get();
+ mPreviousBit = current;
+ }
+
+ float nextFloat() override {
+ advanceSample();
+ float output = mCurrentSamples[mCursor];
+ if (getCurrentBit()) output = -output;
+ return output;
+ }
+
+private:
+
+ bool mPreviousBit = false;
+ float *mCurrentSamples = nullptr;
+ std::unique_ptr<float[]> mZeroAfterZero;
+ std::unique_ptr<float[]> mZeroAfterOne;
+};
+
+#endif //ANALYZER_ROUNDED_MANCHESTER_ENCODER_H
diff --git a/media/libaaudio/examples/loopback/src/loopback.cpp b/media/libaaudio/examples/loopback/src/loopback.cpp
index 49d921f..0d2ec70 100644
--- a/media/libaaudio/examples/loopback/src/loopback.cpp
+++ b/media/libaaudio/examples/loopback/src/loopback.cpp
@@ -20,6 +20,8 @@
#include <assert.h>
#include <cctype>
#include <errno.h>
+#include <iomanip>
+#include <iostream>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
@@ -33,7 +35,9 @@
#include "AAudioSimplePlayer.h"
#include "AAudioSimpleRecorder.h"
#include "AAudioExampleUtils.h"
-#include "LoopbackAnalyzer.h"
+
+#include "analyzer/GlitchAnalyzer.h"
+#include "analyzer/LatencyAnalyzer.h"
#include "../../utils/AAudioExampleUtils.h"
// V0.4.00 = rectify and low-pass filter the echos, auto-correlate entire echo
@@ -41,7 +45,8 @@
// fix -n option to set output buffer for -tm
// plot first glitch
// V0.4.02 = allow -n0 for minimal buffer size
-#define APP_VERSION "0.4.02"
+// V0.5.00 = use latency analyzer from OboeTester, uses random noise for latency
+#define APP_VERSION "0.5.00"
// Tag for machine readable results as property = value pairs
#define RESULT_TAG "RESULT: "
@@ -57,6 +62,20 @@
constexpr int kDefaultHangTimeMillis = 50;
constexpr int kMaxGlitchEventsToSave = 32;
+static void printAudioScope(float sample) {
+ const int maxStars = 80; // arbitrary, fits on one line
+ char c = '*';
+ if (sample < -1.0) {
+ sample = -1.0;
+ c = '$';
+ } else if (sample > 1.0) {
+ sample = 1.0;
+ c = '$';
+ }
+ int numSpaces = (int) (((sample + 1.0) * 0.5) * maxStars);
+ printf("%*c%c\n", numSpaces, ' ', c);
+}
+
struct LoopbackData {
AAudioStream *inputStream = nullptr;
AAudioStream *outputStream = nullptr;
@@ -83,8 +102,8 @@
aaudio_result_t inputError = AAUDIO_OK;
aaudio_result_t outputError = AAUDIO_OK;
- SineAnalyzer sineAnalyzer;
- EchoAnalyzer echoAnalyzer;
+ GlitchAnalyzer sineAnalyzer;
+ PulseLatencyAnalyzer echoAnalyzer;
AudioRecording audioRecording;
LoopbackProcessor *loopbackProcessor;
@@ -254,17 +273,18 @@
}
// Analyze the data.
- LoopbackProcessor::process_result procResult = myData->loopbackProcessor->process(myData->inputFloatData,
+ myData->loopbackProcessor->process(myData->inputFloatData,
myData->actualInputChannelCount,
+ numFrames,
outputData,
myData->actualOutputChannelCount,
numFrames);
-
- if (procResult == LoopbackProcessor::PROCESS_RESULT_GLITCH) {
- if (myData->numGlitchEvents < kMaxGlitchEventsToSave) {
- myData->glitchFrames[myData->numGlitchEvents++] = myData->audioRecording.size();
- }
- }
+//
+// if (procResult == LoopbackProcessor::PROCESS_RESULT_GLITCH) {
+// if (myData->numGlitchEvents < kMaxGlitchEventsToSave) {
+// myData->glitchFrames[myData->numGlitchEvents++] = myData->audioRecording.size();
+// }
+// }
// Save for later.
myData->audioRecording.write(myData->inputFloatData,
@@ -283,8 +303,8 @@
}
static void MyErrorCallbackProc(
- AAudioStream *stream __unused,
- void *userData __unused,
+ AAudioStream * /* stream */,
+ void * userData,
aaudio_result_t error) {
printf("Error Callback, error: %d\n",(int)error);
LoopbackData *myData = (LoopbackData *) userData;
@@ -305,8 +325,8 @@
printf(" l for _LATENCY\n");
printf(" p for _POWER_SAVING\n");
printf(" -t{test} select test mode\n");
- printf(" m for sine magnitude\n");
- printf(" e for echo latency (default)\n");
+ printf(" g for Glitch detection\n");
+ printf(" l for round trip Latency (default)\n");
printf(" f for file latency, analyzes %s\n\n", FILENAME_ECHOS);
printf(" -X use EXCLUSIVE mode for input\n");
printf("Example: aaudio_loopback -n2 -pl -Pl -x\n");
@@ -333,20 +353,22 @@
}
enum {
- TEST_SINE_MAGNITUDE = 0,
- TEST_ECHO_LATENCY,
+ TEST_GLITCHES = 0,
+ TEST_LATENCY,
TEST_FILE_LATENCY,
};
static int parseTestMode(char c) {
- int testMode = TEST_ECHO_LATENCY;
+ int testMode = TEST_LATENCY;
c = tolower(c);
switch (c) {
- case 'm':
- testMode = TEST_SINE_MAGNITUDE;
+ case 'm': // deprecated
+ case 'g':
+ testMode = TEST_GLITCHES;
break;
- case 'e':
- testMode = TEST_ECHO_LATENCY;
+ case 'e': // deprecated
+ case 'l':
+ testMode = TEST_LATENCY;
break;
case 'f':
testMode = TEST_FILE_LATENCY;
@@ -408,9 +430,10 @@
int32_t actualSampleRate = 0;
int written = 0;
- int testMode = TEST_ECHO_LATENCY;
+ int testMode = TEST_LATENCY;
double gain = 1.0;
int hangTimeMillis = 0;
+ std::string report;
// Make printf print immediately so that debug info is not stuck
// in a buffer if we hang or crash.
@@ -488,22 +511,21 @@
int32_t requestedOutputBursts = argParser.getNumberOfBursts();
switch(testMode) {
- case TEST_SINE_MAGNITUDE:
+ case TEST_GLITCHES:
loopbackData.loopbackProcessor = &loopbackData.sineAnalyzer;
break;
- case TEST_ECHO_LATENCY:
- loopbackData.echoAnalyzer.setGain(gain);
+ case TEST_LATENCY:
+ // TODO loopbackData.echoAnalyzer.setGain(gain);
loopbackData.loopbackProcessor = &loopbackData.echoAnalyzer;
break;
case TEST_FILE_LATENCY: {
- loopbackData.echoAnalyzer.setGain(gain);
-
+ // TODO loopbackData.echoAnalyzer.setGain(gain);
loopbackData.loopbackProcessor = &loopbackData.echoAnalyzer;
int read = loopbackData.loopbackProcessor->load(FILENAME_ECHOS);
printf("main() read %d mono samples from %s on Android device, rate = %d\n",
read, FILENAME_ECHOS,
loopbackData.loopbackProcessor->getSampleRate());
- loopbackData.loopbackProcessor->report();
+ std::cout << loopbackData.loopbackProcessor->analyze();
goto report_result;
}
break;
@@ -557,7 +579,7 @@
int32_t actualCapacity = AAudioStream_getBufferCapacityInFrames(inputStream);
(void) AAudioStream_setBufferSizeInFrames(inputStream, actualCapacity);
- if (testMode == TEST_SINE_MAGNITUDE
+ if (testMode == TEST_GLITCHES
&& requestedOutputBursts == AAUDIO_UNSPECIFIED) {
result = AAudioStream_setBufferSizeInFrames(outputStream, actualCapacity);
if (result < 0) {
@@ -594,10 +616,10 @@
loopbackData.inputFloatData = new float[loopbackData.inputFramesMaximum *
loopbackData.actualInputChannelCount]{};
- loopbackData.loopbackProcessor->reset();
-
loopbackData.hangTimeMillis = hangTimeMillis;
+ loopbackData.loopbackProcessor->prepareToTest();
+
// Start OUTPUT first so INPUT does not overflow.
result = player.start();
if (result != AAUDIO_OK) {
@@ -669,7 +691,8 @@
printf("input error = %d = %s\n",
loopbackData.inputError, AAudio_convertResultToText(loopbackData.inputError));
-
+/*
+ // TODO Restore this code some day if we want to save files.
written = loopbackData.loopbackProcessor->save(FILENAME_ECHOS);
if (written > 0) {
printf("main() wrote %8d mono samples to \"%s\" on Android device\n",
@@ -681,9 +704,9 @@
printf("main() wrote %8d mono samples to \"%s\" on Android device\n",
written, FILENAME_ALL);
}
-
+*/
if (loopbackData.inputError == AAUDIO_OK) {
- if (testMode == TEST_SINE_MAGNITUDE) {
+ if (testMode == TEST_GLITCHES) {
if (loopbackData.numGlitchEvents > 0) {
// Graph around the first glitch if there is one.
const int32_t start = loopbackData.glitchFrames[0] - 8;
@@ -697,7 +720,8 @@
}
}
- loopbackData.loopbackProcessor->report();
+ std::cout << "Please wait several seconds for analysis to complete.\n";
+ std::cout << loopbackData.loopbackProcessor->analyze();
}
{