1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.cts.verifier.audio.analyzers; 17 18 import java.util.Random; 19 20 /** 21 * Output a steady sine wave and analyze the return signal. 22 * 23 * Use a cosine transform to measure the predicted magnitude and relative phase of the 24 * looped back sine wave. Then generate a predicted signal and compare with the actual signal. 25 * 26 * Derived from oboetester::BaseSineAnalyzer 27 */ 28 public class BaseSineAnalyzer implements SignalAnalyzer { 29 @SuppressWarnings("unused") 30 static final String TAG = "BaseSineAnalyzer"; 31 32 int mSinePeriod = 1; // this will be set before use 33 double mInverseSinePeriod = 1.0; 34 double mPhaseIncrement = 0.0; 35 double mOutputPhase = 0.0; 36 double mOutputAmplitude = 0.75; 37 double mPreviousPhaseOffset = 0.0; 38 double mPhaseTolerance = 2 * Math.PI / 48; 39 40 double mMagnitude = 0.0; 41 double mMaxMagnitude = 0.0; 42 43 double mPhaseErrorSum; 44 double mPhaseErrorCount; 45 double mPhaseJitter = 0.0; 46 47 int mPhaseCount = 0; 48 49 // If this jumps around then we are probably just hearing noise. 50 double mPhaseOffset = 0.0; 51 int mFramesAccumulated = 0; 52 double mSinAccumulator = 0.0; 53 double mCosAccumulator = 0.0; 54 double mScaledTolerance = 0.0; 55 double mTolerance = 0.10; // scaled from 0.0 to 1.0 56 int mInputChannel = 0; 57 int mOutputChannel = 0; 58 59 static final int DEFAULT_SAMPLERATE = 48000; 60 static final int MILLIS_PER_SECOND = 1000; // by definition 61 static final int MAX_LATENCY_MILLIS = 1000; // arbitrary and generous 62 static final int TARGET_GLITCH_FREQUENCY = 1000; 63 static final double MIN_REQUIRED_MAGNITUDE = 0.001; 64 static final int TYPICAL_SAMPLE_RATE = 48000; 65 static final double MAX_SINE_FREQUENCY = 1000.0; 66 static final double FRAMES_PER_CYCLE = TYPICAL_SAMPLE_RATE / MAX_SINE_FREQUENCY; 67 static final double PHASE_PER_BIN = 2.0 * Math.PI / FRAMES_PER_CYCLE; 68 static final double MAX_ALLOWED_JITTER = 2.0 * PHASE_PER_BIN; 69 // Start by failing then let good results drive us into a pass value. 70 static final double INITIAL_JITTER = 2.0 * MAX_ALLOWED_JITTER; 71 // A coefficient of 0.0 is no filtering. 0.9999 is extreme low pass. 72 static final double JITTER_FILTER_COEFFICIENT = 0.8; 73 74 int mSampleRate = DEFAULT_SAMPLERATE; 75 76 double mNoiseAmplitude = 0.00; // Used to experiment with warbling caused by DRC. 77 Random mWhiteNoise = new Random(); 78 79 MagnitudePhase mMagPhase = new MagnitudePhase(); 80 81 InfiniteRecording mInfiniteRecording = new InfiniteRecording(10 * 48000); 82 83 enum RESULT_CODE { 84 RESULT_OK, 85 ERROR_NOISY, 86 ERROR_VOLUME_TOO_LOW, 87 ERROR_VOLUME_TOO_HIGH, 88 ERROR_CONFIDENCE, 89 ERROR_INVALID_STATE, 90 ERROR_GLITCHES, 91 ERROR_NO_LOCK 92 }; 93 BaseSineAnalyzer()94 public BaseSineAnalyzer() { 95 // Add a little bit of noise to reduce blockage by speaker protection and DRC. 96 mNoiseAmplitude = 0.02; 97 }; 98 getSampleRate()99 public int getSampleRate() { 100 return mSampleRate; 101 } 102 103 /** 104 * Set the assumed sample rate for the analysis 105 * @param sampleRate 106 */ setSampleRate(int sampleRate)107 public void setSampleRate(int sampleRate) { 108 mSampleRate = sampleRate; 109 updatePhaseIncrement(); 110 } 111 112 /** 113 * @return output frequency that will have an integer period on input 114 */ getAdjustedFrequency()115 public double getAdjustedFrequency() { 116 updatePhaseIncrement(); 117 return mInverseSinePeriod * getSampleRate(); 118 } 119 setInputChannel(int inputChannel)120 public void setInputChannel(int inputChannel) { 121 mInputChannel = inputChannel; 122 } 123 getInputChannel()124 public int getInputChannel() { 125 return mInputChannel; 126 } 127 setOutputChannel(int outputChannel)128 public void setOutputChannel(int outputChannel) { 129 mOutputChannel = outputChannel; 130 } 131 getOutputChannel()132 public int getOutputChannel() { 133 return mOutputChannel; 134 } 135 setNoiseAmplitude(double noiseAmplitude)136 public void setNoiseAmplitude(double noiseAmplitude) { 137 mNoiseAmplitude = noiseAmplitude; 138 } 139 getNoiseAmplitude()140 public double getNoiseAmplitude() { 141 return mNoiseAmplitude; 142 } 143 setMagnitude(double magnitude)144 void setMagnitude(double magnitude) { 145 mMagnitude = magnitude; 146 mScaledTolerance = mMagnitude * mTolerance; 147 } 148 getTolerance()149 public double getTolerance() { 150 return mTolerance; 151 } 152 setTolerance(double tolerance)153 public void setTolerance(double tolerance) { 154 mTolerance = tolerance; 155 } 156 getMagnitude()157 public double getMagnitude() { 158 return mMagnitude; 159 } 160 getMaxMagnitude()161 public double getMaxMagnitude() { 162 return mMaxMagnitude; 163 } 164 getPhaseOffset()165 public double getPhaseOffset() { 166 return mPhaseOffset; 167 } 168 getOutputPhase()169 public double getOutputPhase() { 170 return mOutputPhase; 171 } 172 getPhaseJitter()173 public double getPhaseJitter() { 174 return mPhaseJitter; 175 } 176 177 // reset the sine wave detector resetAccumulator()178 void resetAccumulator() { 179 mFramesAccumulated = 0; 180 mSinAccumulator = 0.0; 181 mCosAccumulator = 0.0; 182 } 183 184 /** 185 * Get the audio data recorded during the analysis. 186 * @return recorded data 187 */ getRecordedData()188 public float[] getRecordedData() { 189 return mInfiniteRecording.readAll(); 190 } 191 192 class MagnitudePhase { 193 public double mMagnitude; 194 public double mPhase; 195 } 196 197 /** 198 * Calculate the magnitude of the component of the input signal 199 * that matches the analysis frequency. 200 * Also calculate the phase that we can use to create a 201 * signal that matches that component. 202 * The phase will be between -PI and +PI. 203 */ calculateMagnitudePhase(MagnitudePhase magphase)204 double calculateMagnitudePhase(MagnitudePhase magphase) { 205 if (mFramesAccumulated == 0) { 206 return 0.0; 207 } 208 double sinMean = mSinAccumulator / mFramesAccumulated; 209 double cosMean = mCosAccumulator / mFramesAccumulated; 210 211 double magnitude = 2.0 * Math.sqrt((sinMean * sinMean) + (cosMean * cosMean)); 212 magphase.mPhase = Math.atan2(cosMean, sinMean); 213 return magphase.mMagnitude = magnitude; 214 } 215 216 // advance and wrap phase incrementOutputPhase()217 void incrementOutputPhase() { 218 mOutputPhase += mPhaseIncrement; 219 if (mOutputPhase > Math.PI) { 220 mOutputPhase -= (2.0 * Math.PI); 221 } 222 } 223 calculatePhaseError(double p1, double p2)224 double calculatePhaseError(double p1, double p2) { 225 double diff = p1 - p2; 226 // Wrap around the circle. 227 while (diff > Math.PI) { 228 diff -= 2 * Math.PI; 229 } 230 while (diff < -Math.PI) { 231 diff += 2 * Math.PI; 232 } 233 return diff; 234 } 235 getAveragePhaseError()236 double getAveragePhaseError() { 237 // If we have no measurements then return maximum possible phase jitter 238 // to avoid dividing by zero. 239 return (mPhaseErrorCount > 0) ? (mPhaseErrorSum / mPhaseErrorCount) : Math.PI; 240 } 241 242 /** 243 * Perform sin/cos analysis on each sample. 244 * Measure magnitude and phase on every period. 245 * @param sample 246 * @param referencePhase 247 * @return true if magnitude and phase updated 248 */ transformSample(float sample, double referencePhase)249 boolean transformSample(float sample, double referencePhase) { 250 // Track incoming signal and slowly adjust magnitude to account 251 // for drift in the DRC or AGC. 252 mSinAccumulator += ((double) sample) * Math.sin(referencePhase); 253 mCosAccumulator += ((double) sample) * Math.cos(referencePhase); 254 mFramesAccumulated++; 255 256 incrementOutputPhase(); 257 258 // Must be a multiple of the period or the calculation will not be accurate. 259 if (mFramesAccumulated == mSinePeriod) { 260 final double coefficient = 0.1; 261 262 double magnitude = calculateMagnitudePhase(mMagPhase); 263 mPhaseOffset = mMagPhase.mPhase; 264 // One pole averaging filter. 265 setMagnitude((mMagnitude * (1.0 - coefficient)) + (magnitude * coefficient)); 266 return true; 267 } else { 268 return false; 269 } 270 } 271 272 /** 273 * @param audioData contains microphone data with sine signal feedback 274 * @param offset in the audioData to the sample 275 */ processInputFrame(float[] audioData, int offset)276 RESULT_CODE processInputFrame(float[] audioData, int offset) { 277 RESULT_CODE result = RESULT_CODE.RESULT_OK; 278 279 float sample = audioData[offset]; 280 mInfiniteRecording.write(sample); 281 282 if (transformSample(sample, mOutputPhase)) { 283 resetAccumulator(); 284 if (mMagnitude >= MIN_REQUIRED_MAGNITUDE) { 285 // Analyze magnitude and phase on every period. 286 double phaseError = 287 Math.abs(calculatePhaseError(mPhaseOffset, mPreviousPhaseOffset)); 288 if (phaseError < mPhaseTolerance) { 289 mMaxMagnitude = Math.max(mMagnitude, mMaxMagnitude); 290 } 291 mPreviousPhaseOffset = mPhaseOffset; 292 293 // Only look at the phase if we have a signal. 294 if (mPhaseCount > 3) { 295 // Accumulate phase error and average. 296 mPhaseErrorSum += phaseError; 297 mPhaseErrorCount++; 298 mPhaseJitter = getAveragePhaseError(); 299 } 300 301 mPhaseCount++; 302 } 303 } 304 return result; 305 } 306 updatePhaseIncrement()307 private void updatePhaseIncrement() { 308 mSinePeriod = getSampleRate() / TARGET_GLITCH_FREQUENCY; 309 mInverseSinePeriod = 1.0 / mSinePeriod; 310 mPhaseIncrement = 2.0 * Math.PI * mInverseSinePeriod; 311 } 312 313 @Override reset()314 public void reset() { 315 resetAccumulator(); 316 317 mOutputPhase = 0.0f; 318 mMagnitude = 0.0; 319 mMaxMagnitude = 0.0; 320 mPhaseOffset = 0.0; 321 mPreviousPhaseOffset = 0.0; 322 mPhaseJitter = INITIAL_JITTER; 323 mPhaseCount = 0; 324 mPhaseErrorSum = 0.0; 325 mPhaseErrorCount = 0.0; 326 327 updatePhaseIncrement(); 328 329 mInfiniteRecording.clear(); 330 } 331 332 @Override analyzeBuffer(float[] audioData, int numChannels, int numFrames)333 public void analyzeBuffer(float[] audioData, int numChannels, int numFrames) { 334 int offset = mInputChannel; 335 for (int frameIndex = 0; frameIndex < numFrames; frameIndex++) { 336 // processOutputFrame(audioData, offset, numChannels); 337 processInputFrame(audioData, offset); 338 offset += numChannels; 339 } 340 341 // // Only look at the phase if we have a signal. 342 // if (mMagnitude >= MIN_REQUIRED_MAGNITUDE) { 343 // double phase = mPhaseOffset; 344 // if (mPhaseCount > 3) { 345 // double phaseError = calculatePhaseError(phase, mPhaseOffset); 346 // // Accumulate phase error and average. 347 // mPhaseErrorSum += phaseError; 348 // mPhaseErrorCount++; 349 // mPhaseJitter = getAveragePhaseError(); 350 // } 351 // 352 // mPhaseOffset = phase; 353 // mPhaseCount++; 354 // } 355 } 356 } 357