| /* |
| * 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 |