source: trunk/src/org/apollo/gui/WaveFormRenderProccessingUnit.java@ 1102

Last change on this file since 1102 was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

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