source: trunk/src_apollo/org/apollo/gui/WaveFormRenderProccessingUnit.java@ 315

Last change on this file since 315 was 315, checked in by bjn8, 16 years ago

Apollo spin-off added

File size: 10.6 KB
Line 
1package org.apollo.gui;
2
3import java.awt.BasicStroke;
4import java.awt.Graphics2D;
5import java.awt.image.BufferedImage;
6import java.util.LinkedList;
7
8import javax.sound.sampled.AudioFormat;
9
10import org.apollo.audio.ApolloSubjectChangedEvent;
11import org.apollo.mvc.AbstractSubject;
12import org.apollo.mvc.SubjectChangedEvent;
13
14public class WaveFormRenderProccessingUnit {
15
16 /** Limit the amount of render threads. */
17 private RenderThread[] renderThreads = new RenderThread[4];
18
19 private LinkedList<WaveFormRenderTask> taskQueue = new LinkedList<WaveFormRenderTask>();
20
21 private static WaveFormRenderProccessingUnit instance = new WaveFormRenderProccessingUnit(); // single design pattern
22
23 /**
24 * @return The singleton instance of the WaveFormRenderProccessingUnit
25 */
26 public static WaveFormRenderProccessingUnit getInstance() { // single design pattern
27 return instance;
28 }
29
30 private WaveFormRenderProccessingUnit() { // singleton design pattern
31 }
32
33 /**
34 * Queues a task for rendering as soon as possible (asynchronously).
35 *
36 * @param task
37 * The task to queue
38 *
39 * @throws NullPointerException
40 * If task is null.
41 *
42 * @throws IllegalArgumentException
43 * If task has been proccessed before (must not be re-used).
44 * Or if it is already queued for rendering.
45 */
46 public void queueTask(WaveFormRenderTask task) {
47 if (task == null) throw new NullPointerException("task");
48
49 // Ensure not re-using a WaveFormRenderTask
50 if (task.hasStarted()) throw new IllegalArgumentException("task");
51
52 // Add to the queue
53 synchronized (taskQueue) {
54
55 // Check that not already on the queue
56 if (taskQueue.contains(task)) {
57 throw new IllegalArgumentException("task");
58 }
59
60 taskQueue.add(task);
61
62
63 // If there is are dead thread, re-animate it to ensure
64 // the task begins as soon as possible.
65 for (int i = 0; i < renderThreads.length; i++) {
66
67 if (renderThreads[i] == null
68 || renderThreads[i].isFinishing()
69 || !renderThreads[i].isAlive()) {
70
71 renderThreads[i] = new RenderThread(i);
72 renderThreads[i].start();
73 break;
74 }
75
76 }
77
78 }
79
80
81 }
82
83 /**
84 * Cancels a task from rendering.
85 *
86 * @param task
87 * The task to cancel.
88 *
89 * @throws NullPointerException
90 * If task is null.
91 */
92 public void cancelTask(WaveFormRenderTask task) {
93 if (task == null) throw new NullPointerException("task");
94
95 task.keepRendering = false;
96 synchronized (taskQueue) {
97 taskQueue.remove(task);
98 }
99 }
100
101 /**
102 * Render of wave forms is done in a dedicated thread because some graphs may have
103 * to proccess / render a lot of samples depending on the zoom / size of the audio track.
104 *
105 * From a usability perspective, this means that the waveform can be computed accuratly
106 * because responce time is no longer an issue.
107 *
108 * @author Brook Novak
109 *
110 */
111 private class RenderThread extends Thread {
112
113 private WaveFormRenderTask currentTask;
114
115 private final static float AMPLITUDES_PER_PIXEL = 0.5f; // resolution: how many amplitudes per pixel to render
116 private final static int FRAME_RENDER_RATE = 2000; // The approximate amount of frames to render per pass
117
118 private boolean isFinishing = false;
119
120 public boolean isFinishing() {
121 return isFinishing;
122 }
123
124 /**
125 *
126 * @param audioBytes The reference to the audio bytes to render.
127 *
128 * @param frameLength in frames.
129 */
130 public RenderThread(int id) {
131 super("RenderThread" + id);
132 // Give renderering lowest priority
133 setPriority(Thread.MIN_PRIORITY);
134
135 }
136
137 @Override
138 public void run() {
139
140 while (true) {
141 // Grab a task from the queue
142 synchronized(taskQueue) {
143
144 if (taskQueue.isEmpty()) {
145 isFinishing = true;
146 return;
147 }
148 currentTask = taskQueue.remove();
149 }
150
151 // Perform rendering until the task has been cancelled
152 doRender();
153 }
154 }
155
156 private void doRender() {
157 assert(currentTask != null);
158 assert(!currentTask.hasStarted());
159
160 // Quick check
161 if (!currentTask.keepRendering) return;
162
163 currentTask.setState(WaveFormRenderTask.STATE_RENDERING);
164
165 Graphics2D g = null;
166
167 try {
168
169 int halfMaxHeight;
170 int bufferWidth;
171
172 // Create the graphics and prepare the buffer
173
174 synchronized(currentTask.imageBuffer) {
175
176 halfMaxHeight = currentTask.imageBuffer.getHeight() / 2;
177 bufferWidth = currentTask.imageBuffer.getWidth();
178
179 g = currentTask.imageBuffer.createGraphics();
180
181 g.setStroke(new BasicStroke(1.0f));
182
183 // Clear the buffer with transparent pixels
184 g.setBackground(ApolloColorIndexedModels.KEY_COLOR);
185 g.clearRect(0, 0, bufferWidth, currentTask.imageBuffer.getHeight());
186
187 }
188
189 // Render waveforms in chunks - so that can cancel rendering and the
190 // widget can show progress incrementally
191 int lastXPosition = -1;
192 int lastYPosition = -1;
193
194 // Choose how many amplitudes to render in the graph. i.e. waveform resolution
195 int totalAmplitudes = Math.min((int)(bufferWidth * AMPLITUDES_PER_PIXEL), currentTask.frameLength); // get amount of amps to render
196 if (totalAmplitudes == 0) return;
197
198 int currentAmplitude = 0;
199
200 // Calculate the amount of frames to aggregate
201 int aggregationSize = currentTask.frameLength / totalAmplitudes;
202 assert(aggregationSize >= 1);
203
204 // Limit the amount of amplitudes to render (the small chunks) based on the
205 // aggregation size - since it correlation to proccess time.
206 int amplitudeCountPerPass = (FRAME_RENDER_RATE / aggregationSize);
207 if (amplitudeCountPerPass == 0) amplitudeCountPerPass = 1;
208
209 int renderStart;
210 int renderLength;
211
212 // render until finished or cancelled
213 while (currentTask.keepRendering && currentAmplitude < totalAmplitudes) {
214
215 renderStart = currentAmplitude * aggregationSize;
216
217 // At the last pass, render the last remaining bytes
218 renderLength = Math.min(
219 amplitudeCountPerPass * aggregationSize,
220 currentTask.frameLength - renderStart);
221
222 // At the last pass, be sure that the aggregate size does not exeed the render count
223 // so that last samples are not ignored
224 aggregationSize = Math.min(aggregationSize, renderLength);
225
226 // Perform the waveform rendering
227 float[] amps = currentTask.renderer.getSampleAmplitudes(
228 currentTask.audioBytes,
229 currentTask.startFrame + renderStart,
230 renderLength,
231 aggregationSize);
232
233 // Draw the rendered waveform to the buffer
234 synchronized(currentTask.imageBuffer) {
235
236 g.setColor(ApolloColorIndexedModels.WAVEFORM_COLOR);
237
238 for (float h : amps) {
239
240 int x = (int)(currentAmplitude * ((float)bufferWidth / (float)(totalAmplitudes - 1)));
241 int y = halfMaxHeight - (int)(h * halfMaxHeight);
242
243 if (currentAmplitude == 0) { // never draw line on first point
244
245 lastXPosition = 0;
246 lastYPosition = y;
247
248 } else {
249
250 g.drawLine(
251 lastXPosition,
252 lastYPosition,
253 x,
254 y);
255
256 lastXPosition = x;
257 lastYPosition = y;
258
259 }
260
261 currentAmplitude ++;
262 }
263
264 }
265
266
267 } // next pass
268
269 // Lession learnt: do not request lots of requests to repaint
270 // later on the AWT Event queu otherwise it will get congested
271 // and will freeze up the interact for annoying periods of time.
272 currentTask.setState(WaveFormRenderTask.STATE_STOPPED);
273
274 } finally {
275
276 if (g != null) g.dispose();
277
278 // safety
279 if (!currentTask.isStopped())
280 currentTask.setState(WaveFormRenderTask.STATE_STOPPED);
281
282 currentTask = null;
283 }
284 }
285
286 }
287
288 /**
289 * A task descriptor for rendering wave forms via the WaveFormRenderProccessingUnit.
290 *
291 * Raises {@value ApolloSubjectChangedEvent#RENDER_TASK_INVALIDATION_RECOMENDATION}
292 * when the WaveFormRenderProccessingUnit recommends a refresh
293 * of the BufferedImage. This includes when the WaveFormRenderTask state changes
294 *
295 * The BufferedImage is always locked before handled by the render thread.
296 *
297 * Only can use these once... per render task. I.E. Do not re-use
298 *
299 * @author Brook Novak
300 *
301 */
302 public class WaveFormRenderTask extends AbstractSubject {
303
304 private boolean keepRendering = true; // a cancel flag
305
306 private int state = STATE_PENDING;
307
308 private static final int STATE_PENDING = 1;
309 private static final int STATE_RENDERING = 2;
310 private static final int STATE_STOPPED = 3;
311
312 private BufferedImage imageBuffer; // nullified when stopped.
313 private byte[] audioBytes; // nullified when stopped. - Arrays (not contents) are immutable so no need to worry about threading issues with indexes
314 private final int startFrame;
315 private final int frameLength; // in frames
316 private final WaveFormRenderer renderer;
317 private final boolean recommendInvalidations;
318
319 public WaveFormRenderTask(
320 BufferedImage imageBuffer,
321 byte[] audioBytes,
322 AudioFormat format,
323 int startFrame,
324 int frameLength,
325 boolean recommendInvalidations) {
326
327 assert(audioBytes != null);
328 assert(format != null);
329 assert(imageBuffer != null);
330 assert(((startFrame + frameLength) * format.getFrameSize()) <= audioBytes.length);
331
332 this.imageBuffer = imageBuffer;
333 this.audioBytes = audioBytes;
334 this.startFrame = startFrame;
335 this.frameLength = frameLength;
336 this.recommendInvalidations = recommendInvalidations;
337 renderer = new PeakTroughWaveFormRenderer(format);
338 }
339
340 private void setState(int newState) {
341 this.state = newState;
342
343 if (keepRendering) { // invalidate if cancel not requested
344 recommendInvalidation();
345 }
346
347 // Nullify expensive references when stopped so they can be freed by the garbage collector
348 if (state == STATE_STOPPED) {
349 audioBytes = null;
350 imageBuffer = null;
351 }
352
353 }
354
355 /**
356 *
357 * @return
358 * True if this is rendering .. thus being proccessed.
359 */
360 public boolean isRendering() {
361 return state == STATE_RENDERING;
362 }
363
364
365 /**
366 * <b>WARNING:</b> Think about the race conditions ... now cannot no when this may start therefore
367 * it is always safe to lock buffer if in pending state.
368 *
369 * @return
370 * True if this has not started to render
371 */
372 public boolean hasStarted() {
373 return state != STATE_PENDING;
374 }
375
376 /**
377 *
378 * @return
379 * True if has stopped
380 */
381 public boolean isStopped() {
382 return state == STATE_STOPPED;
383
384 }
385
386 private void recommendInvalidation() {
387 if (recommendInvalidations)
388 fireSubjectChangedLaterOnSwingThread(
389 new SubjectChangedEvent(ApolloSubjectChangedEvent.RENDER_TASK_INVALIDATION_RECOMENDATION));
390 }
391
392 }
393
394}
Note: See TracBrowser for help on using the repository browser.