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