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