| /* |
| * Copyright (C) 2022 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. |
| */ |
| |
| package android.virtualdevice.cts.common; |
| |
| import static android.media.AudioRecord.READ_BLOCKING; |
| import static android.media.AudioRecord.READ_NON_BLOCKING; |
| |
| import android.annotation.IntDef; |
| import android.companion.virtual.audio.AudioCapture; |
| import android.media.AudioRecord; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| |
| /** |
| * Utility methods for creating and processing audio data. |
| */ |
| public final class AudioHelper { |
| /** Tells the activity to play audio for testing. */ |
| public static final String ACTION_PLAY_AUDIO = "android.virtualdevice.cts.PLAY_AUDIO"; |
| |
| /** Tells the activity to record audio for testing. */ |
| public static final String ACTION_RECORD_AUDIO = "android.virtualdevice.cts.RECORD_AUDIO"; |
| |
| /** Tells the activity to play or record for which audio data type. */ |
| public static final String EXTRA_AUDIO_DATA_TYPE = "audio_data_type"; |
| |
| @IntDef({ |
| BYTE_BUFFER, |
| BYTE_ARRAY, |
| SHORT_ARRAY, |
| FLOAT_ARRAY, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface DataType {} |
| |
| public static final int BYTE_BUFFER = 1; |
| public static final int BYTE_ARRAY = 2; |
| public static final int SHORT_ARRAY = 3; |
| public static final int FLOAT_ARRAY = 4; |
| |
| /** Values for read and write verification. */ |
| public static final byte BYTE_VALUE = 8; |
| public static final short SHORT_VALUE = 16; |
| public static final float FLOAT_VALUE = 0.8f; |
| |
| /** Constants of audio config for testing. */ |
| public static final int FREQUENCY = 264; |
| public static final int SAMPLE_RATE = 44100; |
| public static final int CHANNEL_COUNT = 1; |
| public static final int AMPLITUDE = 32767; |
| public static final int BUFFER_SIZE_IN_BYTES = 65536; |
| public static final int NUMBER_OF_SAMPLES = computeNumSamples(/* timeMs= */ 1000, SAMPLE_RATE, |
| CHANNEL_COUNT); |
| |
| public static class CapturedAudio { |
| private int mSamplingRate; |
| private int mChannelCount; |
| private ByteBuffer mCapturedData; |
| private byte mByteValue; |
| private short mShortValue; |
| private float mFloatValue; |
| |
| public CapturedAudio(AudioRecord audioRecord) { |
| mSamplingRate = audioRecord.getSampleRate(); |
| mChannelCount = audioRecord.getChannelCount(); |
| ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE_IN_BYTES).order( |
| ByteOrder.nativeOrder()); |
| while (true) { |
| // Read the first buffer with non-zero data |
| byteBuffer.clear(); |
| int bytesRead = audioRecord.read(byteBuffer, BUFFER_SIZE_IN_BYTES); |
| if (bytesRead == 0 || isAllZero(byteBuffer)) { |
| continue; |
| } |
| mCapturedData = byteBuffer; |
| break; |
| } |
| } |
| |
| public CapturedAudio(AudioCapture audioCapture, ByteBuffer byteBuffer, int readMode) { |
| mSamplingRate = audioCapture.getFormat().getSampleRate(); |
| mChannelCount = audioCapture.getFormat().getChannelCount(); |
| while (true) { |
| // Read the first buffer with non-zero data |
| byteBuffer.clear(); |
| int bytesRead; |
| if (readMode == READ_BLOCKING || readMode == READ_NON_BLOCKING) { |
| bytesRead = audioCapture.read(byteBuffer, BUFFER_SIZE_IN_BYTES, readMode); |
| } else { |
| bytesRead = audioCapture.read(byteBuffer, BUFFER_SIZE_IN_BYTES); |
| } |
| if (bytesRead == 0 || isAllZero(byteBuffer)) { |
| continue; |
| } |
| mCapturedData = byteBuffer; |
| break; |
| } |
| } |
| |
| public CapturedAudio(AudioCapture audioCapture, byte[] audioData, int readMode) { |
| while (true) { |
| int bytesRead; |
| if (readMode == READ_BLOCKING || readMode == READ_NON_BLOCKING) { |
| bytesRead = audioCapture.read(audioData, 0, audioData.length, readMode); |
| } else { |
| bytesRead = audioCapture.read(audioData, 0, audioData.length); |
| } |
| if (bytesRead == 0) { |
| continue; |
| } |
| break; |
| } |
| for (int i = 0; i < audioData.length; i++) { |
| if (audioData[i] != 0) { |
| mByteValue = audioData[i]; |
| break; |
| } |
| } |
| } |
| |
| public CapturedAudio(AudioCapture audioCapture, short[] audioData, int readMode) { |
| while (true) { |
| int bytesRead; |
| if (readMode == READ_BLOCKING || readMode == READ_NON_BLOCKING) { |
| bytesRead = audioCapture.read(audioData, 0, audioData.length, readMode); |
| } else { |
| bytesRead = audioCapture.read(audioData, 0, audioData.length); |
| } |
| if (bytesRead == 0) { |
| continue; |
| } |
| break; |
| } |
| for (int i = 0; i < audioData.length; i++) { |
| if (audioData[i] != 0) { |
| mShortValue = audioData[i]; |
| break; |
| } |
| } |
| } |
| |
| public CapturedAudio(AudioCapture audioCapture, float[] audioData, int readMode) { |
| while (true) { |
| int bytesRead = audioCapture.read(audioData, 0, audioData.length, readMode); |
| if (bytesRead == 0) { |
| continue; |
| } |
| break; |
| } |
| for (int i = 0; i < audioData.length; i++) { |
| if (audioData[i] != 0) { |
| mFloatValue = audioData[i]; |
| break; |
| } |
| } |
| } |
| |
| public double getPowerSpectrum(int frequency) { |
| return getCapturedPowerSpectrum(mSamplingRate, mChannelCount, mCapturedData, frequency); |
| } |
| |
| public byte getByteValue() { |
| return mByteValue; |
| } |
| |
| public short getShortValue() { |
| return mShortValue; |
| } |
| |
| public float getFloatValue() { |
| return mFloatValue; |
| } |
| } |
| |
| public static int computeNumSamples(int timeMs, int samplingRate, int channelCount) { |
| return (int) ((long) timeMs * samplingRate * channelCount / 1000); |
| } |
| |
| public static ByteBuffer createAudioData(int samplingRate, int numSamples, int channelCount, |
| double signalFrequencyHz, float amplitude) { |
| ByteBuffer playBuffer = |
| ByteBuffer.allocateDirect(numSamples * 2).order(ByteOrder.nativeOrder()); |
| final double multiplier = 2f * Math.PI * signalFrequencyHz / samplingRate; |
| for (int i = 0; i < numSamples; ) { |
| double vDouble = amplitude * Math.sin(multiplier * (i / channelCount)); |
| short v = (short) vDouble; |
| for (int c = 0; c < channelCount; c++) { |
| playBuffer.putShort(i * 2, v); |
| i++; |
| } |
| } |
| return playBuffer; |
| } |
| |
| public static double getCapturedPowerSpectrum( |
| int samplingFreq, int channelCount, ByteBuffer capturedData, |
| int expectedSignalFreq) { |
| double power = 0; |
| int length = capturedData.remaining() / 2; // PCM16, so 2 bytes for each |
| for (int i = 0; i < channelCount; i++) { |
| // Get the power in that channel |
| double goertzel = goertzel( |
| expectedSignalFreq, |
| samplingFreq, |
| capturedData, |
| /* offset= */ i, |
| length, |
| channelCount); |
| power += goertzel / channelCount; |
| } |
| return power; |
| } |
| |
| /** |
| * Computes the relative power of a given frequency within a frame of the signal. |
| * See: http://en.wikipedia.org/wiki/Goertzel_algorithm |
| */ |
| private static double goertzel(int signalFreq, int samplingFreq, |
| ByteBuffer samples, int offset, int length, int stride) { |
| final int n = length / stride; |
| final double coeff = Math.cos(signalFreq * 2 * Math.PI / samplingFreq) * 2; |
| double s1 = 0; |
| double s2 = 0; |
| double rms = 0; |
| for (int i = 0; i < n; i++) { |
| double hamming = 0.54 - 0.46 * Math.cos(2 * Math.PI * i / (n - 1)); |
| double x = samples.getShort(i * 2 * stride + offset) * hamming; // apply hamming window |
| double s = x + coeff * s1 - s2; |
| s2 = s1; |
| s1 = s; |
| rms += x * x; |
| } |
| rms = Math.sqrt(rms / n); |
| double magnitude = s2 * s2 + s1 * s1 - coeff * s1 * s2; |
| return Math.sqrt(magnitude) / n / rms; |
| } |
| |
| private static boolean isAllZero(ByteBuffer byteBuffer) { |
| int position = byteBuffer.position(); |
| int limit = byteBuffer.limit(); |
| for (int i = position; i < limit; i += 2) { |
| if (byteBuffer.getShort(i) != 0) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |