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