diff options
author | Thomas Martitz <kugel@rockbox.org> | 2013-03-16 22:35:54 +0100 |
---|---|---|
committer | Thomas Martitz <kugel@rockbox.org> | 2013-04-01 11:26:12 +0200 |
commit | 9f242e7be4f301e965d0bf35908a9bcaacdfdcae (patch) | |
tree | 55952fcaa07f6cdb0657f255c6cbf28cbd2c2c95 | |
parent | 9add11d79a5e1516908a4935a3e538880ff38378 (diff) | |
download | rockbox-9f242e7be4f301e965d0bf35908a9bcaacdfdcae.tar.gz rockbox-9f242e7be4f301e965d0bf35908a9bcaacdfdcae.zip |
android: Rewrite PCM playback without OnPlaybackPositionUpdateListener.
The old way actually mis-used the API (I misunderstood the docs) because
it specified the marker position as a "low buffer watermark" but instead of a
future playback head position.
The replacement is a simple thread that writes the data regardless of the
filling level of the buffer (write() will just block) and polls the playback
state periodically.
Change-Id: If29237cee4ce78dc42f5a8320878bab0cafe78f7
Reviewed-on: http://gerrit.rockbox.org/422
Tested-by: Dominik Riebeling <Dominik.Riebeling@gmail.com>
Reviewed-by: Thomas Martitz <kugel@rockbox.org>
-rw-r--r-- | android/src/org/rockbox/RockboxPCM.java | 158 | ||||
-rw-r--r-- | firmware/target/hosted/android/pcm-android.c | 20 |
2 files changed, 115 insertions, 63 deletions
diff --git a/android/src/org/rockbox/RockboxPCM.java b/android/src/org/rockbox/RockboxPCM.java index 542860a38e..10caa772b6 100644 --- a/android/src/org/rockbox/RockboxPCM.java +++ b/android/src/org/rockbox/RockboxPCM.java | |||
@@ -43,10 +43,6 @@ public class RockboxPCM extends AudioTrack | |||
43 | AudioFormat.CHANNEL_OUT_STEREO; | 43 | AudioFormat.CHANNEL_OUT_STEREO; |
44 | private static final int encoding = | 44 | private static final int encoding = |
45 | AudioFormat.ENCODING_PCM_16BIT; | 45 | AudioFormat.ENCODING_PCM_16BIT; |
46 | /* 32k is plenty, but some devices may have a higher minimum */ | ||
47 | private static final int buf_len = | ||
48 | Math.max(32<<10, 4*getMinBufferSize(samplerate, channels, encoding)); | ||
49 | |||
50 | private AudioManager audiomanager; | 46 | private AudioManager audiomanager; |
51 | private RockboxService rbservice; | 47 | private RockboxService rbservice; |
52 | private byte[] raw_data; | 48 | private byte[] raw_data; |
@@ -58,14 +54,20 @@ public class RockboxPCM extends AudioTrack | |||
58 | private float curpcmvolume = 0; | 54 | private float curpcmvolume = 0; |
59 | private float pcmrange; | 55 | private float pcmrange; |
60 | 56 | ||
57 | /* 8k is plenty, but some devices may have a higher minimum. | ||
58 | * 8k represents 125ms of audio */ | ||
59 | private static final int chunkSize = | ||
60 | Math.max(8<<10, getMinBufferSize(samplerate, channels, encoding)); | ||
61 | Streamer streamer; | ||
62 | |||
61 | public RockboxPCM() | 63 | public RockboxPCM() |
62 | { | 64 | { |
63 | super(streamtype, samplerate, channels, encoding, | 65 | super(streamtype, samplerate, channels, encoding, |
64 | buf_len, AudioTrack.MODE_STREAM); | 66 | chunkSize, AudioTrack.MODE_STREAM); |
65 | HandlerThread ht = new HandlerThread("audio thread", | 67 | |
66 | Process.THREAD_PRIORITY_URGENT_AUDIO); | 68 | streamer = new Streamer(chunkSize); |
67 | ht.start(); | 69 | streamer.start(); |
68 | raw_data = new byte[buf_len]; /* in shorts */ | 70 | raw_data = new byte[chunkSize]; /* in shorts */ |
69 | Arrays.fill(raw_data, (byte) 0); | 71 | Arrays.fill(raw_data, (byte) 0); |
70 | 72 | ||
71 | /* find cleaner way to get context? */ | 73 | /* find cleaner way to get context? */ |
@@ -79,14 +81,80 @@ public class RockboxPCM extends AudioTrack | |||
79 | 81 | ||
80 | setupVolumeHandler(); | 82 | setupVolumeHandler(); |
81 | postVolume(audiomanager.getStreamVolume(streamtype)); | 83 | postVolume(audiomanager.getStreamVolume(streamtype)); |
82 | refillmark = buf_len / 4; /* higher values don't work on many devices */ | 84 | } |
83 | 85 | ||
84 | /* getLooper() returns null if thread isn't running */ | 86 | /** |
85 | while(!ht.isAlive()) Thread.yield(); | 87 | * This class does the actual playback work. Its run() method |
86 | setPlaybackPositionUpdateListener( | 88 | * continuously writes data to the AudioTrack. This operation blocks |
87 | new PCMListener(buf_len / 2), new Handler(ht.getLooper())); | 89 | * and should therefore be run on its own thread. |
88 | refillmark = bytes2frames(refillmark); | 90 | */ |
89 | } | 91 | private class Streamer extends Thread |
92 | { | ||
93 | byte[] buffer; | ||
94 | private boolean quit = false; | ||
95 | |||
96 | Streamer(int bufsize) | ||
97 | { | ||
98 | super("audio thread"); | ||
99 | buffer = new byte[bufsize]; | ||
100 | } | ||
101 | |||
102 | @Override | ||
103 | public void run() | ||
104 | { | ||
105 | /* THREAD_PRIORITY_URGENT_AUDIO can only be specified via | ||
106 | * setThreadPriority(), and not via thread.setPriority(). This is | ||
107 | * also how the android's HandlerThread class implements it */ | ||
108 | Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); | ||
109 | while (!quit) | ||
110 | { | ||
111 | switch(getPlayState()) | ||
112 | { | ||
113 | case PLAYSTATE_PLAYING: | ||
114 | nativeWrite(buffer, buffer.length); | ||
115 | break; | ||
116 | case PLAYSTATE_PAUSED: | ||
117 | case PLAYSTATE_STOPPED: | ||
118 | { | ||
119 | synchronized (this) | ||
120 | { | ||
121 | try | ||
122 | { | ||
123 | wait(); | ||
124 | } | ||
125 | catch (InterruptedException e) { e.printStackTrace(); } | ||
126 | break; | ||
127 | } | ||
128 | } | ||
129 | } | ||
130 | } | ||
131 | } | ||
132 | |||
133 | synchronized void quit() | ||
134 | { | ||
135 | quit = true; | ||
136 | notify(); | ||
137 | } | ||
138 | |||
139 | synchronized void kick() | ||
140 | { | ||
141 | notify(); | ||
142 | } | ||
143 | |||
144 | void quitAndJoin() | ||
145 | { | ||
146 | while(true) | ||
147 | { | ||
148 | try | ||
149 | { | ||
150 | quit(); | ||
151 | join(); | ||
152 | return; | ||
153 | } | ||
154 | catch (InterruptedException e) { } | ||
155 | } | ||
156 | } | ||
157 | } | ||
90 | 158 | ||
91 | private native void postVolumeChangedEvent(int volume); | 159 | private native void postVolumeChangedEvent(int volume); |
92 | 160 | ||
@@ -164,14 +232,22 @@ public class RockboxPCM extends AudioTrack | |||
164 | service.startForeground(); | 232 | service.startForeground(); |
165 | if (getPlayState() == AudioTrack.PLAYSTATE_STOPPED) | 233 | if (getPlayState() == AudioTrack.PLAYSTATE_STOPPED) |
166 | { | 234 | { |
167 | setNotificationMarkerPosition(refillmark); | ||
168 | /* need to fill with silence before starting playback */ | 235 | /* need to fill with silence before starting playback */ |
169 | write(raw_data, 0, raw_data.length); | 236 | write(raw_data, 0, raw_data.length); |
170 | } | 237 | } |
171 | play(); | 238 | play(); |
172 | } | 239 | } |
173 | } | 240 | } |
174 | 241 | ||
242 | @Override | ||
243 | public void play() throws IllegalStateException | ||
244 | { | ||
245 | super.play(); | ||
246 | /* when stopped or paused the streamer is in a wait() state. need | ||
247 | * it to wake it up */ | ||
248 | streamer.kick(); | ||
249 | } | ||
250 | |||
175 | @Override | 251 | @Override |
176 | public synchronized void stop() throws IllegalStateException | 252 | public synchronized void stop() throws IllegalStateException |
177 | { | 253 | { |
@@ -195,7 +271,15 @@ public class RockboxPCM extends AudioTrack | |||
195 | RockboxService.getInstance().sendBroadcast(widgetUpdate); | 271 | RockboxService.getInstance().sendBroadcast(widgetUpdate); |
196 | RockboxService.getInstance().stopForeground(); | 272 | RockboxService.getInstance().stopForeground(); |
197 | } | 273 | } |
198 | 274 | ||
275 | @Override | ||
276 | public void release() | ||
277 | { | ||
278 | super.release(); | ||
279 | /* stop streamer if this AudioTrack is destroyed by whomever */ | ||
280 | streamer.quitAndJoin(); | ||
281 | } | ||
282 | |||
199 | public int setStereoVolume(float leftVolume, float rightVolume) | 283 | public int setStereoVolume(float leftVolume, float rightVolume) |
200 | { | 284 | { |
201 | curpcmvolume = leftVolume; | 285 | curpcmvolume = leftVolume; |
@@ -231,40 +315,4 @@ public class RockboxPCM extends AudioTrack | |||
231 | } | 315 | } |
232 | 316 | ||
233 | public native int nativeWrite(byte[] temp, int len); | 317 | public native int nativeWrite(byte[] temp, int len); |
234 | |||
235 | private class PCMListener implements OnPlaybackPositionUpdateListener | ||
236 | { | ||
237 | byte[] pcm_data; | ||
238 | public PCMListener(int _refill_bufsize) | ||
239 | { | ||
240 | pcm_data = new byte[_refill_bufsize]; | ||
241 | } | ||
242 | |||
243 | public void onMarkerReached(AudioTrack track) | ||
244 | { | ||
245 | /* push new data to the hardware */ | ||
246 | RockboxPCM pcm = (RockboxPCM)track; | ||
247 | int result = -1; | ||
248 | result = pcm.nativeWrite(pcm_data, pcm_data.length); | ||
249 | if (result >= 0) | ||
250 | { | ||
251 | switch(getPlayState()) | ||
252 | { | ||
253 | case PLAYSTATE_PLAYING: | ||
254 | case PLAYSTATE_PAUSED: | ||
255 | setNotificationMarkerPosition(pcm.refillmark); | ||
256 | break; | ||
257 | case PLAYSTATE_STOPPED: | ||
258 | Logger.d("Stopped"); | ||
259 | break; | ||
260 | } | ||
261 | } | ||
262 | else /* stop on error */ | ||
263 | stop(); | ||
264 | } | ||
265 | |||
266 | public void onPeriodicNotification(AudioTrack track) | ||
267 | { | ||
268 | } | ||
269 | } | ||
270 | } | 318 | } |
diff --git a/firmware/target/hosted/android/pcm-android.c b/firmware/target/hosted/android/pcm-android.c index 0428e5f541..0608e971a7 100644 --- a/firmware/target/hosted/android/pcm-android.c +++ b/firmware/target/hosted/android/pcm-android.c | |||
@@ -58,7 +58,6 @@ static inline void unlock_audio(void) | |||
58 | pthread_mutex_unlock(&audio_lock_mutex); | 58 | pthread_mutex_unlock(&audio_lock_mutex); |
59 | } | 59 | } |
60 | 60 | ||
61 | |||
62 | /* | 61 | /* |
63 | * write pcm samples to the hardware. Calls AudioTrack.write directly (which | 62 | * write pcm samples to the hardware. Calls AudioTrack.write directly (which |
64 | * is usually a blocking call) | 63 | * is usually a blocking call) |
@@ -93,18 +92,23 @@ Java_org_rockbox_RockboxPCM_nativeWrite(JNIEnv *env, jobject this, | |||
93 | (*env)->SetByteArrayRegion(env, temp_array, 0, | 92 | (*env)->SetByteArrayRegion(env, temp_array, 0, |
94 | transfer_size, (jbyte*)pcm_data_start); | 93 | transfer_size, (jbyte*)pcm_data_start); |
95 | 94 | ||
96 | ret = (*env)->CallIntMethod(env, this, write_method, | ||
97 | temp_array, 0, transfer_size); | ||
98 | |||
99 | if (new_buffer) | 95 | if (new_buffer) |
100 | { | 96 | { |
101 | new_buffer = false; | 97 | new_buffer = false; |
102 | pcm_play_dma_status_callback(PCM_DMAST_STARTED); | 98 | pcm_play_dma_status_callback(PCM_DMAST_STARTED); |
103 | |||
104 | /* NOTE: might need to release the mutex and sleep here if the | ||
105 | buffer is shorter than the required buffer (like pcm-sdl.c) to | ||
106 | have the mixer clocked at a regular interval */ | ||
107 | } | 99 | } |
100 | /* SetByteArrayRegion copies, which enables us to unlock audio. This | ||
101 | * is good because the below write() call almost certainly block. | ||
102 | * This allows the mixer to be clocked at a regular interval which vastly | ||
103 | * improves responsiveness when pausing/stopping playback */ | ||
104 | unlock_audio(); | ||
105 | ret = (*env)->CallIntMethod(env, this, write_method, | ||
106 | temp_array, 0, transfer_size); | ||
107 | lock_audio(); | ||
108 | |||
109 | /* check if still playing. might have changed during the write() call */ | ||
110 | if (!pcm_is_playing()) | ||
111 | break; | ||
108 | 112 | ||
109 | if (ret < 0) | 113 | if (ret < 0) |
110 | { | 114 | { |