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 17 package android.virtualdevice.cts.common; 18 19 import static android.media.AudioRecord.READ_BLOCKING; 20 import static android.media.AudioRecord.READ_NON_BLOCKING; 21 22 import android.annotation.IntDef; 23 import android.companion.virtual.audio.AudioCapture; 24 import android.media.AudioRecord; 25 26 import java.lang.annotation.Retention; 27 import java.lang.annotation.RetentionPolicy; 28 import java.nio.ByteBuffer; 29 import java.nio.ByteOrder; 30 31 /** 32 * Utility methods for creating and processing audio data. 33 */ 34 public final class AudioHelper { 35 /** Tells the activity to play audio for testing. */ 36 public static final String ACTION_PLAY_AUDIO = "android.virtualdevice.cts.PLAY_AUDIO"; 37 38 /** Tells the activity to record audio for testing. */ 39 public static final String ACTION_RECORD_AUDIO = "android.virtualdevice.cts.RECORD_AUDIO"; 40 41 /** Tells the activity to play or record for which audio data type. */ 42 public static final String EXTRA_AUDIO_DATA_TYPE = "audio_data_type"; 43 44 @IntDef({ 45 BYTE_BUFFER, 46 BYTE_ARRAY, 47 SHORT_ARRAY, 48 FLOAT_ARRAY, 49 }) 50 @Retention(RetentionPolicy.SOURCE) 51 public @interface DataType {} 52 53 public static final int BYTE_BUFFER = 1; 54 public static final int BYTE_ARRAY = 2; 55 public static final int SHORT_ARRAY = 3; 56 public static final int FLOAT_ARRAY = 4; 57 58 /** Values for read and write verification. */ 59 public static final byte BYTE_VALUE = 8; 60 public static final short SHORT_VALUE = 16; 61 public static final float FLOAT_VALUE = 0.8f; 62 63 /** Constants of audio config for testing. */ 64 public static final int FREQUENCY = 264; 65 public static final int SAMPLE_RATE = 44100; 66 public static final int CHANNEL_COUNT = 1; 67 public static final int AMPLITUDE = 32767; 68 public static final int BUFFER_SIZE_IN_BYTES = 65536; 69 public static final int NUMBER_OF_SAMPLES = computeNumSamples(/* timeMs= */ 1000, SAMPLE_RATE, 70 CHANNEL_COUNT); 71 72 public static class CapturedAudio { 73 private int mSamplingRate; 74 private int mChannelCount; 75 private ByteBuffer mCapturedData; 76 private byte mByteValue; 77 private short mShortValue; 78 private float mFloatValue; 79 CapturedAudio(AudioRecord audioRecord)80 public CapturedAudio(AudioRecord audioRecord) { 81 mSamplingRate = audioRecord.getSampleRate(); 82 mChannelCount = audioRecord.getChannelCount(); 83 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE_IN_BYTES).order( 84 ByteOrder.nativeOrder()); 85 while (true) { 86 // Read the first buffer with non-zero data 87 byteBuffer.clear(); 88 int bytesRead = audioRecord.read(byteBuffer, BUFFER_SIZE_IN_BYTES); 89 if (bytesRead == 0 || isAllZero(byteBuffer)) { 90 continue; 91 } 92 mCapturedData = byteBuffer; 93 break; 94 } 95 } 96 CapturedAudio(AudioCapture audioCapture, ByteBuffer byteBuffer, int readMode)97 public CapturedAudio(AudioCapture audioCapture, ByteBuffer byteBuffer, int readMode) { 98 mSamplingRate = audioCapture.getFormat().getSampleRate(); 99 mChannelCount = audioCapture.getFormat().getChannelCount(); 100 while (true) { 101 // Read the first buffer with non-zero data 102 byteBuffer.clear(); 103 int bytesRead; 104 if (readMode == READ_BLOCKING || readMode == READ_NON_BLOCKING) { 105 bytesRead = audioCapture.read(byteBuffer, BUFFER_SIZE_IN_BYTES, readMode); 106 } else { 107 bytesRead = audioCapture.read(byteBuffer, BUFFER_SIZE_IN_BYTES); 108 } 109 if (bytesRead == 0 || isAllZero(byteBuffer)) { 110 continue; 111 } 112 mCapturedData = byteBuffer; 113 break; 114 } 115 } 116 CapturedAudio(AudioCapture audioCapture, byte[] audioData, int readMode)117 public CapturedAudio(AudioCapture audioCapture, byte[] audioData, int readMode) { 118 while (true) { 119 int bytesRead; 120 if (readMode == READ_BLOCKING || readMode == READ_NON_BLOCKING) { 121 bytesRead = audioCapture.read(audioData, 0, audioData.length, readMode); 122 } else { 123 bytesRead = audioCapture.read(audioData, 0, audioData.length); 124 } 125 if (bytesRead == 0) { 126 continue; 127 } 128 break; 129 } 130 for (int i = 0; i < audioData.length; i++) { 131 if (audioData[i] != 0) { 132 mByteValue = audioData[i]; 133 break; 134 } 135 } 136 } 137 CapturedAudio(AudioCapture audioCapture, short[] audioData, int readMode)138 public CapturedAudio(AudioCapture audioCapture, short[] audioData, int readMode) { 139 while (true) { 140 int bytesRead; 141 if (readMode == READ_BLOCKING || readMode == READ_NON_BLOCKING) { 142 bytesRead = audioCapture.read(audioData, 0, audioData.length, readMode); 143 } else { 144 bytesRead = audioCapture.read(audioData, 0, audioData.length); 145 } 146 if (bytesRead == 0) { 147 continue; 148 } 149 break; 150 } 151 for (int i = 0; i < audioData.length; i++) { 152 if (audioData[i] != 0) { 153 mShortValue = audioData[i]; 154 break; 155 } 156 } 157 } 158 CapturedAudio(AudioCapture audioCapture, float[] audioData, int readMode)159 public CapturedAudio(AudioCapture audioCapture, float[] audioData, int readMode) { 160 while (true) { 161 int bytesRead = audioCapture.read(audioData, 0, audioData.length, readMode); 162 if (bytesRead == 0) { 163 continue; 164 } 165 break; 166 } 167 for (int i = 0; i < audioData.length; i++) { 168 if (audioData[i] != 0) { 169 mFloatValue = audioData[i]; 170 break; 171 } 172 } 173 } 174 getPowerSpectrum(int frequency)175 public double getPowerSpectrum(int frequency) { 176 return getCapturedPowerSpectrum(mSamplingRate, mChannelCount, mCapturedData, frequency); 177 } 178 getByteValue()179 public byte getByteValue() { 180 return mByteValue; 181 } 182 getShortValue()183 public short getShortValue() { 184 return mShortValue; 185 } 186 getFloatValue()187 public float getFloatValue() { 188 return mFloatValue; 189 } 190 } 191 computeNumSamples(int timeMs, int samplingRate, int channelCount)192 public static int computeNumSamples(int timeMs, int samplingRate, int channelCount) { 193 return (int) ((long) timeMs * samplingRate * channelCount / 1000); 194 } 195 createAudioData(int samplingRate, int numSamples, int channelCount, double signalFrequencyHz, float amplitude)196 public static ByteBuffer createAudioData(int samplingRate, int numSamples, int channelCount, 197 double signalFrequencyHz, float amplitude) { 198 ByteBuffer playBuffer = 199 ByteBuffer.allocateDirect(numSamples * 2).order(ByteOrder.nativeOrder()); 200 final double multiplier = 2f * Math.PI * signalFrequencyHz / samplingRate; 201 for (int i = 0; i < numSamples; ) { 202 double vDouble = amplitude * Math.sin(multiplier * (i / channelCount)); 203 short v = (short) vDouble; 204 for (int c = 0; c < channelCount; c++) { 205 playBuffer.putShort(i * 2, v); 206 i++; 207 } 208 } 209 return playBuffer; 210 } 211 getCapturedPowerSpectrum( int samplingFreq, int channelCount, ByteBuffer capturedData, int expectedSignalFreq)212 public static double getCapturedPowerSpectrum( 213 int samplingFreq, int channelCount, ByteBuffer capturedData, 214 int expectedSignalFreq) { 215 if (capturedData == null) { 216 return 0; 217 } 218 double power = 0; 219 int length = capturedData.remaining() / 2; // PCM16, so 2 bytes for each 220 for (int i = 0; i < channelCount; i++) { 221 // Get the power in that channel 222 double goertzel = goertzel( 223 expectedSignalFreq, 224 samplingFreq, 225 capturedData, 226 /* offset= */ i, 227 length, 228 channelCount); 229 power += goertzel / channelCount; 230 } 231 return power; 232 } 233 234 /** 235 * Computes the relative power of a given frequency within a frame of the signal. 236 * See: http://en.wikipedia.org/wiki/Goertzel_algorithm 237 */ goertzel(int signalFreq, int samplingFreq, ByteBuffer samples, int offset, int length, int stride)238 private static double goertzel(int signalFreq, int samplingFreq, 239 ByteBuffer samples, int offset, int length, int stride) { 240 final int n = length / stride; 241 final double coeff = Math.cos(signalFreq * 2 * Math.PI / samplingFreq) * 2; 242 double s1 = 0; 243 double s2 = 0; 244 double rms = 0; 245 for (int i = 0; i < n; i++) { 246 double hamming = 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (n - 1)); 247 double x = samples.getShort(i * 2 * stride + offset) * hamming; // apply hamming window 248 double s = x + coeff * s1 - s2; 249 s2 = s1; 250 s1 = s; 251 rms += x * x; 252 } 253 rms = Math.sqrt(rms / n); 254 double magnitude = s2 * s2 + s1 * s1 - coeff * s1 * s2; 255 return Math.sqrt(magnitude) / n / rms; 256 } 257 isAllZero(ByteBuffer byteBuffer)258 private static boolean isAllZero(ByteBuffer byteBuffer) { 259 int position = byteBuffer.position(); 260 int limit = byteBuffer.limit(); 261 for (int i = position; i < limit; i += 2) { 262 if (byteBuffer.getShort(i) != 0) { 263 return false; 264 } 265 } 266 return true; 267 } 268 } 269