1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package org.hyphonate.megaaudio.player; 17 18 import android.media.AudioDeviceInfo; 19 import android.media.AudioFormat; 20 import android.media.AudioTimestamp; 21 import android.media.AudioTrack; 22 import android.util.Log; 23 24 import org.hyphonate.megaaudio.common.BuilderBase; 25 import org.hyphonate.megaaudio.common.StreamBase; 26 import org.hyphonate.megaaudio.common.StreamState; 27 28 /** 29 * Implementation of abstract Player class implemented for the Android Java-based audio playback 30 * API, i.e. AudioTrack. 31 */ 32 public class JavaPlayer extends Player { 33 @SuppressWarnings("unused") 34 private static final String TAG = JavaPlayer.class.getSimpleName(); 35 @SuppressWarnings("unused") 36 private static final boolean LOG = true; 37 38 /* 39 * Player infrastructure 40 */ 41 /* The AudioTrack for playing the audio stream */ 42 private AudioTrack mAudioTrack; 43 44 /* 45 * Data buffers 46 */ 47 /** The Burst Buffer. This is the buffer we fill with audio and feed into the AudioTrack. */ 48 private float[] mAudioBuffer; 49 50 // Player-specific extension 51 52 /** 53 * @return The underlying Java API AudioTrack object 54 */ getAudioTrack()55 public AudioTrack getAudioTrack() { return mAudioTrack; } 56 57 /** 58 * Constructs a JavaPlayer object. Create and sets up the AudioTrack for playback. 59 * @param builder Provides the attributes for the underlying AudioTrack. 60 * @param sourceProvider The AudioSource object providing audio data to play. 61 */ JavaPlayer(PlayerBuilder builder, AudioSourceProvider sourceProvider)62 public JavaPlayer(PlayerBuilder builder, AudioSourceProvider sourceProvider) { 63 super(sourceProvider); 64 mNumExchangeFrames = -1; // TODO need error defines 65 66 setupStream(builder); 67 } 68 69 /** 70 * Allocates the array for the burst buffer. 71 */ allocBurstBuffer()72 private void allocBurstBuffer() { 73 if (LOG) { 74 Log.i(TAG, "allocBurstBuffer() mNumExchangeFrames:" + mNumExchangeFrames); 75 } 76 // pad it by 1 frame. This allows some sources to not have to worry about 77 // handling the end-of-buffer edge case. i.e. a "Guard Point" for interpolation. 78 mAudioBuffer = new float[(mNumExchangeFrames + 1) * mChannelCount]; 79 } 80 81 // 82 // Attributes 83 // 84 @Override getRoutedDeviceId()85 public int getRoutedDeviceId() { 86 if (mAudioTrack != null) { 87 AudioDeviceInfo routedDevice = mAudioTrack.getRoutedDevice(); 88 return routedDevice != null 89 ? routedDevice.getId() : BuilderBase.ROUTED_DEVICE_ID_DEFAULT; 90 } else { 91 return BuilderBase.ROUTED_DEVICE_ID_DEFAULT; 92 } 93 } 94 95 /* 96 * State 97 */ setupStream(PlayerBuilder builder)98 private int setupStream(PlayerBuilder builder) { 99 mChannelCount = builder.getChannelCount(); 100 mSampleRate = builder.getSampleRate(); 101 mNumExchangeFrames = builder.getNumExchangeFrames(); 102 mPerformanceMode = builder.getJavaPerformanceMode(); 103 int routeDeviceId = builder.getRouteDeviceId(); 104 if (LOG) { 105 Log.i(TAG, "setupStream()"); 106 Log.i(TAG, " chans:" + mChannelCount); 107 Log.i(TAG, " rate: " + mSampleRate); 108 Log.i(TAG, " frames: " + mNumExchangeFrames); 109 Log.i(TAG, " perf mode: " + mPerformanceMode); 110 Log.i(TAG, " route device: " + routeDeviceId); 111 } 112 113 mAudioSource = mSourceProvider.getJavaSource(); 114 mAudioSource.init(mNumExchangeFrames, mChannelCount); 115 116 try { 117 AudioFormat.Builder formatBuilder = new AudioFormat.Builder(); 118 formatBuilder.setEncoding(AudioFormat.ENCODING_PCM_FLOAT) 119 .setSampleRate(mSampleRate) 120 // setChannelIndexMask() won't give us a FAST_PATH 121 // .setChannelIndexMask( 122 // StreamBase.channelCountToIndexMask(mChannelCount)) 123 .setChannelMask(StreamBase.channelCountToOutPositionMask(mChannelCount)); 124 AudioTrack.Builder audioTrackBuilder = new AudioTrack.Builder(); 125 audioTrackBuilder.setAudioFormat(formatBuilder.build()) 126 .setPerformanceMode(mPerformanceMode); 127 mAudioTrack = audioTrackBuilder.build(); 128 129 allocBurstBuffer(); 130 mAudioTrack.setPreferredDevice(builder.getRouteDevice()); 131 132 if (LOG) { 133 Log.i(TAG, " mAudioTrack.getBufferSizeInFrames(): " 134 + mAudioTrack.getBufferSizeInFrames()); 135 Log.i(TAG, " mAudioTrack.getBufferCapacityInFrames() :" 136 + mAudioTrack.getBufferCapacityInFrames()); 137 } 138 } catch (UnsupportedOperationException ex) { 139 Log.e(TAG, "Couldn't open AudioTrack: " + ex); 140 return ERROR_UNSUPPORTED; 141 } catch (java.lang.IllegalArgumentException ex) { 142 Log.e(TAG, "Invalid arguments to AudioTrack.Builder: " + ex); 143 return ERROR_UNSUPPORTED; 144 } 145 146 return OK; 147 } 148 149 @Override teardownStream()150 public int teardownStream() { 151 if (LOG) { 152 Log.i(TAG, "teardownStream()"); 153 } 154 stopStream(); 155 156 waitForStreamThreadToExit(); 157 158 if (mAudioTrack != null) { 159 mAudioTrack.release(); 160 mAudioTrack = null; 161 } 162 163 mChannelCount = 0; 164 mSampleRate = 0; 165 166 //TODO - Retrieve errors from above 167 return OK; 168 } 169 170 /** 171 * Allocates the underlying AudioTrack and begins Playback. 172 * @return True if the stream is successfully started. 173 * 174 * This method returns when the start operation is complete, but before the first 175 * call to the AudioSource.pull() method. 176 */ 177 @Override startStream()178 public int startStream() { 179 if (mAudioTrack == null) { 180 return ERROR_INVALID_STATE; 181 } 182 waitForStreamThreadToExit(); // just to be sure. 183 184 mStreamThread = new Thread(new StreamPlayerRunnable(), "StreamPlayer Thread"); 185 mPlaying = true; 186 mStreamThread.start(); 187 188 return OK; 189 } 190 191 /** 192 * Marks the stream for stopping on the next callback from the underlying system. 193 * 194 * Returns immediately, though a call to AudioSource.pull() may be in progress. 195 */ 196 @Override stopStream()197 public int stopStream() { 198 mPlaying = false; 199 return OK; 200 } 201 202 /** 203 * @return See StreamState constants 204 */ getStreamState()205 public int getStreamState() { 206 //TODO - track state so we can return something meaningful here. 207 return StreamState.UNKNOWN; 208 } 209 210 /** 211 * @return The last error callback result (these must match Oboe). See Oboe constants 212 */ getLastErrorCallbackResult()213 public int getLastErrorCallbackResult() { 214 //TODO - track errors so we can return something meaningful here. 215 return ERROR_UNKNOWN; 216 } 217 218 /** 219 * Gets a timestamp from the audio stream 220 * @param timestamp 221 * @return 222 */ getTimestamp(AudioTimestamp timestamp)223 public boolean getTimestamp(AudioTimestamp timestamp) { 224 return mPlaying ? mAudioTrack.getTimestamp(timestamp) : false; 225 } 226 227 // 228 // StreamPlayerRunnable 229 // 230 /** 231 * Implements the <code>run</code> method for the playback thread. 232 * Gets initial audio data and starts the AudioTrack. Then continuously provides audio data 233 * until the flag <code>mPlaying</code> is set to false (in the stop() method). 234 */ 235 private class StreamPlayerRunnable implements Runnable { 236 @Override run()237 public void run() { 238 final int mNumPlaySamples = mNumExchangeFrames * mChannelCount; 239 if (LOG) { 240 Log.i(TAG, "mNumPlaySamples: " + mNumPlaySamples); 241 } 242 mAudioTrack.play(); 243 while (mPlaying) { 244 mAudioSource.pull(mAudioBuffer, mNumExchangeFrames, mChannelCount); 245 246 onPull(); 247 248 int numSamplesWritten = mAudioTrack.write( 249 mAudioBuffer, 0, mNumPlaySamples, AudioTrack.WRITE_BLOCKING); 250 if (numSamplesWritten < 0) { 251 // error 252 Log.e(TAG, "AudioTrack write error - numSamplesWritten: " + numSamplesWritten); 253 stopStream(); 254 } else if (numSamplesWritten < mNumPlaySamples) { 255 // end of stream 256 if (LOG) { 257 Log.i(TAG, "Stream Complete."); 258 } 259 stopStream(); 260 } 261 } 262 } 263 } 264 } 265