1 package org.libsdl.app; 2 3 import android.media.*; 4 import android.os.Build; 5 import android.util.Log; 6 7 public class SDLAudioManager 8 { 9 protected static final String TAG = "SDLAudio"; 10 11 protected static AudioTrack mAudioTrack; 12 protected static AudioRecord mAudioRecord; 13 initialize()14 public static void initialize() { 15 mAudioTrack = null; 16 mAudioRecord = null; 17 } 18 19 // Audio 20 getAudioFormatString(int audioFormat)21 protected static String getAudioFormatString(int audioFormat) { 22 switch (audioFormat) { 23 case AudioFormat.ENCODING_PCM_8BIT: 24 return "8-bit"; 25 case AudioFormat.ENCODING_PCM_16BIT: 26 return "16-bit"; 27 case AudioFormat.ENCODING_PCM_FLOAT: 28 return "float"; 29 default: 30 return Integer.toString(audioFormat); 31 } 32 } 33 open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames)34 protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { 35 int channelConfig; 36 int sampleSize; 37 int frameSize; 38 39 Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz"); 40 41 /* On older devices let's use known good settings */ 42 if (Build.VERSION.SDK_INT < 21) { 43 if (desiredChannels > 2) { 44 desiredChannels = 2; 45 } 46 if (sampleRate < 8000) { 47 sampleRate = 8000; 48 } else if (sampleRate > 48000) { 49 sampleRate = 48000; 50 } 51 } 52 53 if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) { 54 int minSDKVersion = (isCapture ? 23 : 21); 55 if (Build.VERSION.SDK_INT < minSDKVersion) { 56 audioFormat = AudioFormat.ENCODING_PCM_16BIT; 57 } 58 } 59 switch (audioFormat) 60 { 61 case AudioFormat.ENCODING_PCM_8BIT: 62 sampleSize = 1; 63 break; 64 case AudioFormat.ENCODING_PCM_16BIT: 65 sampleSize = 2; 66 break; 67 case AudioFormat.ENCODING_PCM_FLOAT: 68 sampleSize = 4; 69 break; 70 default: 71 Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT"); 72 audioFormat = AudioFormat.ENCODING_PCM_16BIT; 73 sampleSize = 2; 74 break; 75 } 76 77 if (isCapture) { 78 switch (desiredChannels) { 79 case 1: 80 channelConfig = AudioFormat.CHANNEL_IN_MONO; 81 break; 82 case 2: 83 channelConfig = AudioFormat.CHANNEL_IN_STEREO; 84 break; 85 default: 86 Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); 87 desiredChannels = 2; 88 channelConfig = AudioFormat.CHANNEL_IN_STEREO; 89 break; 90 } 91 } else { 92 switch (desiredChannels) { 93 case 1: 94 channelConfig = AudioFormat.CHANNEL_OUT_MONO; 95 break; 96 case 2: 97 channelConfig = AudioFormat.CHANNEL_OUT_STEREO; 98 break; 99 case 3: 100 channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; 101 break; 102 case 4: 103 channelConfig = AudioFormat.CHANNEL_OUT_QUAD; 104 break; 105 case 5: 106 channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; 107 break; 108 case 6: 109 channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; 110 break; 111 case 7: 112 channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; 113 break; 114 case 8: 115 if (Build.VERSION.SDK_INT >= 23) { 116 channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; 117 } else { 118 Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround"); 119 desiredChannels = 6; 120 channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; 121 } 122 break; 123 default: 124 Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); 125 desiredChannels = 2; 126 channelConfig = AudioFormat.CHANNEL_OUT_STEREO; 127 break; 128 } 129 130 /* 131 Log.v(TAG, "Speaker configuration (and order of channels):"); 132 133 if ((channelConfig & 0x00000004) != 0) { 134 Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT"); 135 } 136 if ((channelConfig & 0x00000008) != 0) { 137 Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT"); 138 } 139 if ((channelConfig & 0x00000010) != 0) { 140 Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER"); 141 } 142 if ((channelConfig & 0x00000020) != 0) { 143 Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY"); 144 } 145 if ((channelConfig & 0x00000040) != 0) { 146 Log.v(TAG, " CHANNEL_OUT_BACK_LEFT"); 147 } 148 if ((channelConfig & 0x00000080) != 0) { 149 Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT"); 150 } 151 if ((channelConfig & 0x00000100) != 0) { 152 Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER"); 153 } 154 if ((channelConfig & 0x00000200) != 0) { 155 Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER"); 156 } 157 if ((channelConfig & 0x00000400) != 0) { 158 Log.v(TAG, " CHANNEL_OUT_BACK_CENTER"); 159 } 160 if ((channelConfig & 0x00000800) != 0) { 161 Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT"); 162 } 163 if ((channelConfig & 0x00001000) != 0) { 164 Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT"); 165 } 166 */ 167 } 168 frameSize = (sampleSize * desiredChannels); 169 170 // Let the user pick a larger buffer if they really want -- but ye 171 // gods they probably shouldn't, the minimums are horrifyingly high 172 // latency already 173 int minBufferSize; 174 if (isCapture) { 175 minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); 176 } else { 177 minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); 178 } 179 desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize); 180 181 int[] results = new int[4]; 182 183 if (isCapture) { 184 if (mAudioRecord == null) { 185 mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate, 186 channelConfig, audioFormat, desiredFrames * frameSize); 187 188 // see notes about AudioTrack state in audioOpen(), above. Probably also applies here. 189 if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) { 190 Log.e(TAG, "Failed during initialization of AudioRecord"); 191 mAudioRecord.release(); 192 mAudioRecord = null; 193 return null; 194 } 195 196 mAudioRecord.startRecording(); 197 } 198 199 results[0] = mAudioRecord.getSampleRate(); 200 results[1] = mAudioRecord.getAudioFormat(); 201 results[2] = mAudioRecord.getChannelCount(); 202 results[3] = desiredFrames; 203 204 } else { 205 if (mAudioTrack == null) { 206 mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); 207 208 // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid 209 // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java 210 // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() 211 if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { 212 /* Try again, with safer values */ 213 214 Log.e(TAG, "Failed during initialization of Audio Track"); 215 mAudioTrack.release(); 216 mAudioTrack = null; 217 return null; 218 } 219 220 mAudioTrack.play(); 221 } 222 223 results[0] = mAudioTrack.getSampleRate(); 224 results[1] = mAudioTrack.getAudioFormat(); 225 results[2] = mAudioTrack.getChannelCount(); 226 results[3] = desiredFrames; 227 } 228 229 Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz"); 230 231 return results; 232 } 233 234 /** 235 * This method is called by SDL using JNI. 236 */ audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames)237 public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { 238 return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames); 239 } 240 241 /** 242 * This method is called by SDL using JNI. 243 */ audioWriteFloatBuffer(float[] buffer)244 public static void audioWriteFloatBuffer(float[] buffer) { 245 if (mAudioTrack == null) { 246 Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); 247 return; 248 } 249 250 for (int i = 0; i < buffer.length;) { 251 int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING); 252 if (result > 0) { 253 i += result; 254 } else if (result == 0) { 255 try { 256 Thread.sleep(1); 257 } catch(InterruptedException e) { 258 // Nom nom 259 } 260 } else { 261 Log.w(TAG, "SDL audio: error return from write(float)"); 262 return; 263 } 264 } 265 } 266 267 /** 268 * This method is called by SDL using JNI. 269 */ audioWriteShortBuffer(short[] buffer)270 public static void audioWriteShortBuffer(short[] buffer) { 271 if (mAudioTrack == null) { 272 Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); 273 return; 274 } 275 276 for (int i = 0; i < buffer.length;) { 277 int result = mAudioTrack.write(buffer, i, buffer.length - i); 278 if (result > 0) { 279 i += result; 280 } else if (result == 0) { 281 try { 282 Thread.sleep(1); 283 } catch(InterruptedException e) { 284 // Nom nom 285 } 286 } else { 287 Log.w(TAG, "SDL audio: error return from write(short)"); 288 return; 289 } 290 } 291 } 292 293 /** 294 * This method is called by SDL using JNI. 295 */ audioWriteByteBuffer(byte[] buffer)296 public static void audioWriteByteBuffer(byte[] buffer) { 297 if (mAudioTrack == null) { 298 Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); 299 return; 300 } 301 302 for (int i = 0; i < buffer.length; ) { 303 int result = mAudioTrack.write(buffer, i, buffer.length - i); 304 if (result > 0) { 305 i += result; 306 } else if (result == 0) { 307 try { 308 Thread.sleep(1); 309 } catch(InterruptedException e) { 310 // Nom nom 311 } 312 } else { 313 Log.w(TAG, "SDL audio: error return from write(byte)"); 314 return; 315 } 316 } 317 } 318 319 /** 320 * This method is called by SDL using JNI. 321 */ captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames)322 public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { 323 return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames); 324 } 325 326 /** This method is called by SDL using JNI. */ captureReadFloatBuffer(float[] buffer, boolean blocking)327 public static int captureReadFloatBuffer(float[] buffer, boolean blocking) { 328 return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); 329 } 330 331 /** This method is called by SDL using JNI. */ captureReadShortBuffer(short[] buffer, boolean blocking)332 public static int captureReadShortBuffer(short[] buffer, boolean blocking) { 333 if (Build.VERSION.SDK_INT < 23) { 334 return mAudioRecord.read(buffer, 0, buffer.length); 335 } else { 336 return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); 337 } 338 } 339 340 /** This method is called by SDL using JNI. */ captureReadByteBuffer(byte[] buffer, boolean blocking)341 public static int captureReadByteBuffer(byte[] buffer, boolean blocking) { 342 if (Build.VERSION.SDK_INT < 23) { 343 return mAudioRecord.read(buffer, 0, buffer.length); 344 } else { 345 return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); 346 } 347 } 348 349 /** This method is called by SDL using JNI. */ audioClose()350 public static void audioClose() { 351 if (mAudioTrack != null) { 352 mAudioTrack.stop(); 353 mAudioTrack.release(); 354 mAudioTrack = null; 355 } 356 } 357 358 /** This method is called by SDL using JNI. */ captureClose()359 public static void captureClose() { 360 if (mAudioRecord != null) { 361 mAudioRecord.stop(); 362 mAudioRecord.release(); 363 mAudioRecord = null; 364 } 365 } 366 367 /** This method is called by SDL using JNI. */ audioSetThreadPriority(boolean iscapture, int device_id)368 public static void audioSetThreadPriority(boolean iscapture, int device_id) { 369 try { 370 371 /* Set thread name */ 372 if (iscapture) { 373 Thread.currentThread().setName("SDLAudioC" + device_id); 374 } else { 375 Thread.currentThread().setName("SDLAudioP" + device_id); 376 } 377 378 /* Set thread priority */ 379 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); 380 381 } catch (Exception e) { 382 Log.v(TAG, "modify thread properties failed " + e.toString()); 383 } 384 } 385 nativeSetupJNI()386 public static native int nativeSetupJNI(); 387 } 388