• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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 
17 package org.drrickorang.loopback;
18 
19 import android.content.Context;
20 import android.media.AudioManager;
21 import android.os.Build;
22 import android.os.Handler;
23 import android.os.Looper;
24 import android.util.Log;
25 
26 class SoundLevelCalibration {
27     private static final int SECONDS_PER_LEVEL = 1;
28     private static final int MAX_STEPS = 15; // The maximum number of levels that should be tried
29     private static final double CRITICAL_RATIO = 0.41; // Ratio of input over output amplitude at
30                                                        // which the feedback loop neither decays nor
31                                                        // grows (determined experimentally)
32     private static final String TAG = "SoundLevelCalibration";
33 
34     private NativeAudioThread mNativeAudioThread = null;
35     private AudioManager mAudioManager;
36 
37     private SoundLevelChangeListener mChangeListener;
38 
39     abstract static class SoundLevelChangeListener {
40         // used to run the callback on the UI thread
41         private Handler handler = new Handler(Looper.getMainLooper());
42 
onChange(int newLevel)43         abstract void onChange(int newLevel);
44 
go(final int newLevel)45         private void go(final int newLevel) {
46             handler.post(new Runnable() {
47                 @Override
48                 public void run() {
49                     onChange(newLevel);
50                 }
51             });
52         }
53     }
54 
SoundLevelCalibration(int threadType, int samplingRate, int playerBufferSizeInBytes, int recorderBufferSizeInBytes, int micSource, int performanceMode, Context context)55     SoundLevelCalibration(int threadType, int samplingRate, int playerBufferSizeInBytes,
56             int recorderBufferSizeInBytes, int micSource, int performanceMode, Context context) {
57 
58         // TODO: Allow capturing wave data without doing glitch detection.
59         CaptureHolder captureHolder = new CaptureHolder(0, "", false, false, false, context,
60                 samplingRate);
61         // TODO: Run for less than 1 second.
62         mNativeAudioThread = new NativeAudioThread(threadType, samplingRate,
63                 playerBufferSizeInBytes, recorderBufferSizeInBytes, micSource, performanceMode,
64                 Constant.LOOPBACK_PLUG_AUDIO_THREAD_TEST_TYPE_BUFFER_PERIOD, SECONDS_PER_LEVEL,
65                 SECONDS_PER_LEVEL, 0, captureHolder);
66         mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
67     }
68 
69     // TODO: Allow stopping in the middle of calibration
calibrate()70     int calibrate() {
71         final int maxLevel = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
72         int levelBottom = 0;
73         int levelTop = maxLevel + 1;
74 
75         // The ratio of 0.36 seems to correctly calibrate with the Mir dongle on Taimen and Walleye,
76         // but it does not work with the Mir dongle on devices with a 3.5mm jack. Using
77         // CRITICAL_RATIO leads tp a correct calibration when plugging the loopback dongle into
78         // a 3.5mm jack directly.
79         // TODO: Find a better solution that, if possible, doesn't involve querying device names.
80         final double ratio = (Build.DEVICE.equals("walleye")
81                               || Build.DEVICE.equals("taimen")) ? 0.36 : CRITICAL_RATIO;
82 
83         while (levelTop - levelBottom > 1) {
84             int level = (levelBottom + levelTop) / 2;
85             Log.d(TAG, "setting level to " + level);
86             setVolume(level);
87 
88             double amplitude = runAudioThread(mNativeAudioThread);
89             mNativeAudioThread = new NativeAudioThread(mNativeAudioThread); // generate fresh thread
90             Log.d(TAG, "calibrate: at sound level " + level + " volume was " + amplitude);
91 
92             if (amplitude < Constant.SINE_WAVE_AMPLITUDE * ratio) {
93                 levelBottom = level;
94             } else {
95                 levelTop = level;
96             }
97         }
98         // At this point, levelBottom has the highest proper value, if there is one (0 otherwise)
99         Log.d(TAG, "Final level: " + levelBottom);
100         setVolume(levelBottom);
101         return levelBottom;
102     }
103 
runAudioThread(NativeAudioThread thread)104     private double runAudioThread(NativeAudioThread thread) {
105         // runs the native audio thread and returns the average amplitude
106         thread.start();
107         try {
108             thread.join();
109         } catch (InterruptedException e) {
110             e.printStackTrace();
111         }
112         double[] data = thread.getWaveData();
113         return averageAmplitude(data);
114     }
115 
116     // TODO: Only gives accurate results for an undistorted sine wave. Check for distortion.
117     // TODO move to audio_utils
averageAmplitude(double[] data)118     private static double averageAmplitude(double[] data) {
119         if (data == null || data.length == 0) {
120             return 0; // no data is present
121         }
122         double sumSquare = 0;
123         for (double x : data) {
124             sumSquare += x * x;
125         }
126         return Math.sqrt(2.0 * sumSquare / data.length); // amplitude of the sine wave
127     }
128 
setVolume(int level)129     private void setVolume(int level) {
130         mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, level, 0);
131         if (mChangeListener != null) {
132             mChangeListener.go(level);
133         }
134     }
135 
setChangeListener(SoundLevelChangeListener changeListener)136     void setChangeListener(SoundLevelChangeListener changeListener) {
137         mChangeListener = changeListener;
138     }
139 }
140