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