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 @Override getSharingMode()70 public int getSharingMode() { 71 // JAVA Audio API does not support a sharing mode 72 return BuilderBase.SHARING_MODE_NOTSUPPORTED; 73 } 74 75 @Override getChannelCount()76 public int getChannelCount() { 77 return mAudioTrack != null ? mAudioTrack.getChannelCount() : -1; 78 } 79 80 @Override isMMap()81 public boolean isMMap() { 82 // Java Streams are never MMAP 83 return false; 84 } 85 86 /** 87 * Calculate the number of channels taking into account channel mask or channel count. 88 */ calcChannelCount()89 private int calcChannelCount() { 90 return mChannelCount != 0 ? mChannelCount : Integer.bitCount(mChannelMask); 91 } 92 93 /** 94 * Allocates the array for the burst buffer. 95 */ allocBurstBuffer()96 private void allocBurstBuffer() { 97 if (LOG) { 98 Log.d(TAG, "allocBurstBuffer() mNumExchangeFrames:" + mNumExchangeFrames); 99 } 100 101 // pad it by 1 frame. This allows some sources to not have to worry about 102 // handling the end-of-buffer edge case. i.e. a "Guard Point" for interpolation. 103 mAudioBuffer = new float[(mNumExchangeFrames + 1) * calcChannelCount()]; 104 } 105 106 // 107 // Attributes 108 // 109 @Override getRoutedDeviceId()110 public int getRoutedDeviceId() { 111 if (mAudioTrack != null) { 112 AudioDeviceInfo routedDevice = mAudioTrack.getRoutedDevice(); 113 return routedDevice != null 114 ? routedDevice.getId() : BuilderBase.ROUTED_DEVICE_ID_DEFAULT; 115 } else { 116 return BuilderBase.ROUTED_DEVICE_ID_DEFAULT; 117 } 118 } 119 120 /* 121 * State 122 */ setupStream(PlayerBuilder builder)123 private int setupStream(PlayerBuilder builder) { 124 mChannelCount = builder.getChannelCount(); 125 mChannelMask = builder.getChannelMask(); 126 mSampleRate = builder.getSampleRate(); 127 mNumExchangeFrames = builder.getNumExchangeFrames(); 128 mPerformanceMode = builder.getJavaPerformanceMode(); 129 int routeDeviceId = builder.getRouteDeviceId(); 130 if (LOG) { 131 Log.d(TAG, "setupStream()"); 132 Log.d(TAG, " chans:" + mChannelCount); 133 Log.d(TAG, " mask:0x" + Integer.toHexString(mChannelMask)); 134 Log.d(TAG, " rate: " + mSampleRate); 135 Log.d(TAG, " frames: " + mNumExchangeFrames); 136 Log.d(TAG, " perf mode: " + mPerformanceMode); 137 Log.d(TAG, " route device: " + routeDeviceId); 138 } 139 140 mAudioSource = mSourceProvider.getJavaSource(); 141 mAudioSource.init(mNumExchangeFrames, mChannelCount); 142 143 try { 144 AudioFormat.Builder formatBuilder = new AudioFormat.Builder(); 145 formatBuilder.setEncoding(AudioFormat.ENCODING_PCM_FLOAT) 146 .setSampleRate(mSampleRate); 147 // setChannelIndexMask() won't give us a FAST_PATH 148 // .setChannelIndexMask( 149 // StreamBase.channelCountToIndexMask(mChannelCount)) 150 // .setChannelMask(StreamBase.channelCountToOutPositionMask(mChannelCount)); 151 if (mChannelCount != 0) { 152 formatBuilder.setChannelMask( 153 StreamBase.channelCountToOutPositionMask(mChannelCount)); 154 } else { 155 formatBuilder.setChannelMask(mChannelMask); 156 } 157 AudioTrack.Builder audioTrackBuilder = new AudioTrack.Builder(); 158 audioTrackBuilder.setAudioFormat(formatBuilder.build()) 159 .setPerformanceMode(mPerformanceMode); 160 mAudioTrack = audioTrackBuilder.build(); 161 162 allocBurstBuffer(); 163 mAudioTrack.setPreferredDevice(builder.getRouteDevice()); 164 165 if (LOG) { 166 Log.d(TAG, " mAudioTrack.getBufferSizeInFrames(): " 167 + mAudioTrack.getBufferSizeInFrames()); 168 Log.d(TAG, " mAudioTrack.getBufferCapacityInFrames() :" 169 + mAudioTrack.getBufferCapacityInFrames()); 170 } 171 } catch (UnsupportedOperationException ex) { 172 Log.e(TAG, "Couldn't open AudioTrack: " + ex); 173 return ERROR_UNSUPPORTED; 174 } catch (java.lang.IllegalArgumentException ex) { 175 Log.e(TAG, "Invalid arguments to AudioTrack.Builder: " + ex); 176 return ERROR_UNSUPPORTED; 177 } 178 179 return OK; 180 } 181 182 @Override teardownStream()183 public int teardownStream() { 184 if (LOG) { 185 Log.d(TAG, "teardownStream()"); 186 } 187 stopStream(); 188 189 waitForStreamThreadToExit(); 190 191 if (mAudioTrack != null) { 192 mAudioTrack.release(); 193 mAudioTrack = null; 194 } 195 196 mChannelCount = 0; 197 mSampleRate = 0; 198 199 //TODO - Retrieve errors from above 200 return OK; 201 } 202 203 /** 204 * Allocates the underlying AudioTrack and begins Playback. 205 * @return True if the stream is successfully started. 206 * 207 * This method returns when the start operation is complete, but before the first 208 * call to the AudioSource.pull() method. 209 */ 210 @Override startStream()211 public int startStream() { 212 if (mAudioTrack == null) { 213 return ERROR_INVALID_STATE; 214 } 215 waitForStreamThreadToExit(); // just to be sure. 216 217 mStreamThread = new Thread(new StreamPlayerRunnable(), "StreamPlayer Thread"); 218 mPlaying = true; 219 mStreamThread.start(); 220 221 return OK; 222 } 223 224 /** 225 * Marks the stream for stopping on the next callback from the underlying system. 226 * 227 * Returns immediately, though a call to AudioSource.pull() may be in progress. 228 */ 229 @Override stopStream()230 public int stopStream() { 231 mPlaying = false; 232 return OK; 233 } 234 235 /** 236 * @return See StreamState constants 237 */ getStreamState()238 public int getStreamState() { 239 //TODO - track state so we can return something meaningful here. 240 return StreamState.UNKNOWN; 241 } 242 243 /** 244 * @return The last error callback result (these must match Oboe). See Oboe constants 245 */ getLastErrorCallbackResult()246 public int getLastErrorCallbackResult() { 247 //TODO - track errors so we can return something meaningful here. 248 return ERROR_UNKNOWN; 249 } 250 251 /** 252 * Gets a timestamp from the audio stream 253 * @param timestamp 254 * @return 255 */ getTimestamp(AudioTimestamp timestamp)256 public boolean getTimestamp(AudioTimestamp timestamp) { 257 return mPlaying ? mAudioTrack.getTimestamp(timestamp) : false; 258 } 259 260 // 261 // StreamPlayerRunnable 262 // 263 /** 264 * Implements the <code>run</code> method for the playback thread. 265 * Gets initial audio data and starts the AudioTrack. Then continuously provides audio data 266 * until the flag <code>mPlaying</code> is set to false (in the stop() method). 267 */ 268 private class StreamPlayerRunnable implements Runnable { 269 @Override run()270 public void run() { 271 int channelCount = calcChannelCount(); 272 final int mNumPlaySamples = mNumExchangeFrames * channelCount; 273 if (LOG) { 274 Log.d(TAG, "mNumExchangeFrames:" + mNumExchangeFrames); 275 Log.d(TAG, "channelCount:" + channelCount); 276 Log.d(TAG, "mNumPlaySamples: " + mNumPlaySamples); 277 } 278 mAudioTrack.play(); 279 while (mPlaying) { 280 mAudioSource.pull(mAudioBuffer, mNumExchangeFrames, channelCount); 281 282 onPull(); 283 284 int numSamplesWritten = mAudioTrack.write( 285 mAudioBuffer, 0, mNumPlaySamples, AudioTrack.WRITE_BLOCKING); 286 if (numSamplesWritten < 0) { 287 // error 288 Log.e(TAG, "AudioTrack write error - numSamplesWritten: " + numSamplesWritten); 289 stopStream(); 290 } else if (numSamplesWritten < mNumPlaySamples) { 291 // end of stream 292 if (LOG) { 293 Log.d(TAG, "Stream Complete."); 294 } 295 stopStream(); 296 } 297 } 298 } 299 } 300 } 301