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