| /* |
| * 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 OBOETESTER_LATENCY_ANALYSER_H |
| #define OBOETESTER_LATENCY_ANALYSER_H |
| |
| #include <algorithm> |
| #include <assert.h> |
| #include <cctype> |
| #include <math.h> |
| #include <memory> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <unistd.h> |
| #include <vector> |
| |
| #include "RandomPulseGenerator.h" |
| |
| #define LOOPBACK_RESULT_TAG "RESULT: " |
| |
| constexpr int32_t kDefaultSampleRate = 48000; |
| constexpr int32_t kMillisPerSecond = 1000; |
| |
| constexpr int32_t kMaxLatencyMillis = 700; // arbitrary and generous |
| constexpr double kMinimumConfidence = 0.2; |
| |
| 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() { |
| #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 = 99887766; |
| }; |
| |
| class PeakFollower { |
| public: |
| float process(float input) { |
| mPrevious *= kDecayCoefficient; |
| const float positive = abs(input); |
| if (positive > mPrevious) { |
| mPrevious = positive; |
| } |
| return mPrevious; |
| } |
| |
| float get() { |
| return mPrevious; |
| } |
| |
| private: |
| static constexpr float kDecayCoefficient = 0.99f; |
| float mPrevious = 0.0f; |
| }; |
| |
| typedef struct LatencyReport_s { |
| int32_t latencyInFrames = 0.0; |
| double confidence = 0.0; |
| |
| void reset() { |
| latencyInFrames = 0; |
| confidence = 0.0; |
| } |
| } LatencyReport; |
| |
| // 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 = (float) (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: |
| 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; |
| } |
| |
| // 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; |
| } |
| |
| void clear() { |
| mFrameCounter = 0; |
| } |
| int32_t size() { |
| return mFrameCounter; |
| } |
| |
| bool isFull() { |
| return mFrameCounter >= mMaxFrames; |
| } |
| |
| 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) { |
| LOGD("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) { |
| LOGD("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; |
| } |
| } |
| |
| /** |
| * 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: |
| float *mData = nullptr; |
| int32_t mFrameCounter = 0; |
| int32_t mMaxFrames = 0; |
| int32_t mSampleRate = kDefaultSampleRate; // common default |
| }; |
| |
| static int measureLatencyFromPulse(AudioRecording &recorded, |
| AudioRecording &pulse, |
| int32_t framesPerEncodedBit, |
| LatencyReport *report) { |
| |
| report->latencyInFrames = 0; |
| report->confidence = 0.0; |
| |
| int numCorrelations = recorded.size() - pulse.size(); |
| if (numCorrelations < 10) { |
| LOGE("%s() recording too small = %d frames", __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) { |
| LOGE("%s() no signal for correlation", __func__); |
| return -2; |
| } |
| |
| report->latencyInFrames = peakIndex; |
| report->confidence = peakCorrelation; |
| |
| return 0; |
| } |
| |
| // ==================================================================================== |
| class LoopbackProcessor { |
| public: |
| virtual ~LoopbackProcessor() = default; |
| |
| // Note that these values must match the switch in RoundTripLatencyActivity.h |
| 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 onStartTest() { |
| 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; |
| |
| result_code process(float *inputData, int inputChannelCount, int numInputFrames, |
| float *outputData, int outputChannelCount, int numOutputFrames) { |
| result_code result = RESULT_OK; |
| int numBoth = std::min(numInputFrames, numOutputFrames); |
| // Process one frame at a time. |
| for (int i = 0; i < numBoth; i++) { |
| result = processInputFrame(inputData, inputChannelCount); |
| inputData += inputChannelCount; |
| if (result != RESULT_OK) return result; |
| result = processOutputFrame(outputData, outputChannelCount); |
| outputData += outputChannelCount; |
| if (result != RESULT_OK) return result; |
| } |
| for (int i = numBoth; i < numInputFrames; i++) { |
| result = processInputFrame(inputData, inputChannelCount); |
| inputData += inputChannelCount; |
| if (result != RESULT_OK) return result; |
| } |
| for (int i = numBoth; i < numOutputFrames; i++) { |
| result = processOutputFrame(outputData, outputChannelCount); |
| outputData += outputChannelCount; |
| if (result != RESULT_OK) return result; |
| } |
| return result; |
| } |
| |
| virtual void 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() { |
| return mSampleRate; |
| } |
| |
| int32_t getResetCount() { |
| 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 PeakAnalyzer { |
| 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; |
| }; |
| */ |
| |
| class LatencyAnalyzer : public LoopbackProcessor { |
| public: |
| |
| LatencyAnalyzer() : LoopbackProcessor() {} |
| virtual ~LatencyAnalyzer() = default; |
| |
| virtual int32_t getProgress() = 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; |
| LOGD("PulseLatencyAnalyzer: allocate recording with %d frames", mFramesToRecord); |
| 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; |
| |
| LOGD("state reset to STATE_MEASURE_BACKGROUND"); |
| mState = STATE_MEASURE_BACKGROUND; |
| mAudioRecording.clear(); |
| mLatencyReport.reset(); |
| } |
| |
| bool hasEnoughData() { |
| return mAudioRecording.isFull(); |
| } |
| |
| bool isDone() override { |
| return mState == STATE_DONE; |
| } |
| |
| int32_t getProgress() override { |
| return mAudioRecording.size(); |
| } |
| |
| void analyze() override { |
| LOGD("PulseLatencyAnalyzer ---------------"); |
| LOGD(LOOPBACK_RESULT_TAG "test.state = %8d", mState); |
| LOGD(LOOPBACK_RESULT_TAG "test.state.name = %8s", convertStateToText(mState)); |
| LOGD(LOOPBACK_RESULT_TAG "background.rms = %8f", mBackgroundRMS); |
| |
| int32_t newResult = RESULT_OK; |
| if (mState != STATE_GOT_DATA) { |
| LOGD("WARNING - Bad state. Check volume on device."); |
| // setResult(ERROR_INVALID_STATE); |
| } else { |
| LOGD("Please wait several seconds for cross-correlation to complete."); |
| float gain = mAudioRecording.normalize(1.0f); |
| measureLatencyFromPulse(mAudioRecording, |
| mPulse, |
| kFramesPerEncodedBit, |
| &mLatencyReport); |
| |
| if (mLatencyReport.confidence < kMinimumConfidence) { |
| LOGD(" ERROR - confidence too low!"); |
| newResult = ERROR_CONFIDENCE; |
| } else { |
| mSignalRMS = calculateRootMeanSquare( |
| &mAudioRecording.getData()[mLatencyReport.latencyInFrames], mPulse.size()) |
| / gain; |
| } |
| #if OBOE_ENABLE_LOGGING |
| double latencyMillis = kMillisPerSecond * (double) mLatencyReport.latencyInFrames |
| / getSampleRate(); |
| #endif |
| LOGD(LOOPBACK_RESULT_TAG "latency.frames = %8d", |
| mLatencyReport.latencyInFrames); |
| LOGD(LOOPBACK_RESULT_TAG "latency.msec = %8.2f", |
| latencyMillis); |
| LOGD(LOOPBACK_RESULT_TAG "latency.confidence = %8.6f", |
| mLatencyReport.confidence); |
| } |
| mState = STATE_DONE; |
| if (getResult() == RESULT_OK) { |
| setResult(newResult); |
| } |
| } |
| |
| 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 { |
| LOGD("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; |
| LOGD("LatencyAnalyzer state => STATE_SENDING_PULSE"); |
| } |
| break; |
| |
| case STATE_IN_PULSE: |
| // Record input until the mAudioRecording is full. |
| mAudioRecording.write(frameData, channelCount, 1); |
| if (hasEnoughData()) { |
| LOGD("LatencyAnalyzer state => STATE_GOT_DATA"); |
| 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) { |
| const char *result = "Unknown"; |
| switch(state) { |
| case STATE_MEASURE_BACKGROUND: |
| result = "INIT"; |
| break; |
| case STATE_IN_PULSE: |
| result = "PULSE"; |
| break; |
| case STATE_GOT_DATA: |
| result = "GOT_DATA"; |
| break; |
| case STATE_DONE: |
| result = "DONE"; |
| break; |
| } |
| return result; |
| } |
| |
| 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; |
| |
| float mBackgroundSumSquare = 0.0f; |
| int32_t mBackgroundSumCount = 0; |
| float mBackgroundRMS = 0.0f; |
| float mSignalRMS = 0.0f; |
| int32_t mFramesToRecord = 0; |
| |
| AudioRecording mAudioRecording; // contains only the input after starting the pulse |
| LatencyReport mLatencyReport; |
| }; |
| |
| // ==================================================================================== |
| /** |
| * 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 GlitchAnalyzer : public LoopbackProcessor { |
| public: |
| |
| int32_t getState() { |
| return mState; |
| } |
| |
| float getPeakAmplitude() { |
| return mPeakFollower.get(); |
| } |
| |
| float getTolerance() { |
| return mTolerance; |
| } |
| |
| void setTolerance(float tolerance) { |
| mTolerance = tolerance; |
| mScaledTolerance = mMagnitude * mTolerance; |
| } |
| |
| void setMagnitude(double magnitude) { |
| mMagnitude = magnitude; |
| mScaledTolerance = mMagnitude * mTolerance; |
| } |
| |
| int32_t getGlitchCount() { |
| return mGlitchCount; |
| } |
| |
| int32_t getStateFrameCount(int state) { |
| 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_SNRATIO_DB) { |
| LOGD("ERROR - signal to noise ratio is too low! < %d dB. Adjust volume.", |
| MIN_SNRATIO_DB); |
| setResult(ERROR_VOLUME_TOO_LOW); |
| } |
| return signalToNoiseDB; |
| } |
| } |
| |
| void analyze() override { |
| LOGD("GlitchAnalyzer ------------------"); |
| LOGD(LOOPBACK_RESULT_TAG "peak.amplitude = %8f", getPeakAmplitude()); |
| LOGD(LOOPBACK_RESULT_TAG "sine.magnitude = %8f", mMagnitude); |
| LOGD(LOOPBACK_RESULT_TAG "rms.noise = %8f", mMeanSquareNoise); |
| LOGD(LOOPBACK_RESULT_TAG "signal.to.noise.db = %8.2f", getSignalToNoiseDB()); |
| LOGD(LOOPBACK_RESULT_TAG "frames.accumulated = %8d", mFramesAccumulated); |
| LOGD(LOOPBACK_RESULT_TAG "sine.period = %8d", mSinePeriod); |
| LOGD(LOOPBACK_RESULT_TAG "test.state = %8d", mState); |
| LOGD(LOOPBACK_RESULT_TAG "frame.count = %8d", mFrameCounter); |
| // Did we ever get a lock? |
| bool gotLock = (mState == STATE_LOCKED) || (mGlitchCount > 0); |
| if (!gotLock) { |
| LOGD("ERROR - failed to lock on reference sine tone"); |
| setResult(ERROR_NO_LOCK); |
| } else { |
| // Only print if meaningful. |
| LOGD(LOOPBACK_RESULT_TAG "glitch.count = %8d", mGlitchCount); |
| LOGD(LOOPBACK_RESULT_TAG "max.glitch = %8f", mMaxGlitchDelta); |
| if (mGlitchCount > 0) { |
| LOGD("ERROR - number of glitches > 0"); |
| setResult(ERROR_GLITCHES); |
| } |
| } |
| } |
| |
| void printStatus() override { |
| LOGD("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 frameData contains microphone data with sine signal feedback |
| * @param channelCount |
| * @param outputData contains the reference sine wave |
| */ |
| |
| 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! |
| if (mForceGlitchDuration > 0) { |
| if (mForceGlitchCounter == 0) { |
| LOGE("%s: force a glitch!!", __func__); |
| mForceGlitchCounter = getSampleRate(); |
| } else if (mForceGlitchCounter <= mForceGlitchDuration) { |
| 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_WAITING_FOR_SIGNAL; |
| mDownCounter = NOISE_FRAME_COUNT; |
| 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; |
| //LOGD("%5d: switch to STATE_WAITING_FOR_LOCK", mFrameCounter); |
| resetAccumulator(); |
| } |
| break; |
| |
| case STATE_WAITING_FOR_LOCK: |
| mSinAccumulator += sample * sinf(mPhase); |
| 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; |
| setMagnitude(calculateMagnitude(&mPhaseOffset)); |
| // LOGD("%s() mag = %f, offset = %f, prev = %f", |
| // __func__, mMagnitude, mPhaseOffset, mPreviousPhaseOffset); |
| if (mMagnitude > mThreshold) { |
| if (fabs(mPreviousPhaseOffset - mPhaseOffset) < 0.001) { |
| mState = STATE_LOCKED; |
| // LOGD("%5d: switch to STATE_LOCKED", mFrameCounter); |
| } |
| mPreviousPhaseOffset = mPhaseOffset; |
| } |
| resetAccumulator(); |
| } |
| break; |
| |
| case STATE_LOCKED: { |
| // Predict next sine value |
| float predicted = sinf(mPhase + mPhaseOffset) * mMagnitude; |
| // LOGD(" predicted = %f, actual = %f", predicted, sample); |
| |
| float diff = predicted - sample; |
| float absDiff = fabs(diff); |
| mMaxGlitchDelta = std::max(mMaxGlitchDelta, absDiff); |
| if (absDiff > mScaledTolerance) { |
| result = ERROR_GLITCHES; |
| onGlitchStart(); |
| } 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(mPhase); |
| 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. |
| setMagnitude((mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient)); |
| |
| mMeanSquareNoise = mSumSquareNoise * mInverseSinePeriod; |
| mMeanSquareSignal = mSumSquareSignal * mInverseSinePeriod; |
| resetAccumulator(); |
| |
| if (mMagnitude < mThreshold) { |
| result = ERROR_GLITCHES; |
| onGlitchStart(); |
| } |
| } |
| } |
| } break; |
| |
| case STATE_GLITCHING: { |
| // Predict next sine value |
| mGlitchLength++; |
| float predicted = sinf(mPhase + mPhaseOffset) * mMagnitude; |
| float diff = predicted - sample; |
| // LOGD(" STATE_GLITCHING: predicted = %f, actual = %f, length = %d", predicted, sample, mGlitchLength); |
| float 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(); |
| } |
| } |
| } break; |
| |
| case NUM_STATES: // not a real state |
| break; |
| } |
| |
| mFrameCounter++; |
| |
| return result; |
| } |
| |
| /** |
| * @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(mPhase); |
| output = (sinOut * mOutputAmplitude) |
| + (mWhiteNoise.nextRandomDouble() * kNoiseAmplitude); |
| // LOGD("%5d: sin(%f) = %f, %f", i, mPhase, sinOut, mPhaseIncrement); |
| // advance and wrap phase |
| mPhase += mPhaseIncrement; |
| if (mPhase > M_PI) { |
| mPhase -= (2.0 * M_PI); |
| } |
| } |
| |
| frameData[0] = output; |
| for (int i = 1; i < channelCount; i++) { |
| frameData[i] = 0.0f; |
| } |
| return RESULT_OK; |
| } |
| |
| void onGlitchStart() { |
| mGlitchCount++; |
| // LOGD("%5d: STARTED a glitch # %d", mFrameCounter, mGlitchCount); |
| mState = STATE_GLITCHING; |
| mGlitchLength = 1; |
| mNonGlitchCount = 0; |
| } |
| |
| void onGlitchEnd() { |
| // LOGD("%5d: ENDED a glitch # %d, length = %d", mFrameCounter, mGlitchCount, mGlitchLength); |
| mState = STATE_LOCKED; |
| resetAccumulator(); |
| } |
| |
| void onInsufficientRead() override { |
| if (mState == STATE_LOCKED) { |
| mGlitchCount++; |
| } |
| LoopbackProcessor::onInsufficientRead(); |
| } |
| |
| // reset the sine wave detector |
| void resetAccumulator() { |
| mFramesAccumulated = 0; |
| mSinAccumulator = 0.0; |
| mCosAccumulator = 0.0; |
| mSumSquareSignal = 0.0; |
| mSumSquareNoise = 0.0; |
| } |
| |
| void relock() { |
| mState = STATE_WAITING_FOR_LOCK; |
| resetAccumulator(); |
| } |
| |
| void reset() override { |
| LoopbackProcessor::reset(); |
| mState = STATE_IDLE; |
| mDownCounter = IDLE_FRAME_COUNT; |
| resetAccumulator(); |
| } |
| |
| void onStartTest() override { |
| LoopbackProcessor::onStartTest(); |
| mSinePeriod = getSampleRate() / kTargetGlitchFrequency; |
| mPhase = 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, |
| NOISE_FRAME_COUNT = 48 * 600, |
| PERIODS_NEEDED_FOR_LOCK = 8, |
| PERIODS_IMMUNE = 2, |
| MIN_SNRATIO_DB = 65 |
| }; |
| |
| static constexpr float kNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC. |
| static constexpr int kTargetGlitchFrequency = 607; |
| |
| 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 mPhase = 0.0; |
| double mPhaseOffset = 0.0; |
| double mPreviousPhaseOffset = 0.0; |
| double mMagnitude = 0.0; |
| int32_t mFramesAccumulated = 0; |
| double mSinAccumulator = 0.0; |
| double mCosAccumulator = 0.0; |
| float mMaxGlitchDelta = 0.0f; |
| int32_t mGlitchCount = 0; |
| int32_t mNonGlitchCount = 0; |
| int32_t mGlitchLength = 0; |
| float mScaledTolerance = 0.0; |
| int mDownCounter = IDLE_FRAME_COUNT; |
| int32_t mFrameCounter = 0; |
| float 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; |
| |
| PeakFollower mPeakFollower; |
| |
| PseudoRandom mWhiteNoise; |
| |
| sine_state_t mState = STATE_IDLE; |
| }; |
| |
| #undef LOOPBACK_RESULT_TAG |
| |
| #endif // OBOETESTER_LATENCY_ANALYSER_H |