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.StreamBase; 25 26 /** 27 * Implementation of abstract Player class implemented for the Android Java-based audio playback 28 * API, i.e. AudioTrack. 29 */ 30 public class JavaPlayer extends Player { 31 @SuppressWarnings("unused") private static String TAG = JavaPlayer.class.getSimpleName(); 32 @SuppressWarnings("unused") private static final boolean LOG = false; 33 34 /* 35 * Player infrastructure 36 */ 37 /* The AudioTrack for playing the audio stream */ 38 private AudioTrack mAudioTrack; 39 40 private AudioSource mAudioSource; 41 42 // Playback state 43 /** <code>true</code> if currently playing audio data */ 44 private boolean mPlaying; 45 46 /* 47 * Data buffers 48 */ 49 /** Number of FRAMES of audio data in a burst buffer */ 50 private int mNumBufferFrames; 51 52 /** The Burst Buffer. This is the buffer we fill with audio and feed into the AudioTrack. */ 53 private float[] mAudioBuffer; 54 55 // Player-specific extension getAudioTrack()56 public AudioTrack getAudioTrack() { return mAudioTrack; } 57 JavaPlayer(AudioSourceProvider sourceProvider)58 public JavaPlayer(AudioSourceProvider sourceProvider) { 59 super(sourceProvider); 60 mNumBufferFrames = -1; // TODO need error defines 61 } 62 63 @Override getAudioSource()64 public AudioSource getAudioSource() { 65 return mAudioSource; 66 } 67 68 // 69 // Status 70 // 71 @Override isPlaying()72 public boolean isPlaying() { 73 return mPlaying; 74 } 75 76 /** 77 * Allocates the array for the burst buffer. 78 */ allocBurstBuffer()79 private void allocBurstBuffer() { 80 // pad it by 1 frame. This allows some sources to not have to worry about 81 // handling the end-of-buffer edge case. i.e. a "Guard Point" for interpolation. 82 mAudioBuffer = new float[(mNumBufferFrames + 1) * mChannelCount]; 83 } 84 85 // 86 // Attributes 87 // 88 /** 89 * @return The number of frames of audio data contained in the internal buffer. 90 */ 91 @Override getNumBufferFrames()92 public int getNumBufferFrames() { 93 return mNumBufferFrames; 94 } 95 96 @Override getRoutedDeviceId()97 public int getRoutedDeviceId() { 98 if (mAudioTrack != null) { 99 AudioDeviceInfo routedDevice = mAudioTrack.getRoutedDevice(); 100 return routedDevice != null ? routedDevice.getId() : ROUTED_DEVICE_ID_INVALID; 101 } else { 102 return ROUTED_DEVICE_ID_INVALID; 103 } 104 } 105 106 /* 107 * State 108 */ 109 @Override setupStream(int channelCount, int sampleRate, int numBufferFrames)110 public int setupStream(int channelCount, int sampleRate, int numBufferFrames) { 111 if (LOG) { 112 Log.i(TAG, "setupStream(chans:" + channelCount + ", rate:" + sampleRate + 113 ", frames:" + numBufferFrames); 114 } 115 116 mChannelCount = channelCount; 117 mSampleRate = sampleRate; 118 mNumBufferFrames = numBufferFrames; 119 120 mAudioSource = mSourceProvider.getJavaSource(); 121 mAudioSource.init(mNumBufferFrames, mChannelCount); 122 123 try { 124 int bufferSizeInBytes = mNumBufferFrames * mChannelCount 125 * sampleSizeInBytes(AudioFormat.ENCODING_PCM_FLOAT); 126 mAudioTrack = new AudioTrack.Builder() 127 .setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY) 128 .setAudioFormat(new AudioFormat.Builder() 129 .setEncoding(AudioFormat.ENCODING_PCM_FLOAT) 130 .setSampleRate(mSampleRate) 131 .setChannelIndexMask(StreamBase.channelCountToIndexMask(mChannelCount)) 132 // .setChannelMask(channelMask) 133 .build()) 134 .setBufferSizeInBytes(bufferSizeInBytes) 135 .build(); 136 137 allocBurstBuffer(); 138 mAudioTrack.setPreferredDevice(mRouteDevice); 139 } catch (UnsupportedOperationException ex) { 140 if (LOG) { 141 Log.i(TAG, "Couldn't open AudioTrack: " + ex); 142 } 143 mAudioTrack = null; 144 return ERROR_UNSUPPORTED; 145 } 146 147 return OK; 148 } 149 150 @Override teardownStream()151 public int teardownStream() { 152 stopStream(); 153 154 waitForStreamThreadToExit(); 155 156 if (mAudioTrack != null) { 157 mAudioTrack.release(); 158 mAudioTrack = null; 159 } 160 161 mChannelCount = 0; 162 mSampleRate = 0; 163 164 //TODO - Retrieve errors from above 165 return OK; 166 } 167 168 /** 169 * Allocates the underlying AudioTrack and begins Playback. 170 * @return True if the stream is successfully started. 171 * 172 * This method returns when the start operation is complete, but before the first 173 * call to the AudioSource.pull() method. 174 */ 175 @Override startStream()176 public int startStream() { 177 if (mAudioTrack == null) { 178 return ERROR_INVALID_STATE; 179 } 180 waitForStreamThreadToExit(); // just to be sure. 181 182 mStreamThread = new Thread(new StreamPlayerRunnable(), "StreamPlayer Thread"); 183 mPlaying = true; 184 mStreamThread.start(); 185 186 return OK; 187 } 188 189 /** 190 * Marks the stream for stopping on the next callback from the underlying system. 191 * 192 * Returns immediately, though a call to AudioSource.pull() may be in progress. 193 */ 194 @Override stopStream()195 public int stopStream() { 196 mPlaying = false; 197 return OK; 198 } 199 200 /** 201 * Gets a timestamp from the audio stream 202 * @param timestamp 203 * @return 204 */ getTimestamp(AudioTimestamp timestamp)205 public boolean getTimestamp(AudioTimestamp timestamp) { 206 return mPlaying ? mAudioTrack.getTimestamp(timestamp) : false; 207 } 208 209 // 210 // StreamPlayerRunnable 211 // 212 /** 213 * Implements the <code>run</code> method for the playback thread. 214 * Gets initial audio data and starts the AudioTrack. Then continuously provides audio data 215 * until the flag <code>mPlaying</code> is set to false (in the stop() method). 216 */ 217 private class StreamPlayerRunnable implements Runnable { 218 @Override run()219 public void run() { 220 final int numBufferSamples = mNumBufferFrames * mChannelCount; 221 222 mAudioTrack.play(); 223 while (mPlaying) { 224 mAudioSource.pull(mAudioBuffer, mNumBufferFrames, mChannelCount); 225 226 onPull(); 227 228 int numSamplesWritten = mAudioTrack.write( 229 mAudioBuffer,0, numBufferSamples, AudioTrack.WRITE_BLOCKING); 230 if (numSamplesWritten < 0) { 231 // error 232 if (LOG) { 233 Log.i(TAG, "AudioTrack write error: " + numSamplesWritten); 234 } 235 stopStream(); 236 } else if (numSamplesWritten < numBufferSamples) { 237 // end of stream 238 if (LOG) { 239 Log.i(TAG, "Stream Complete."); 240 } 241 stopStream(); 242 } 243 } 244 } 245 } 246 } 247