1 /* 2 * Copyright (C) 2024 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.media.audio.cts; 18 19 import static android.media.AudioAttributes.ALLOW_CAPTURE_BY_ALL; 20 21 import static org.hamcrest.Matchers.greaterThan; 22 import static org.hamcrest.Matchers.is; 23 import static org.junit.Assert.assertEquals; 24 import static org.junit.Assert.assertFalse; 25 import static org.junit.Assert.assertNotNull; 26 import static org.junit.Assert.assertNotSame; 27 import static org.junit.Assert.assertThat; 28 import static org.junit.Assert.assertTrue; 29 import static org.junit.Assert.fail; 30 import static org.junit.Assume.assumeThat; 31 32 import android.app.Instrumentation; 33 import android.content.Context; 34 import android.media.AudioAttributes; 35 import android.media.AudioAttributes.AttributeUsage; 36 import android.media.AudioAttributes.CapturePolicy; 37 import android.media.AudioFormat; 38 import android.media.AudioManager; 39 import android.media.AudioPlaybackCaptureConfiguration; 40 import android.media.AudioRecord; 41 import android.media.MediaPlayer; 42 import android.media.cts.MediaProjectionActivity; 43 import android.media.cts.Utils; 44 import android.media.projection.MediaProjection; 45 import android.platform.test.annotations.AppModeFull; 46 import android.platform.test.annotations.Presubmit; 47 import android.view.KeyEvent; 48 49 import androidx.test.platform.app.InstrumentationRegistry; 50 import androidx.test.rule.ActivityTestRule; 51 import androidx.test.uiautomator.UiDevice; 52 53 import org.junit.After; 54 import org.junit.Before; 55 import org.junit.Rule; 56 import org.junit.Test; 57 58 import java.nio.ByteBuffer; 59 import java.util.HashMap; 60 import java.util.Map; 61 62 /** 63 * Validate that there is no discontinuity in the AudioRecord data from Remote Submix. 64 * 65 * The tests do the following: 66 * - Start AudioRecord and MediaPlayer. 67 * - Play sine wav audio and read the recorded audio in rawBuffer. 68 * - Add screen lock during playback. 69 * - Stop MediaPlayer and AudioRecord, and then unlock the device. 70 * - Verify that the recorded audio doesn't have any discontinuity. 71 * 72 * Testing at sample level that audio playback and record do not make any alterations to input 73 * signal. 74 */ 75 76 @Presubmit 77 @AppModeFull(reason = "instant apps can't set up test conditions") 78 public class RemoteSubmixTest { 79 private static final String TAG = "RemoteSubmixTest"; 80 private static final int SAMPLE_RATE = 44100; 81 private static final int DURATION_IN_SEC = 1; 82 private static final int ENCODING_FORMAT = AudioFormat.ENCODING_PCM_16BIT; 83 private static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_MONO; 84 private static final int BUFFER_SIZE = SAMPLE_RATE * DURATION_IN_SEC 85 * Integer.bitCount(CHANNEL_MASK) 86 * Short.BYTES; // Size in bytes for 16bit mono at 44.1k/s 87 private static final int TEST_ITERATIONS = 10; // Using iterations for regression failure 88 private static final int RETRY_RECORD_READ = 3; 89 90 private Context mContext; 91 private AudioManager mAudioManager; 92 private MediaProjectionActivity mActivity; 93 private MediaProjection mMediaProjection; 94 private Map<Integer, Integer> mStreamVolume = new HashMap<Integer, Integer>(); 95 private Map<Integer, String> mStreamNames = new HashMap<Integer, String>(); 96 97 @Rule 98 public ActivityTestRule<MediaProjectionActivity> mActivityRule = 99 new ActivityTestRule<>(MediaProjectionActivity.class); 100 101 @Before setup()102 public void setup() throws Exception { 103 mActivity = mActivityRule.getActivity(); 104 mContext = getInstrumentation().getContext(); 105 mAudioManager = mActivity.getSystemService(AudioManager.class); 106 mMediaProjection = mActivity.waitForMediaProjection(); 107 mStreamNames.put(AudioManager.STREAM_RING, "RING"); 108 mStreamNames.put(AudioManager.STREAM_NOTIFICATION, "NOTIFICATION"); 109 mStreamNames.put(AudioManager.STREAM_SYSTEM, "SYSTEM"); 110 } 111 112 @After tearDown()113 public void tearDown() throws Exception { 114 unmuteStreams(); 115 } 116 getInstrumentation()117 private static Instrumentation getInstrumentation() { 118 return androidx.test.platform.app.InstrumentationRegistry.getInstrumentation(); 119 } 120 createPlaybackCaptureRecord()121 private AudioRecord createPlaybackCaptureRecord() throws Exception { 122 AudioPlaybackCaptureConfiguration apcConfig = 123 new AudioPlaybackCaptureConfiguration.Builder(mMediaProjection) 124 .addMatchingUsage(AudioAttributes.USAGE_MEDIA) 125 .build(); 126 127 AudioFormat audioFormat = new AudioFormat.Builder() 128 .setEncoding(ENCODING_FORMAT) 129 .setSampleRate(SAMPLE_RATE) 130 .setChannelMask(CHANNEL_MASK) 131 .build(); 132 133 assertEquals( 134 "matchingUsages", AudioAttributes.USAGE_MEDIA, apcConfig.getMatchingUsages()[0]); 135 136 AudioRecord audioRecord = new AudioRecord.Builder() 137 .setAudioPlaybackCaptureConfig(apcConfig) 138 .setAudioFormat(audioFormat) 139 .build(); 140 141 assertEquals("AudioRecord failed to initialized", AudioRecord.STATE_INITIALIZED, 142 audioRecord.getState()); 143 144 return audioRecord; 145 } 146 createMediaPlayer( @apturePolicy int capturePolicy, int resid, @AttributeUsage int usage)147 private MediaPlayer createMediaPlayer( 148 @CapturePolicy int capturePolicy, int resid, @AttributeUsage int usage) { 149 MediaPlayer mediaPlayer = MediaPlayer.create(mActivity, resid, 150 new AudioAttributes.Builder() 151 .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) 152 .setUsage(usage) 153 .setAllowedCapturePolicy(capturePolicy) 154 .build(), 155 mAudioManager.generateAudioSessionId()); 156 return mediaPlayer; 157 } 158 readToBuffer(AudioRecord audioRecord, int bufferSize)159 private static ByteBuffer readToBuffer(AudioRecord audioRecord, int bufferSize) 160 throws Exception { 161 assertEquals("AudioRecord is not recording", AudioRecord.RECORDSTATE_RECORDING, 162 audioRecord.getRecordingState()); 163 ByteBuffer buffer = ByteBuffer.allocateDirect(bufferSize); 164 int retry = RETRY_RECORD_READ; 165 boolean silence = true; 166 while (silence && buffer.hasRemaining()) { 167 assertNotSame(buffer.remaining() + "/" + bufferSize + " remaining", 0, retry--); 168 int written = audioRecord.read(buffer, buffer.remaining()); 169 assertThat("audioRecord did not read frames", written, greaterThan(0)); 170 for (int i = 0; i < written; i++) { 171 if (buffer.get() != 0) { 172 silence = false; 173 break; 174 } 175 } 176 } 177 buffer.rewind(); 178 return buffer; 179 } 180 181 /** 182 * Mute device audio streams 183 */ muteStreams()184 private void muteStreams() throws Exception { 185 try { 186 Utils.toggleNotificationPolicyAccess( 187 mContext.getPackageName(), getInstrumentation(), true); 188 for (Map.Entry<Integer, String> map : mStreamNames.entrySet()) { 189 // Get current device stream volume level 190 mStreamVolume.put(map.getKey(), mAudioManager.getStreamVolume(map.getKey())); 191 // Mute device streams 192 mAudioManager.adjustStreamVolume( 193 map.getKey(), AudioManager.ADJUST_MUTE, 0 /*no flag used*/); 194 assumeThat("Stream " + map.getValue() + " can not be muted", 195 mAudioManager.getStreamVolume(map.getKey()), is(0)); 196 } 197 } finally { 198 Utils.toggleNotificationPolicyAccess( 199 mContext.getPackageName(), getInstrumentation(), false); 200 } 201 } 202 203 /** 204 * Unmute device audio streams 205 */ unmuteStreams()206 private void unmuteStreams() throws Exception { 207 try { 208 Utils.toggleNotificationPolicyAccess( 209 mContext.getPackageName(), getInstrumentation(), true); 210 for (Map.Entry<Integer, Integer> map : mStreamVolume.entrySet()) { 211 // Restore device stream volume 212 mAudioManager.setStreamVolume(map.getKey(), map.getValue(), 0 /*no flag used*/); 213 } 214 } finally { 215 Utils.toggleNotificationPolicyAccess( 216 mContext.getPackageName(), getInstrumentation(), false); 217 } 218 mStreamVolume.clear(); 219 } 220 testPlaybackCapture(boolean testWithScreenLock)221 public void testPlaybackCapture(boolean testWithScreenLock) throws Exception { 222 MediaPlayer mediaPlayer = createMediaPlayer( 223 ALLOW_CAPTURE_BY_ALL, R.raw.sine1320hz5sec, AudioAttributes.USAGE_MEDIA); 224 AudioRecord audioRecord = createPlaybackCaptureRecord(); 225 ByteBuffer rawBuffer = null; 226 227 try { 228 audioRecord.startRecording(); 229 mediaPlayer.start(); 230 231 assertEquals(AudioRecord.RECORDSTATE_RECORDING, audioRecord.getRecordingState()); 232 assertTrue(mediaPlayer.isPlaying()); 233 234 if (testWithScreenLock) { 235 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 236 .pressKeyCode(KeyEvent.KEYCODE_POWER); 237 } 238 239 rawBuffer = readToBuffer(audioRecord, BUFFER_SIZE); 240 241 audioRecord.stop(); 242 mediaPlayer.stop(); 243 244 assertEquals(AudioRecord.RECORDSTATE_STOPPED, audioRecord.getRecordingState()); 245 assertFalse(mediaPlayer.isPlaying()); 246 247 } catch (Exception e) { 248 throw e; 249 } finally { 250 if (testWithScreenLock) { 251 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 252 .pressKeyCode(KeyEvent.KEYCODE_WAKEUP); 253 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) 254 .executeShellCommand("wm dismiss-keyguard"); 255 } 256 257 audioRecord.release(); 258 mediaPlayer.release(); 259 } 260 261 assertNotNull("Recorded data is null ", rawBuffer); 262 263 short[] recordArray = new short[BUFFER_SIZE / Short.BYTES]; 264 265 for (int i = 0; i < recordArray.length; i++) { 266 recordArray[i] = rawBuffer.getShort(); 267 } 268 269 int recordingStartIndex = -1; 270 271 // Skip leading silence of the Recorded Audio 272 for (int i = 0; i < recordArray.length; i++) { 273 if (recordArray[i] != 0) { 274 recordingStartIndex = i; 275 break; 276 } 277 } 278 279 assertFalse("No audio recorded", recordingStartIndex == -1); 280 // Validate that there is no continuous silence in recorded sine audio 281 for (int i = recordingStartIndex; i < recordArray.length; i++) { 282 assertFalse("Discontunuity found in the Record Audio\n", 283 recordArray[i] == 0 && recordArray[i + 1] == 0); 284 } 285 } 286 287 @Test testRemoteSubmixRecordingContinuity()288 public void testRemoteSubmixRecordingContinuity() throws Exception { 289 muteStreams(); 290 for (int i = 0; i < TEST_ITERATIONS; i++) { 291 try { 292 testPlaybackCapture(/* testWithScreenLock */ false); 293 } catch (Exception e) { 294 fail("testPlaybackCapture throws exception: " + e + " at the " + i 295 + "th iteration"); 296 } 297 } 298 } 299 300 @Test testRemoteSubmixRecordingContinuityWithScreenLock()301 public void testRemoteSubmixRecordingContinuityWithScreenLock() throws Exception { 302 muteStreams(); 303 for (int i = 0; i < TEST_ITERATIONS; i++) { 304 try { 305 testPlaybackCapture(/* testWithScreenLock */ true); 306 } catch (Exception e) { 307 fail("testPlaybackCapture with screen lock throws exception: " + e + " at the " + i 308 + "th iteration"); 309 } 310 } 311 } 312 } 313