package org.libsdl.app; import android.media.AudioFormat; import android.media.AudioManager; import android.media.AudioRecord; import android.media.AudioTrack; import android.media.MediaRecorder; import android.os.Build; import android.util.Log; public class SDLAudioManager { protected static final String TAG = "yoyo"; protected static AudioTrack mAudioTrack; protected static AudioRecord mAudioRecord; public static void initialize() { mAudioTrack = null; mAudioRecord = null; } // Audio protected static String getAudioFormatString(int audioFormat) { switch (audioFormat) { case AudioFormat.ENCODING_PCM_8BIT: return "8-bit"; case AudioFormat.ENCODING_PCM_16BIT: return "16-bit"; case AudioFormat.ENCODING_PCM_FLOAT: return "float"; default: return Integer.toString(audioFormat); } } protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { int channelConfig; int sampleSize; int frameSize; Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz"); /* On older devices let's use known good settings */ if (Build.VERSION.SDK_INT < 21) { if (desiredChannels > 2) { desiredChannels = 2; } if (sampleRate < 8000) { sampleRate = 8000; } else if (sampleRate > 48000) { sampleRate = 48000; } } if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) { int minSDKVersion = (isCapture ? 23 : 21); if (Build.VERSION.SDK_INT < minSDKVersion) { audioFormat = AudioFormat.ENCODING_PCM_16BIT; } } switch (audioFormat) { case AudioFormat.ENCODING_PCM_8BIT: sampleSize = 1; break; case AudioFormat.ENCODING_PCM_16BIT: sampleSize = 2; break; case AudioFormat.ENCODING_PCM_FLOAT: sampleSize = 4; break; default: Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT"); audioFormat = AudioFormat.ENCODING_PCM_16BIT; sampleSize = 2; break; } if (isCapture) { switch (desiredChannels) { case 1: channelConfig = AudioFormat.CHANNEL_IN_MONO; break; case 2: channelConfig = AudioFormat.CHANNEL_IN_STEREO; break; default: Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); desiredChannels = 2; channelConfig = AudioFormat.CHANNEL_IN_STEREO; break; } } else { switch (desiredChannels) { case 1: channelConfig = AudioFormat.CHANNEL_OUT_MONO; break; case 2: channelConfig = AudioFormat.CHANNEL_OUT_STEREO; break; case 3: channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER; break; case 4: channelConfig = AudioFormat.CHANNEL_OUT_QUAD; break; case 5: channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER; break; case 6: channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; break; case 7: channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER; break; case 8: if (Build.VERSION.SDK_INT >= 23) { channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; } else { Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround"); desiredChannels = 6; channelConfig = AudioFormat.CHANNEL_OUT_5POINT1; } break; default: Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo"); desiredChannels = 2; channelConfig = AudioFormat.CHANNEL_OUT_STEREO; break; } /* Log.v(TAG, "Speaker configuration (and order of channels):"); if ((channelConfig & 0x00000004) != 0) { Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT"); } if ((channelConfig & 0x00000008) != 0) { Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT"); } if ((channelConfig & 0x00000010) != 0) { Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER"); } if ((channelConfig & 0x00000020) != 0) { Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY"); } if ((channelConfig & 0x00000040) != 0) { Log.v(TAG, " CHANNEL_OUT_BACK_LEFT"); } if ((channelConfig & 0x00000080) != 0) { Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT"); } if ((channelConfig & 0x00000100) != 0) { Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER"); } if ((channelConfig & 0x00000200) != 0) { Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER"); } if ((channelConfig & 0x00000400) != 0) { Log.v(TAG, " CHANNEL_OUT_BACK_CENTER"); } if ((channelConfig & 0x00000800) != 0) { Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT"); } if ((channelConfig & 0x00001000) != 0) { Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT"); } */ } frameSize = (sampleSize * desiredChannels); // Let the user pick a larger buffer if they really want -- but ye // gods they probably shouldn't, the minimums are horrifyingly high // latency already int minBufferSize; if (isCapture) { minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat); } else { minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat); } desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize); int[] results = new int[4]; if (isCapture) { if (mAudioRecord == null) { mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize); // see notes about AudioTrack state in audioOpen(), above. Probably also applies here. if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) { Log.e(TAG, "Failed during initialization of AudioRecord"); mAudioRecord.release(); mAudioRecord = null; return null; } mAudioRecord.startRecording(); } results[0] = mAudioRecord.getSampleRate(); results[1] = mAudioRecord.getAudioFormat(); results[2] = mAudioRecord.getChannelCount(); } else { if (mAudioTrack == null) { mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM); // Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid // Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java // Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState() if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) { /* Try again, with safer values */ Log.e(TAG, "Failed during initialization of Audio Track"); mAudioTrack.release(); mAudioTrack = null; return null; } mAudioTrack.play(); } results[0] = mAudioTrack.getSampleRate(); results[1] = mAudioTrack.getAudioFormat(); results[2] = mAudioTrack.getChannelCount(); } results[3] = desiredFrames; Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz"); return results; } /** * This method is called by SDL using JNI. */ public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames); } /** * This method is called by SDL using JNI. */ public static void audioWriteFloatBuffer(float[] buffer) { if (mAudioTrack == null) { Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); return; } for (int i = 0; i < buffer.length;) { int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING); if (result > 0) { i += result; } else if (result == 0) { try { Thread.sleep(1); } catch(InterruptedException e) { // Nom nom } } else { Log.w(TAG, "SDL audio: error return from write(float)"); return; } } } /** * This method is called by SDL using JNI. */ public static void audioWriteShortBuffer(short[] buffer) { if (mAudioTrack == null) { Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); return; } for (int i = 0; i < buffer.length;) { int result = mAudioTrack.write(buffer, i, buffer.length - i); if (result > 0) { i += result; } else if (result == 0) { try { Thread.sleep(1); } catch(InterruptedException e) { // Nom nom } } else { Log.w(TAG, "SDL audio: error return from write(short)"); return; } } } /** * This method is called by SDL using JNI. */ public static void audioWriteByteBuffer(byte[] buffer) { if (mAudioTrack == null) { Log.e(TAG, "Attempted to make audio call with uninitialized audio!"); return; } for (int i = 0; i < buffer.length; ) { int result = mAudioTrack.write(buffer, i, buffer.length - i); if (result > 0) { i += result; } else if (result == 0) { try { Thread.sleep(1); } catch(InterruptedException e) { // Nom nom } } else { Log.w(TAG, "SDL audio: error return from write(byte)"); return; } } } /** * This method is called by SDL using JNI. */ public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames) { return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames); } /** This method is called by SDL using JNI. */ public static int captureReadFloatBuffer(float[] buffer, boolean blocking) { return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); } /** This method is called by SDL using JNI. */ public static int captureReadShortBuffer(short[] buffer, boolean blocking) { if (Build.VERSION.SDK_INT < 23) { return mAudioRecord.read(buffer, 0, buffer.length); } else { return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); } } /** This method is called by SDL using JNI. */ public static int captureReadByteBuffer(byte[] buffer, boolean blocking) { if (Build.VERSION.SDK_INT < 23) { return mAudioRecord.read(buffer, 0, buffer.length); } else { return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING); } } /** This method is called by SDL using JNI. */ public static void audioClose() { if (mAudioTrack != null) { mAudioTrack.stop(); mAudioTrack.release(); mAudioTrack = null; } } /** This method is called by SDL using JNI. */ public static void captureClose() { if (mAudioRecord != null) { mAudioRecord.stop(); mAudioRecord.release(); mAudioRecord = null; } } /** This method is called by SDL using JNI. */ public static void audioSetThreadPriority(boolean iscapture, int device_id) { try { /* Set thread name */ if (iscapture) { Thread.currentThread().setName("SDLAudioC" + device_id); } else { Thread.currentThread().setName("SDLAudioP" + device_id); } /* Set thread priority */ android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); } catch (Exception e) { Log.v(TAG, "modify thread properties failed " + e.toString()); } } public static native int nativeSetupJNI(); }