| 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 = "SDLAudio"; |
| |
| 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(); |
| } |