OLD | NEW |
---|---|
(Empty) | |
1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 package org.chromium.media; | |
6 | |
7 import android.annotation.SuppressLint; | |
8 import android.media.AudioFormat; | |
9 import android.media.AudioManager; | |
10 import android.media.AudioTrack; | |
11 import android.os.Build; | |
12 | |
13 import org.chromium.base.Log; | |
14 import org.chromium.base.VisibleForTesting; | |
15 import org.chromium.base.annotations.CalledByNative; | |
16 import org.chromium.base.annotations.JNINamespace; | |
17 | |
18 import java.nio.ByteBuffer; | |
19 | |
20 @JNINamespace("media") | |
21 class AudioTrackOutputStream { | |
22 // Provide dependency injection points for unit tests. | |
23 interface Callback { | |
24 int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFor mat); | |
25 AudioTrack createAudioTrack(int streamType, int sampleRateInHz, int chan nelConfig, | |
26 int audioFormat, int bufferSizeInBytes, int mode); | |
27 int onMoreData(ByteBuffer audioData, int totalPlayedFrames); | |
28 } | |
29 | |
30 private static final String TAG = "AudioTrackOutput"; | |
31 private Callback mCallback; | |
32 private AudioTrack mAudioTrack; | |
33 private long mNativeAudioTrackOutputStream; | |
34 private int mBufferSizeInBytes; | |
35 | |
36 private ByteBuffer mAudioBuffer; | |
37 private WorkerThread mWorkerThread; | |
38 | |
39 class WorkerThread extends Thread { | |
DaleCurtis
2017/06/15 21:46:32
I think instead of doing the threading in Java you
AndyWu
2017/08/02 01:43:39
TBD
Will look into that. However, from test point
| |
40 private volatile boolean mDone = false; | |
41 | |
42 public void finish() { | |
43 mDone = true; | |
44 } | |
45 | |
46 public void run() { | |
47 while (!mDone) { | |
48 if (!readMoreData()) { | |
49 msleep(10); | |
50 } | |
51 } | |
52 } | |
53 | |
54 private void msleep(int msec) { | |
55 try { | |
56 Thread.sleep(msec); | |
57 } catch (InterruptedException e) { | |
58 } | |
59 } | |
60 } | |
61 | |
62 @CalledByNative | |
63 private static AudioTrackOutputStream create() { | |
64 return new AudioTrackOutputStream(null); | |
65 } | |
66 | |
67 @VisibleForTesting | |
68 static AudioTrackOutputStream create(Callback callback) { | |
69 return new AudioTrackOutputStream(callback); | |
70 } | |
71 | |
72 private AudioTrackOutputStream(Callback callback) { | |
73 mCallback = callback; | |
74 if (mCallback == null) { | |
75 mCallback = new Callback() { | |
76 @Override | |
77 public int getMinBufferSize( | |
78 int sampleRateInHz, int channelConfig, int audioFormat) { | |
79 return AudioTrack.getMinBufferSize(sampleRateInHz, channelCo nfig, audioFormat); | |
80 } | |
81 | |
82 @Override | |
83 public AudioTrack createAudioTrack(int streamType, int sampleRat eInHz, | |
84 int channelConfig, int audioFormat, int bufferSizeInByte s, int mode) { | |
85 return new AudioTrack(streamType, sampleRateInHz, channelCon fig, audioFormat, | |
86 bufferSizeInBytes, mode); | |
87 } | |
88 | |
89 @Override | |
90 public int onMoreData(ByteBuffer audioData, int totalPlayedFrame s) { | |
91 return nativeOnMoreData( | |
92 mNativeAudioTrackOutputStream, audioData, totalPlaye dFrames); | |
93 } | |
94 }; | |
95 } | |
96 } | |
97 | |
98 @SuppressWarnings("deprecation") | |
DaleCurtis
2017/06/15 21:46:32
What's deprecated here?
AndyWu
2017/08/02 01:43:39
AudioFormat.CHANNEL_OUT_7POINT1
| |
99 private int getChannelConfig(int channelCount) { | |
100 switch (channelCount) { | |
101 case 1: | |
102 return AudioFormat.CHANNEL_OUT_MONO; | |
103 case 2: | |
104 return AudioFormat.CHANNEL_OUT_STEREO; | |
105 case 4: | |
106 return AudioFormat.CHANNEL_OUT_QUAD; | |
107 case 6: | |
108 return AudioFormat.CHANNEL_OUT_5POINT1; | |
109 case 8: | |
110 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | |
111 return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND; | |
112 } else { | |
113 return AudioFormat.CHANNEL_OUT_7POINT1; | |
114 } | |
115 default: | |
116 return AudioFormat.CHANNEL_OUT_DEFAULT; | |
117 } | |
118 } | |
119 | |
120 @CalledByNative | |
121 boolean open(int channelCount, int sampleRate, int sampleFormat) { | |
122 int channelConfig = getChannelConfig(channelCount); | |
123 mBufferSizeInBytes = | |
DaleCurtis
2017/06/15 21:46:32
You might consider 3x buffers here to handle momen
AndyWu
2017/08/02 01:43:39
Done.
| |
124 2 * mCallback.getMinBufferSize(sampleRate, channelConfig, sample Format); | |
125 | |
126 if (mAudioTrack != null) mAudioTrack.release(); | |
DaleCurtis
2017/06/15 21:46:32
Assert instead, this should not happen.
AndyWu
2017/08/02 01:43:39
Done.
| |
127 | |
128 try { | |
129 Log.d(TAG, "Crate AudioTrack with sample rate:%d, channel:%d, format :%d ", sampleRate, | |
130 channelConfig, sampleFormat); | |
131 | |
132 mAudioTrack = mCallback.createAudioTrack(AudioManager.STREAM_MUSIC, sampleRate, | |
133 channelConfig, sampleFormat, mBufferSizeInBytes, AudioTrack. MODE_STREAM); | |
134 assert mAudioTrack != null; | |
135 } catch (IllegalArgumentException ile) { | |
136 Log.e(TAG, "Exception creating AudioTrack for playback: ", ile); | |
137 return false; | |
138 } | |
139 | |
140 if (mAudioTrack.getState() == AudioTrack.STATE_UNINITIALIZED) { | |
141 Log.e(TAG, "Cannot create AudioTrack"); | |
142 mAudioTrack = null; | |
143 return false; | |
144 } | |
145 | |
146 return true; | |
147 } | |
148 | |
149 @CalledByNative | |
150 void start(long nativeAudioTrackOutputStream) { | |
151 Log.d(TAG, "AudioTrackOutputStream.start()"); | |
152 if (mWorkerThread != null) return; | |
153 | |
154 mNativeAudioTrackOutputStream = nativeAudioTrackOutputStream; | |
155 | |
156 mAudioBuffer = ByteBuffer.allocateDirect(mBufferSizeInBytes); | |
157 mAudioTrack.play(); | |
158 | |
159 mWorkerThread = new WorkerThread(); | |
160 mWorkerThread.start(); | |
161 } | |
162 | |
163 @CalledByNative | |
164 void stop() { | |
165 Log.d(TAG, "AudioTrackOutputStream.stop()"); | |
166 if (mWorkerThread != null) { | |
167 mWorkerThread.finish(); | |
168 try { | |
169 mWorkerThread.interrupt(); | |
170 mWorkerThread.join(); | |
171 } catch (SecurityException e) { | |
172 Log.e(TAG, "Exception while waiting for AudioTrack worker thread finished: ", e); | |
173 } catch (InterruptedException e) { | |
174 Log.e(TAG, "Exception while waiting for AudioTrack worker thread finished: ", e); | |
175 } | |
176 mWorkerThread = null; | |
177 } | |
178 | |
179 mAudioTrack.pause(); | |
180 mAudioTrack.flush(); | |
181 mNativeAudioTrackOutputStream = 0; | |
182 } | |
183 | |
184 @SuppressWarnings("deprecation") | |
185 @CalledByNative | |
186 void setVolume(double volume) { | |
DaleCurtis
2017/06/15 21:46:32
Is this going to work at all for bitstream output?
AndyWu
2017/08/02 01:43:39
Good catch! It does not work for bitstream output.
| |
187 // Chrome sends the volume in the range [0, 1.0], whereas Android | |
188 // expects the volume to be within [0, getMaxVolume()]. | |
189 float scaledVolume = (float) (volume * mAudioTrack.getMaxVolume()); | |
190 mAudioTrack.setStereoVolume(scaledVolume, scaledVolume); | |
191 } | |
192 | |
193 @CalledByNative | |
194 void close() { | |
195 Log.d(TAG, "AudioTrackOutputStream.close()"); | |
196 if (mAudioTrack != null) mAudioTrack.release(); | |
197 } | |
DaleCurtis
2017/06/15 21:46:32
mAudioTrack = nullptr; note once this is called th
AndyWu
2017/08/02 01:43:39
Done.
| |
198 | |
199 private boolean readMoreData() { | |
chcunningham
2017/06/14 20:32:19
IIUC, the way this is structured you will lose dat
AndyWu
2017/08/02 01:43:39
You are right. However, I am using blocking versio
chcunningham
2017/08/04 19:26:40
From my read of the docs, it seems even the blocki
AndyWu
2017/08/04 21:45:52
Yes, you are right.
See AudioTrackOutputStream.clo
AndyWu
2017/08/05 07:17:10
Done, thanks.
media/base/android/java/src/test/org
| |
200 if (mNativeAudioTrackOutputStream == 0) return false; | |
201 | |
202 int position = mAudioTrack.getPlaybackHeadPosition(); | |
chcunningham
2017/06/14 20:27:21
The documentation mentions this is secretly an uns
AndyWu
2017/08/02 01:43:39
Done.
| |
203 int size = mCallback.onMoreData(mAudioBuffer, position); | |
204 if (size <= 0) { | |
205 return false; | |
206 } | |
207 | |
208 ByteBuffer readOnlyBuffer = mAudioBuffer.asReadOnlyBuffer(); | |
209 int result = writeAudio(readOnlyBuffer, size); | |
210 | |
211 if (result < 0) { | |
212 Log.e(TAG, "AudioTrack.write() failed. Error:" + result); | |
213 return false; | |
214 } else if (result != size) { | |
215 Log.e(TAG, "AudioTrack.write() incomplete. Data size: %d, written si ze: %d", size, | |
216 result); | |
217 return false; | |
218 } | |
219 | |
220 return true; | |
221 } | |
222 | |
223 @SuppressLint("NewApi") | |
224 private int writeAudio(ByteBuffer buffer, int size) { | |
225 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | |
226 return mAudioTrack.write(buffer, size, AudioTrack.WRITE_BLOCKING); | |
227 } else { | |
228 if (buffer.hasArray()) { | |
229 return mAudioTrack.write(buffer.array(), buffer.arrayOffset(), s ize); | |
230 } else { | |
DaleCurtis
2017/06/15 21:46:32
How common is this? This seems like a high frequen
AndyWu
2017/08/02 01:43:40
Done.
| |
231 byte[] array = new byte[size]; | |
232 buffer.get(array); | |
233 return mAudioTrack.write(array, 0, size); | |
234 } | |
235 } | |
236 } | |
237 | |
238 private native int nativeOnMoreData( | |
239 long nativeAudioTrackOutputStream, ByteBuffer audioData, int totalPl ayedFrames); | |
240 private native void nativeOnError(long nativeAudioTrackOutputStream); | |
241 } | |
OLD | NEW |