Skip to content

Commit

Permalink
Improve glitch tests (#1956)
Browse files Browse the repository at this point in the history
There are often glitches counted in the manual Glitch test that are not visible or audible!

Those "glitches" were caused by slowly drifting sample rates.
Apparently input and output may be on different clocks!
The fix was to continuously adjust the phase of the reference sine wave
so that it tracks the incoming signal. This is like a "phase locked loop".

Also:
* Improve display of glitches, add cursor
* Add "Auto draw" checkbox
* Add "Force glitch" checkbox
* Replace abs() with fabs() calls.
* Bump OboeTester version to 2.5.9
  • Loading branch information
philburk authored Feb 6, 2024
1 parent db4c694 commit 5b2a750
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 69 deletions.
4 changes: 2 additions & 2 deletions apps/OboeTester/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ android {
applicationId = "com.mobileer.oboetester"
minSdkVersion 23
targetSdkVersion 34
versionCode 78
versionName "2.5.7"
versionCode 80
versionName "2.5.9"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
Expand Down
2 changes: 1 addition & 1 deletion apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ oboe::DataCallbackResult FullDuplexAnalyzer::onBothStreamsReadyFloat(
inputFloat += inputStride;
mRecording->write(buffer, 1);
}
// Handle mismatch in in numFrames.
// Handle mismatch in numFrames.
buffer[0] = 0.0f; // gap in output
for (int i = numBoth; i < numInputFrames; i++) {
buffer[1] = *inputFloat;
Expand Down
4 changes: 2 additions & 2 deletions apps/OboeTester/app/src/main/cpp/analyzer/BaseSineAnalyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ class BaseSineAnalyzer : public LoopbackProcessor {
: LoopbackProcessor()
, mInfiniteRecording(64 * 1024) {}


virtual bool isOutputEnabled() { return true; }

void setMagnitude(double magnitude) {
Expand Down Expand Up @@ -185,7 +184,8 @@ class BaseSineAnalyzer : public LoopbackProcessor {
}

protected:
static constexpr int32_t kTargetGlitchFrequency = 1000;
// Try to get a prime period so the waveform plot changes every time.
static constexpr int32_t kTargetGlitchFrequency = 48000 / 113;

int32_t mSinePeriod = 1; // this will be set before use
double mInverseSinePeriod = 1.0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ class DataPathAnalyzer : public BaseSineAnalyzer {

if (transformSample(sample, mOutputPhase)) {
// Analyze magnitude and phase on every period.
double diff = abs(calculatePhaseError(mPhaseOffset, mPreviousPhaseOffset));
double diff = fabs(calculatePhaseError(mPhaseOffset, mPreviousPhaseOffset));
if (diff < mPhaseTolerance) {
mMaxMagnitude = std::max(mMagnitude, mMaxMagnitude);
}
Expand Down
135 changes: 98 additions & 37 deletions apps/OboeTester/app/src/main/cpp/analyzer/GlitchAnalyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,22 @@ class GlitchAnalyzer : public BaseSineAnalyzer {
return mMagnitude;
}

int getSinePeriod() const {
return mSinePeriod;
}

float getPhaseOffset() const {
return mPhaseOffset;
}

int32_t getGlitchCount() const {
return mGlitchCount;
}

int32_t getGlitchLength() const {
return mGlitchLength;
}

int32_t getStateFrameCount(int state) const {
return mStateFrameCounters[state];
}
Expand Down Expand Up @@ -124,21 +136,22 @@ class GlitchAnalyzer : public BaseSineAnalyzer {
result_code result = RESULT_OK;

float sample = frameData[getInputChannel()];
float peak = mPeakFollower.process(sample);
mInfiniteRecording.write(sample);

// Force a periodic glitch to test the detector!
if (mForceGlitchDuration > 0) {
if (mForceGlitchDurationFrames > 0) {
if (mForceGlitchCounter == 0) {
ALOGE("%s: force a glitch!!", __func__);
mForceGlitchCounter = getSampleRate();
} else if (mForceGlitchCounter <= mForceGlitchDuration) {
ALOGE("%s: finish a glitch!!", __func__);
mForceGlitchCounter = kForceGlitchPeriod;
} else if (mForceGlitchCounter <= mForceGlitchDurationFrames) {
// Force an abrupt offset.
sample += (sample > 0.0) ? -0.5f : 0.5f;
sample += (sample > 0.0) ? -kForceGlitchOffset : kForceGlitchOffset;
}
--mForceGlitchCounter;
}

float peak = mPeakFollower.process(sample);
mInfiniteRecording.write(sample);

mStateFrameCounters[mState]++; // count how many frames we are in each state

switch (mState) {
Expand Down Expand Up @@ -178,8 +191,9 @@ class GlitchAnalyzer : public BaseSineAnalyzer {
// ALOGD("%s() mag = %f, offset = %f, prev = %f",
// __func__, mMagnitude, mPhaseOffset, mPreviousPhaseOffset);
if (mMagnitude > mThreshold) {
if (abs(mPhaseOffset) < kMaxPhaseError) {
if (fabs(mPhaseOffset) < kMaxPhaseError) {
mState = STATE_LOCKED;
mConsecutiveBadFrames = 0;
// ALOGD("%5d: switch to STATE_LOCKED", mFrameCounter);
}
// Adjust mInputPhase to match measured phase
Expand All @@ -196,24 +210,36 @@ class GlitchAnalyzer : public BaseSineAnalyzer {
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 {
if (absDiff > mScaledTolerance) { // bad frame
mConsecutiveBadFrames++;
mConsecutiveGoodFrames = 0;
LOGI("diff glitch frame #%d detected, absDiff = %g > %g",
mConsecutiveBadFrames, absDiff, mScaledTolerance);
if (mConsecutiveBadFrames > 0) {
result = ERROR_GLITCHES;
onGlitchStart();
}
resetAccumulator();
} else { // good frame
mConsecutiveBadFrames = 0;
mConsecutiveGoodFrames++;

mSumSquareSignal += predicted * predicted;
mSumSquareNoise += diff * diff;

// Track incoming signal and slowly adjust magnitude to account
// for drift in the DRC or AGC.
// Must be a multiple of the period or the calculation will not be accurate.
if (transformSample(sample, mInputPhase)) {
// Adjust phase to account for sample rate drift.
mInputPhase += mPhaseOffset;

mMeanSquareNoise = mSumSquareNoise * mInverseSinePeriod;
mMeanSquareSignal = mSumSquareSignal * mInverseSinePeriod;
mSumSquareNoise = 0.0;
mSumSquareSignal = 0.0;

if (abs(mPhaseOffset) > kMaxPhaseError) {
if (fabs(mPhaseOffset) > kMaxPhaseError) {
result = ERROR_GLITCHES;
onGlitchStart();
ALOGD("phase glitch detected, phaseOffset = %g", mPhaseOffset);
Expand All @@ -229,22 +255,25 @@ class GlitchAnalyzer : public BaseSineAnalyzer {

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.
if (absDiff > mScaledTolerance) { // bad frame
mConsecutiveBadFrames++;
mConsecutiveGoodFrames = 0;
mGlitchLength++;
if (mGlitchLength > maxMeasurableGlitchLength()) {
onGlitchTerminated();
}
} else { // good frame
mConsecutiveBadFrames = 0;
mConsecutiveGoodFrames++;
// If we get a full sine period of good 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) {
if (mConsecutiveGoodFrames > mSinePeriod) {
onGlitchEnd();
}
} else {
mNonGlitchCount = 0;
if (mGlitchLength > (4 * mSinePeriod)) {
relock();
}
}
incrementInputPhase();
} break;
Expand All @@ -258,6 +287,8 @@ class GlitchAnalyzer : public BaseSineAnalyzer {
return result;
}

int maxMeasurableGlitchLength() const { return 2 * mSinePeriod; }

// advance and wrap phase
void incrementInputPhase() {
mInputPhase += mPhaseIncrement;
Expand All @@ -269,16 +300,29 @@ class GlitchAnalyzer : public BaseSineAnalyzer {
bool isOutputEnabled() override { return mState != STATE_IDLE; }

void onGlitchStart() {
mGlitchCount++;
// ALOGD("%5d: STARTED a glitch # %d", mFrameCounter, mGlitchCount);
mState = STATE_GLITCHING;
mGlitchLength = 1;
mNonGlitchCount = 0;
mLastGlitchPosition = mInfiniteRecording.getTotalWritten();
ALOGD("%5d: STARTED a glitch # %d, pos = %5d",
mFrameCounter, mGlitchCount, (int)mLastGlitchPosition);
ALOGD("glitch mSinePeriod = %d", mSinePeriod);
}

/**
* Give up waiting for a glitch to end and try to resync.
*/
void onGlitchTerminated() {
mGlitchCount++;
ALOGD("%5d: TERMINATED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength);
// We don't know how long the glitch really is so set the length to -1.
mGlitchLength = -1;
mState = STATE_WAITING_FOR_LOCK;
resetAccumulator();
}

void onGlitchEnd() {
// ALOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength);
mGlitchCount++;
ALOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength);
mState = STATE_LOCKED;
resetAccumulator();
}
Expand All @@ -288,12 +332,6 @@ class GlitchAnalyzer : public BaseSineAnalyzer {
BaseSineAnalyzer::resetAccumulator();
}

void relock() {
// ALOGD("relock: %d because of a very long %d glitch", mFrameCounter, mGlitchLength);
mState = STATE_WAITING_FOR_LOCK;
resetAccumulator();
}

void reset() override {
BaseSineAnalyzer::reset();
mState = STATE_IDLE;
Expand All @@ -303,14 +341,34 @@ class GlitchAnalyzer : public BaseSineAnalyzer {
void prepareToTest() override {
BaseSineAnalyzer::prepareToTest();
mGlitchCount = 0;
mGlitchLength = 0;
mMaxGlitchDelta = 0.0;
for (int i = 0; i < NUM_STATES; i++) {
mStateFrameCounters[i] = 0;
}
}

int32_t getLastGlitch(float *buffer, int32_t length) {
return mInfiniteRecording.readFrom(buffer, mLastGlitchPosition - 32, length);
const int margin = mSinePeriod;
int32_t numSamples = mInfiniteRecording.readFrom(buffer,
mLastGlitchPosition - margin,
length);
ALOGD("%s: glitch at %d, edge = %7.4f, %7.4f, %7.4f",
__func__, (int)mLastGlitchPosition,
buffer[margin - 1], buffer[margin], buffer[margin+1]);
return numSamples;
}

int32_t getRecentSamples(float *buffer, int32_t length) {
int firstSample = mInfiniteRecording.getTotalWritten() - length;
int32_t numSamples = mInfiniteRecording.readFrom(buffer,
firstSample,
length);
return numSamples;
}

void setForcedGlitchDuration(int frames) {
mForceGlitchDurationFrames = frames;
}

private:
Expand Down Expand Up @@ -345,13 +403,16 @@ class GlitchAnalyzer : public BaseSineAnalyzer {
double mInputPhase = 0.0;
double mMaxGlitchDelta = 0.0;
int32_t mGlitchCount = 0;
int32_t mNonGlitchCount = 0;
int32_t mConsecutiveBadFrames = 0;
int32_t mConsecutiveGoodFrames = 0;
int32_t mGlitchLength = 0;
int mDownCounter = IDLE_FRAME_COUNT;
int32_t mFrameCounter = 0;

int32_t mForceGlitchDuration = 0; // if > 0 then force a glitch for debugging
int32_t mForceGlitchCounter = 4 * 48000; // count down and trigger at zero
int32_t mForceGlitchDurationFrames = 0; // if > 0 then force a glitch for debugging
static constexpr int32_t kForceGlitchPeriod = 2 * 48000; // How often we glitch
static constexpr float kForceGlitchOffset = 0.20f;
int32_t mForceGlitchCounter = kForceGlitchPeriod; // count down and trigger at zero

// measure background noise continuously as a deviation from the expected signal
double mSumSquareSignal = 0.0;
Expand Down
5 changes: 3 additions & 2 deletions apps/OboeTester/app/src/main/cpp/analyzer/InfiniteRecording.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class InfiniteRecording {
int32_t readFrom(T *buffer, size_t position, size_t count) {
const size_t maxPosition = mWritten.load();
position = std::min(position, maxPosition);

size_t numToRead = std::min(count, mMaxSamples);
numToRead = std::min(numToRead, maxPosition - position);
if (numToRead == 0) return 0;
Expand Down Expand Up @@ -61,7 +62,7 @@ class InfiniteRecording {

private:
std::unique_ptr<T[]> mData;
std::atomic<size_t> mWritten{0};
const size_t mMaxSamples;
std::atomic<size_t> mWritten{0};
const size_t mMaxSamples;
};
#endif //OBOETESTER_INFINITE_RECORDING_H
4 changes: 2 additions & 2 deletions apps/OboeTester/app/src/main/cpp/analyzer/LatencyAnalyzer.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ class AudioRecording
float normalize(float target) {
float maxValue = 1.0e-9f;
for (int i = 0; i < mFrameCounter; i++) {
maxValue = std::max(maxValue, abs(mData[i]));
maxValue = std::max(maxValue, fabsf(mData[i]));
}
float gain = target / maxValue;
for (int i = 0; i < mFrameCounter; i++) {
Expand Down Expand Up @@ -263,7 +263,7 @@ static int measureLatencyFromPulsePartial(AudioRecording &recorded,
float peakCorrelation = 0.0;
int32_t peakIndex = -1;
for (int32_t i = 0; i < numCorrelations; i++) {
float value = abs(correlations[i]);
float value = fabsf(correlations[i]);
if (value > peakCorrelation) {
peakCorrelation = value;
peakIndex = i;
Expand Down
Loading

0 comments on commit 5b2a750

Please sign in to comment.