xref: /aosp_15_r20/cts/apps/CtsVerifier/src/com/android/cts/verifier/audio/analyzers/BaseSineAnalyzer.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
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