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
gugelfrei
Phil Burk 5 years ago
parent 9c7e2a51e2
commit 62aa0219c2

File diff suppressed because it is too large Load Diff

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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 kNumCallbacksToDiscard = 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 @@ struct LoopbackData {
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 @@ static aaudio_data_callback_result_t MyDataCallbackProc(
}
// 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 aaudio_data_callback_result_t MyDataCallbackProc(
}
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 @@ static void usage() {
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 @@ static aaudio_performance_mode_t parsePerformanceMode(char c) {
}
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 @@ int main(int argc, const char **argv)
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 @@ int main(int argc, const char **argv)
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 @@ int main(int argc, const char **argv)
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 @@ int main(int argc, const char **argv)
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 @@ int main(int argc, const char **argv)
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 @@ int main(int argc, const char **argv)
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 @@ int main(int argc, const char **argv)
}
}
loopbackData.loopbackProcessor->report();
std::cout << "Please wait several seconds for analysis to complete.\n";
std::cout << loopbackData.loopbackProcessor->analyze();
}
{

Loading…
Cancel
Save