1 package com.hammurapi.jcapture;
2 
3 import java.awt.AlphaComposite;
4 import java.awt.BorderLayout;
5 import java.awt.Color;
6 import java.awt.Component;
7 import java.awt.Dimension;
8 import java.awt.Graphics;
9 import java.awt.Graphics2D;
10 import java.awt.GridBagConstraints;
11 import java.awt.GridBagLayout;
12 import java.awt.Image;
13 import java.awt.Insets;
14 import java.awt.Point;
15 import java.awt.Rectangle;
16 import java.awt.RenderingHints;
17 import java.awt.Toolkit;
18 import java.awt.event.ActionEvent;
19 import java.awt.event.ActionListener;
20 import java.awt.event.MouseAdapter;
21 import java.awt.event.MouseEvent;
22 import java.awt.event.WindowAdapter;
23 import java.awt.event.WindowEvent;
24 import java.awt.image.BufferedImage;
25 import java.io.File;
26 import java.io.IOException;
27 import java.lang.ref.Reference;
28 import java.lang.ref.SoftReference;
29 import java.util.ArrayList;
30 import java.util.Collections;
31 import java.util.List;
32 import java.util.concurrent.Executor;
33 
34 import javax.sound.sampled.AudioFormat;
35 import javax.sound.sampled.AudioInputStream;
36 import javax.sound.sampled.AudioSystem;
37 import javax.sound.sampled.DataLine;
38 import javax.sound.sampled.SourceDataLine;
39 import javax.swing.AbstractAction;
40 import javax.swing.Action;
41 import javax.swing.JButton;
42 import javax.swing.JCheckBox;
43 import javax.swing.JCheckBoxMenuItem;
44 import javax.swing.JComponent;
45 import javax.swing.JFrame;
46 import javax.swing.JMenuItem;
47 import javax.swing.JOptionPane;
48 import javax.swing.JPanel;
49 import javax.swing.JPopupMenu;
50 import javax.swing.JScrollPane;
51 import javax.swing.JTable;
52 import javax.swing.JToolTip;
53 import javax.swing.ListSelectionModel;
54 import javax.swing.ProgressMonitor;
55 import javax.swing.SwingWorker;
56 import javax.swing.Timer;
57 import javax.swing.event.PopupMenuEvent;
58 import javax.swing.event.PopupMenuListener;
59 import javax.swing.table.DefaultTableModel;
60 import javax.swing.table.TableCellRenderer;
61 import javax.swing.table.TableModel;
62 
63 import com.hammurapi.jcapture.ShapeImpl.ImageImpl;
64 import com.hammurapi.jcapture.VideoEncoder.Fragment;
65 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame;
66 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape;
67 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ImageReference;
68 import com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.ShapeContent;
69 
70 public class MovieEditorDialog extends javax.swing.JDialog {
71 
72 	private static final double DECIBELS_PER_PIXEL = 2.0;
73 	private static final double NORMALIZED_LEVEL = 0.95;
74 	private static final int AUDIO_CELL_HEIGHT = 50;
75 	private static final int MEDIAN = AUDIO_CELL_HEIGHT/2;
76 	int minCellDimension = 10;
77 	int minToolTipImageDimension = 150;
78 
79 	int splashIndex = -1;
80 
81 	double coeff;
82 
83 	private static Color INACTIVE_COLOR = new Color(230, 230, 230);
84 	private static Color ACTIVE_COLOR = Color.white;
85 	private static Color SELECTED_COLOR = new Color(0, 0, 255, 70);
86 	private static Color FOCUSED_COLOR = new Color(0, 0, 255, 100);
87 	private static Color PLAYING_COLOR = new Color(255, 0, 0, 100);
88 
89 	private static Color SPLASH_COLOR = new Color(0, 255, 0, 127);
90 
91 	private static Color SOUND_COLOR = new Color(0, 0, 127);
92 	private static Color DELETED_SOUND_COLOR = new Color(100, 100, 100);
93 
94 	private JButton saveButton;
95 	private JPanel contentPanel;
96 	private JScrollPane timeLineScrollPane;
97 	private JCheckBox normalizeVolumeCheckBox;
98 	private JPanel frameCanvas;
99 	JTable timeLineTable;
100 	private JButton discardButton;
101 	private int focusColumn = 0;
102 	private int playingColumn = -1;
103 	private Image mouseImage;
104 	private double maxVolume = -1;
105 	FrameEntry[] frameEntries;
106 	int cellWidth;
107 	int cellHeight;
108 	int toolTipImageWidth;
109 	int toolTipImageHeight;
110 	boolean hasAudio;
111 	private Movie movie;
112 
113 	int numChannels;
114 	int validBits;
115 	long sampleRate;
116 	Timer[] playTimera = {null};
117 	private Executor backgroundProcessor;
118 	private double inactivityInterval;
119 	private String imageFormat;
120 
121 	private class FrameEntry {
122 
123 		// Not null for first frames in fragments indicating that
124 		// Indicating that it's time to open a new audio file.
125 		File audioFile;
126 
127 		boolean mouseMoved;
128 
129 		// Scaled samples for painting - not real ones.
130 		// idx, {min, max}
131 		double[] audioSamples;
132 
133 		// Number of real samples falling to this frame.
134 		int audioSamplesInFrame;
135 
136 		boolean isDeleted;
137 
138 		Reference<BufferedImage> toolTipImageRef;
139 
140 		Reference<BufferedImage> frameImageRef;
141 
142 		// row, selected, focus
143 		private JPanel[][][] canvases = {
144 				{ {new FrameCellCanvas(false, false), new FrameCellCanvas(false, true)}, {new FrameCellCanvas(true, false), new FrameCellCanvas(true, true)} },
145 				{ {new AudioCellCanvas(false, false), new AudioCellCanvas(false, true)}, {new AudioCellCanvas(true, false), new AudioCellCanvas(true, true)} }
146 		};
147 
148 		class CellCanvas extends JPanel {
149 
150 			boolean selected;
151 			boolean hasFocus;
152 
CellCanvas(boolean selected, boolean hasFocus)153 			CellCanvas(boolean selected, boolean hasFocus) {
154 				this.selected = selected;
155 				this.hasFocus = hasFocus;
156 			}
157 
158 		}
159 
160 		class FrameCellCanvas extends CellCanvas {
161 
FrameCellCanvas(boolean selected, boolean hasFocus)162 			FrameCellCanvas(boolean selected, boolean hasFocus) {
163 				super(selected, hasFocus);
164 			}
165 
166 			@Override
paintComponent(Graphics g)167 			public void paintComponent(Graphics g) {
168 				super.paintComponent(g);
169 				paintFrame(this, g, selected, hasFocus);
170 			}
171 
172 		}
173 
174 		class AudioCellCanvas extends CellCanvas {
175 
AudioCellCanvas(boolean selected, boolean hasFocus)176 			AudioCellCanvas(boolean selected, boolean hasFocus) {
177 				super(selected, hasFocus);
178 			}
179 
180 			@Override
paintComponent(Graphics g)181 			public void paintComponent(Graphics g) {
182 				super.paintComponent(g);
183 				paintAudio(this, g, selected, hasFocus);
184 			}
185 
186 		}
187 
188 		int idx;
189 		int delta;
190 
191 		Frame frame;
192 
getToolTipImage()193 		BufferedImage getToolTipImage() throws IOException {
194 			BufferedImage ret = toolTipImageRef==null ? null : toolTipImageRef.get();
195 			if (ret == null) {
196 				BufferedImage image = getImage();
197 		    	ret = new BufferedImage(toolTipImageWidth, toolTipImageHeight, image.getType());
198 		    	Graphics2D g = ret.createGraphics();
199 		    	g.setComposite(AlphaComposite.Src);
200 		    	g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
201 		    	g.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
202 		    	g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
203 		    	g.drawImage(image, 0, 0, ret.getWidth(), ret.getHeight(), null);
204 		    	g.dispose();
205 				toolTipImageRef = new SoftReference<BufferedImage>(ret);
206 			}
207 			return ret;
208 
209 		}
210 
paintFrame(FrameCellCanvas frameCellCanvas, Graphics g, boolean selected, boolean hasFocus)211 		void paintFrame(FrameCellCanvas frameCellCanvas, Graphics g, boolean selected, boolean hasFocus) {
212 			g.setColor(frame.isActive() ? ACTIVE_COLOR : INACTIVE_COLOR);
213 			g.fillRect(0, 0, frameCellCanvas.getWidth(), frameCellCanvas.getHeight());
214 
215 			if (idx==splashIndex) {
216 				g.setColor(SPLASH_COLOR);
217 				g.fillRect(1, 1, frameCellCanvas.getWidth()-2, frameCellCanvas.getHeight()-2);
218 
219 			}
220 
221 			if (frame.getMousePointer()!=null) {
222 				int mx = (int) (frame.getMousePointer().getX()*(frameCellCanvas.getWidth()-3)/frame.getSize().getWidth())+1;
223 				int my = (int) (frame.getMousePointer().getY()*(frameCellCanvas.getHeight()-3)/frame.getSize().getHeight())+1;
224 				g.setColor(mouseMoved ? Color.BLACK : Color.GRAY);
225 				g.fillRect(mx, my, 2, 2);
226 			}
227 
228 			if (isDeleted) {
229 				g.setColor(Color.RED);
230 				g.drawLine(2, 2, frameCellCanvas.getWidth()-2, frameCellCanvas.getHeight()-2);
231 				g.drawLine(frameCellCanvas.getWidth()-2, 2, 2, frameCellCanvas.getHeight()-2);
232 			}
233 
234 			decorate(frameCellCanvas, g, selected, hasFocus);
235 		}
236 
paintAudio(AudioCellCanvas audioCellCanvas, Graphics g, boolean selected, boolean hasFocus)237 		void paintAudio(AudioCellCanvas audioCellCanvas, Graphics g, boolean selected, boolean hasFocus) {
238 			g.setColor(frame.isActive() ? ACTIVE_COLOR : INACTIVE_COLOR);
239 			g.fillRect(0, 0, audioCellCanvas.getWidth(), audioCellCanvas.getHeight());
240 
241 			if (audioSamples!=null) {
242 				for (int i = 0; i<audioCellCanvas.getWidth(); ++i) {
243 					g.setColor(isDeleted ? DELETED_SOUND_COLOR : SOUND_COLOR);
244 					int volume = (int) (20.0*Math.log10(coeff*audioSamples[Math.min(i, audioSamples.length-1)]+1)/DECIBELS_PER_PIXEL);
245 					g.drawLine(i, MEDIAN - volume, i,  MEDIAN + volume);
246 				}
247 			}
248 
249 			decorate(audioCellCanvas, g, selected, hasFocus);
250 		}
251 
decorate(JComponent component, Graphics g, boolean selected, boolean hasFocus)252 		private void decorate(JComponent component, Graphics g, boolean selected, boolean hasFocus) {
253 			if (idx==playingColumn) {
254 				g.setColor(PLAYING_COLOR);
255 				Rectangle bounds = g.getClipBounds();
256 				g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
257 			} else if (hasFocus) {
258 				g.setColor(FOCUSED_COLOR);
259 				Rectangle bounds = g.getClipBounds();
260 				g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
261 			} else if (selected) {
262 				g.setColor(SELECTED_COLOR);
263 				Rectangle bounds = g.getClipBounds();
264 				g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
265 			}
266 		}
267 
getImage()268 		BufferedImage getImage() throws IOException {
269 			BufferedImage ret = frameImageRef==null ? null : frameImageRef.get();
270 			if (ret == null) {
271 				int startIdx = idx;
272 				while (startIdx>0 && !coversEverything(startIdx)) {
273 					--startIdx;
274 				}
275 				int deltaArea = 0;
276 				ret = new BufferedImage(frame.getSize().width, frame.getSize().height, shapeImage(frameEntries[startIdx].frame.getShapes().get(0)).getType());
277 				Graphics2D g = ret.createGraphics();
278 				for (int i=startIdx; i<=idx; ++i) {
279 					for (Shape shape: frameEntries[i].frame.getShapes()) {
280 						BufferedImage si = shapeImage(shape);
281 						g.drawImage(si, shape.getLocation().x, shape.getLocation().y, null);
282 						if (i==idx) {
283 							deltaArea+=si.getWidth()*si.getHeight();
284 						}
285 					}
286 				}
287 				delta = (int) (100.0*deltaArea/(frame.getSize().width * frame.getSize().height));
288 				if (frame.getMousePointer()!=null) {
289 					g.drawImage(mouseImage, frame.getMousePointer().x, frame.getMousePointer().y, null);
290 				}
291 				frameImageRef = new SoftReference<BufferedImage>(ret);
292 			}
293 			return ret;
294 		}
295 
shapeImage(Shape shape)296 		private BufferedImage shapeImage(Shape shape) throws IOException {
297 			ShapeContent shapeContent = shape.getContent();
298 			if (shapeContent instanceof ImageReference) {
299 				return ((ImageReference) shapeContent).getImage().getImage().getImage();
300 			}
301 			return ((com.hammurapi.jcapture.VideoEncoder.Fragment.Frame.Shape.Image) shapeContent).getImage().getImage();
302 		}
303 
coversEverything(int entryIdx)304 		boolean coversEverything(int entryIdx) {
305 			for (Shape shape: frameEntries[entryIdx].frame.getShapes()) {
306 				if (shape.getContent().coversEverything()) {
307 					return true;
308 				}
309 			}
310 			return false;
311 		}
312 
getCellRendererComponent(int row, boolean isSelected, boolean hasFocus)313 		public Component getCellRendererComponent(int row, boolean isSelected, boolean hasFocus) {
314 			return canvases[row][isSelected ? 1 : 0][hasFocus ? 1 : 0];
315 		}
316 
317 	}
318 
MovieEditorDialog( final JFrame frame, final Movie movie, final Executor backgroundProcessor, double inactivityInterval, String imageFormat)319 	public MovieEditorDialog(
320 			final JFrame frame,
321 			final Movie movie,
322 			final Executor backgroundProcessor,
323 			double inactivityInterval,
324 			String imageFormat) {
325 
326 		super(frame, "Movie editor ("+movie+")");
327 		frame.setAlwaysOnTop(false);
328 		frame.setVisible(false);
329 
330 		this.movie = movie;
331 		this.backgroundProcessor = backgroundProcessor;
332 		this.inactivityInterval = inactivityInterval;
333 		this.imageFormat = imageFormat;
334 
335 		setModal(true);
336 		setIconImage(frame.getIconImage());
337 
338 		mouseImage = Toolkit.getDefaultToolkit().getImage(getClass().getResource("mouse.png"));
339 
340 		double aspectRatio = (double) movie.getFrameDimension().getWidth()/(double) movie.getFrameDimension().getHeight();
341 
342 		if (aspectRatio>1) {
343 			cellHeight = minCellDimension;
344 			cellWidth = (int) Math.round(aspectRatio*cellHeight);
345 
346 			toolTipImageHeight = minToolTipImageDimension;
347 			toolTipImageWidth = (int) Math.round(aspectRatio*toolTipImageHeight);
348 		} else {
349 			cellWidth = minCellDimension;
350 			cellHeight = (int) Math.round((double) cellWidth/aspectRatio);
351 
352 			toolTipImageWidth = minToolTipImageDimension;
353 			toolTipImageHeight = (int) Math.round((double) toolTipImageWidth/aspectRatio);
354 		}
355 
356 		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
357 
358 		addWindowListener(new WindowAdapter() {
359             public void windowClosing(WindowEvent e) {
360                 int confirmed = JOptionPane.showConfirmDialog(null,
361                                 "Are you sure you want to exit and discard the movie?", "User Confirmation",
362                                 JOptionPane.YES_NO_OPTION);
363                 if (confirmed == JOptionPane.YES_OPTION) {
364                 	dispose();
365 					getOwner().setVisible(false);
366                 }
367             }
368 		});
369 
370 		SwingWorker<Boolean, Long> loader = new SwingWorker<Boolean, Long>() {
371 
372 			@Override
373 			protected Boolean doInBackground() throws Exception {
374 				int totalFrames = 0;
375 				for (Fragment fr: movie.getFragments()) {
376 					if (fr.getAudio()!=null) {
377 						hasAudio = true;
378 					}
379 					totalFrames+=fr.getFrames().size();
380 				}
381 
382 				ProgressMonitor progressMonitor = new ProgressMonitor(frame, "Loading frames", "Loading movie frames", 0, totalFrames);
383 
384 				try {
385 					frameEntries = new FrameEntry[totalFrames];
386 					int idx = 0;
387 					double audioSamplesPerFrame = -1;
388 					Point prevMouse = null;
389 					for (Fragment fr: movie.getFragments()) {
390 						WavFile wavFile = fr.getAudio()==null ? null : WavFile.openWavFile(fr.getAudio());
391 						if (wavFile!=null) {
392 							audioSamplesPerFrame = wavFile.getSampleRate()/movie.getFramesPerSecond();
393 							numChannels = wavFile.getNumChannels();
394 							validBits = wavFile.getValidBits();
395 							sampleRate = wavFile.getSampleRate();
396 						}
397 						int audioFramesRead = 0;
398 						int framePosition = 0;
399 						for (Frame frm: fr.getFrames()) {
400 							if (progressMonitor.isCanceled()) {
401 								return false;
402 							}
403 							frameEntries[idx] = new FrameEntry();
404 							frameEntries[idx].frame = frm;
405 							frameEntries[idx].idx = idx;
406 							if (frm.getMousePointer()!=null) {
407 								frameEntries[idx].mouseMoved = !frm.getMousePointer().equals(prevMouse);
408 							}
409 							prevMouse = frm.getMousePointer();
410 
411 							if (framePosition == 0) {
412 								frameEntries[idx].audioFile = fr.getAudio();
413 							}
414 
415 							if (wavFile!=null && wavFile.getFramesRemaining()>0) {
416 								frameEntries[idx].audioSamplesInFrame = (int) ((framePosition+1)*audioSamplesPerFrame-audioFramesRead);
417 								frameEntries[idx].audioSamples = new double[cellWidth];
418 								double[][] sampleBuffer = new double[wavFile.getNumChannels()][frameEntries[idx].audioSamplesInFrame];
419 								frameEntries[idx].audioSamplesInFrame=wavFile.readFrames(sampleBuffer, frameEntries[idx].audioSamplesInFrame);
420 								audioFramesRead+=frameEntries[idx].audioSamplesInFrame;
421 								for (int i=0; i<frameEntries[idx].audioSamplesInFrame; ++i) {
422 									for (int ch=0; ch<wavFile.getNumChannels(); ++ch) {
423 										maxVolume = Math.max(maxVolume, Math.abs(sampleBuffer[ch][i]));
424 										int asidx = i*cellWidth/frameEntries[idx].audioSamplesInFrame;
425 										frameEntries[idx].audioSamples[asidx] = Math.max(Math.abs(sampleBuffer[ch][i]), frameEntries[idx].audioSamples[asidx]);
426 									}
427 								}
428 							}
429 
430 							++idx;
431 							++framePosition;
432 							progressMonitor.setProgress(idx);
433 						}
434 						if (wavFile!=null) {
435 							wavFile.close();
436 						}
437 
438 					}
439 
440 					coeff = Math.pow(10.0, DECIBELS_PER_PIXEL*(MEDIAN-1)/20.0)/maxVolume;
441 
442 					return true;
443 				} finally {
444 					progressMonitor.close();
445 				}
446 			}
447 
448 			protected void done() {
449 				try {
450 					if (get()) {
451 						buildUI();
452 						setLocationRelativeTo(frame);
453 						setVisible(true);
454 					} else {
455 				    	JOptionPane.showMessageDialog(
456 				    			MovieEditorDialog.this,
457 								"Loading operation was cancelled",
458 								"Loading cancelled",
459 								JOptionPane.ERROR_MESSAGE);
460 					}
461 				} catch (Exception e) {
462 					e.printStackTrace();
463 			    	JOptionPane.showMessageDialog(
464 			    			MovieEditorDialog.this,
465 							e.toString(),
466 							"Error loading frames",
467 							JOptionPane.ERROR_MESSAGE);
468 				}
469 
470 			};
471 
472 		};
473 
474 		loader.execute();
475 
476 	}
477 
buildUI()478 	void buildUI() {
479 		BorderLayout thisLayout = new BorderLayout();
480 		getContentPane().setLayout(thisLayout);
481 
482 		contentPanel = new JPanel();
483 		GridBagLayout contentPanelLayout = new GridBagLayout();
484 		getContentPane().add(contentPanel, BorderLayout.CENTER);
485 		contentPanelLayout.rowWeights = new double[] { 0.1, 0.0, 0.0, 0.0, 0.0,	0.0 };
486 		contentPanelLayout.rowHeights = new int[] { movie.getFrameDimension().height, 7, cellHeight+(hasAudio? 23 + AUDIO_CELL_HEIGHT : 22), 7, 7, 7 };
487 		contentPanelLayout.columnWeights = new double[] { 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 };
488 		contentPanelLayout.columnWidths = new int[] { 7, 7, 7, 7, 7, 7, 7 };
489 		contentPanel.setLayout(contentPanelLayout);
490 
491 		saveButton = new JButton("Save");
492 		contentPanel.add(saveButton, new GridBagConstraints(3, 4, 1, 1, 0.0,
493 				0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
494 				new Insets(0, 0, 0, 0), 0, 0));
495 		saveButton.addActionListener(new ActionListener() {
496 			public void actionPerformed(ActionEvent evt) {
497 				SwingWorker<Movie, Long> encoder = new SwingWorker<Movie, Long>() {
498 
499 					@Override
500 					protected Movie doInBackground() throws Exception {
501 						ProgressMonitor progressMonitor = new ProgressMonitor(MovieEditorDialog.this, "Saving movie", "Composing movie", 0, frameEntries.length);
502 
503 						List<Frame> newFrames = new ArrayList<Frame>();
504 
505 						if (splashIndex!=-1) {
506 							newFrames.add(new FrameImpl(
507 									Collections.singletonList((Shape) new ShapeImpl(new Point(0,0), new ImageImpl(new MappedImage(frameEntries[splashIndex].getImage(), imageFormat, null), true))) ,
508 									frameEntries[splashIndex].frame.getMousePointer(),
509 									frameEntries[splashIndex].frame.getSize(),
510 									false));
511 						}
512 
513 						File newAudio = hasAudio ? File.createTempFile("jCaptureAudioSink", ".wav") : null;
514 
515 						long numFrames=0;
516 						for (FrameEntry fe: frameEntries) {
517 							if (!fe.isDeleted) {
518 								numFrames+=fe.audioSamplesInFrame;
519 							}
520 						}
521 						WavFile newWavFile = newAudio==null ? null : WavFile.newWavFile(newAudio, numChannels, numFrames, validBits, sampleRate);
522 
523 						File currentAudio = null;
524 						WavFile currentWav = null;
525 
526 						for (int i=0; i<frameEntries.length; ++i) {
527 							if (frameEntries[i].audioFile!=null) {
528 								if (currentWav!=null) {
529 									currentWav.close();
530 								}
531 								if (currentAudio!=null) {
532 									if (!currentAudio.delete()) {
533 										currentAudio.deleteOnExit();
534 									}
535 								}
536 
537 								currentAudio = frameEntries[i].audioFile;
538 								currentWav = WavFile.openWavFile(currentAudio);
539 							}
540 
541 							if (currentWav!=null) {
542 								if (normalizeVolumeCheckBox.isSelected()) {
543 									double[][] buf = new double[numChannels][frameEntries[i].audioSamplesInFrame];
544 									int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame);
545 									if (read>0 && !frameEntries[i].isDeleted) {
546 										// Normalization
547 										for (double[] ch: buf) {
548 											for (int j=0; j<ch.length; ++j) {
549 												ch[j] = ch[j] * NORMALIZED_LEVEL / maxVolume;
550 											}
551 										}
552 										newWavFile.writeFrames(buf, read);
553 									}
554 								} else {
555 									long[][] buf = new long[numChannels][frameEntries[i].audioSamplesInFrame];
556 									int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame);
557 									if (read>0 && !frameEntries[i].isDeleted) {
558 										newWavFile.writeFrames(buf, read);
559 									}
560 								}
561 							}
562 
563 							if (frameEntries[i].isDeleted) {
564 								if (i<frameEntries.length-1) {
565 									((FrameImpl) frameEntries[i+1].frame).merge(frameEntries[i].frame);
566 								}
567 							} else {
568 								newFrames.add(frameEntries[i].frame);
569 							}
570 
571 							progressMonitor.setProgress(i);
572 							if (progressMonitor.isCanceled()) {
573 								if (currentWav!=null) {
574 									currentWav.close();
575 								}
576 								if (currentAudio!=null) {
577 									if (!currentAudio.delete()) {
578 										currentAudio.deleteOnExit();
579 									}
580 								}
581 								if (newWavFile!=null) {
582 									newWavFile.close();
583 								}
584 								if (newAudio!=null) {
585 									if (!newAudio.delete()) {
586 										newAudio.deleteOnExit();
587 									}
588 								}
589 								return null;
590 							}
591 						}
592 
593 						if (currentWav!=null) {
594 							currentWav.close();
595 						}
596 						if (currentAudio!=null) {
597 							if (!currentAudio.delete()) {
598 								currentAudio.deleteOnExit();
599 							}
600 						}
601 
602 						if (newWavFile!=null) {
603 							newWavFile.close();
604 						}
605 
606 						return new Movie(movie.getFrameDimension(), movie.getFramesPerSecond(), Collections.singletonList((Fragment) new FragmentImpl(newFrames, newAudio)), movie);
607 					}
608 
609 					@Override
610 					protected void done() {
611 						try {
612 							MovieEditorDialog.this.setVisible(false);
613 							((RecordingControlsFrame) getOwner()).uploadMovie(get());
614 							MovieEditorDialog.this.dispose();
615 						} catch (Exception e) {
616 							e.printStackTrace();
617 							JOptionPane.showMessageDialog(
618 									MovieEditorDialog.this, e.toString(),
619 									"Error saving recording",
620 									JOptionPane.ERROR_MESSAGE);
621 							MovieEditorDialog.this.setVisible(false);
622 							MovieEditorDialog.this.getOwner().setVisible(false);
623 						}
624 					}
625 
626 				};
627 
628 				encoder.execute();
629 			}
630 		});
631 
632 		discardButton = new JButton("Discard");
633 		contentPanel.add(discardButton, new GridBagConstraints(5, 4, 1, 1, 0.0,
634 				0.0, GridBagConstraints.CENTER, GridBagConstraints.NONE,
635 				new Insets(0, 0, 0, 0), 0, 0));
636 		discardButton.addActionListener(new ActionListener() {
637 			public void actionPerformed(ActionEvent evt) {
638 				if (JOptionPane.showConfirmDialog(MovieEditorDialog.this, "Are you sure you want to discard the recording?", "Confirm discarding movie", JOptionPane.YES_NO_OPTION)==JOptionPane.YES_OPTION) {
639 					MovieEditorDialog.this.setVisible(false);
640 					MovieEditorDialog.this.dispose();
641 					MovieEditorDialog.this.getOwner().setVisible(false);
642 				}
643 			}
644 		});
645 
646 		timeLineScrollPane = new JScrollPane();
647 		timeLineScrollPane.setPreferredSize(new Dimension(movie.getFrameDimension().width, cellHeight+(hasAudio? 23 + AUDIO_CELL_HEIGHT : 22)));
648 		contentPanel.add(timeLineScrollPane, new GridBagConstraints(0, 2, 7, 1,
649 				0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
650 				new Insets(0, 0, 0, 0), 0, 0));
651 
652 		System.out.println("Loaded "+frameEntries.length+" frames");
653 		TableModel timeLineTableModel = new DefaultTableModel(hasAudio ? 2 : 1, frameEntries.length) {
654 
655 			@Override
656 			public boolean isCellEditable(int row, int column) {
657 				return false;
658 			}
659 		};
660 		timeLineTable = new JTable() {
661 
662 		    @Override
663 			public JToolTip createToolTip() {
664 				Point p = getMousePosition();
665 
666 				// Locate the renderer under the event location
667 				int hitColumnIndex = columnAtPoint(p);
668 				int hitRowIndex = rowAtPoint(p);
669 
670 				if ((hitColumnIndex != -1) && (hitRowIndex != -1)) {
671 					try {
672 						BufferedImage toolTipImage = frameEntries[hitColumnIndex].getToolTipImage();
673 						return new ImageToolTip("Frame "+(hitColumnIndex+1)+", delta "+frameEntries[hitColumnIndex].delta+"%", toolTipImage);
674 					} catch (IOException e) {
675 						e.printStackTrace();
676 					}
677 				}
678 				return super.createToolTip();
679 			}
680 
681 		};
682 
683 		JPopupMenu popup = new JPopupMenu("Context");
684 
685 		popup.addPopupMenuListener(new PopupMenuListener() {
686 
687 			@Override
688 			public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
689 				if (playTimera[0]!=null) {
690 					playTimera[0].stop();
691 				}
692 			}
693 
694 			@Override
695 			public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
696 				// TODO Auto-generated method stub
697 
698 			}
699 
700 			@Override
701 			public void popupMenuCanceled(PopupMenuEvent e) {
702 				// TODO Auto-generated method stub
703 
704 			}
705 		});
706 
707 		addDeleteFramesMenuItem(popup);
708 		addUndeleteFramesMenuItem(popup);
709 		addRemoveInactivityMenuItem(popup);
710 
711 		final JCheckBoxMenuItem splashMenuItem = new JCheckBoxMenuItem("Splash");
712 
713 		splashMenuItem.addActionListener(new ActionListener() {
714 
715 			@Override
716 			public void actionPerformed(ActionEvent e) {
717 				splashIndex = splashMenuItem.isSelected() ? focusColumn : -1;
718 				timeLineTable.repaint();
719 			}
720 		});
721 
722 		popup.add(splashMenuItem);
723 
724 		popup.addPopupMenuListener(new PopupMenuListener() {
725 
726 			@Override
727 			public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
728 				splashMenuItem.setSelected(focusColumn!=-1 && focusColumn==splashIndex);
729 
730 			}
731 
732 			@Override
733 			public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
734 
735 			}
736 
737 			@Override
738 			public void popupMenuCanceled(PopupMenuEvent e) {
739 				// TODO Auto-generated method stub
740 
741 			}
742 		});
743 
744 		JMenuItem playMenuItem = new JMenuItem();
745 		Action playAction = new AbstractAction("Play") {
746 
747 			@Override
748 			public void actionPerformed(ActionEvent e) {
749 				if (playTimera[0]!=null) {
750 					playTimera[0].stop();
751 					playTimera[0] = null;
752 				}
753 
754 				final int range[] = {focusColumn, focusColumn};
755 
756 				for (int i=focusColumn; i<frameEntries.length && timeLineTable.isColumnSelected(i); ++i) {
757 					range[1] = i;
758 				}
759 
760 				for (int i=focusColumn; i>=0 && timeLineTable.isColumnSelected(i); --i) {
761 					range[0] = i;
762 				}
763 
764 				if (range[0]==range[1]) {
765 					range[1]=frameEntries.length-1;
766 				}
767 
768 				playingColumn = range[0];
769 
770 				if (hasAudio) {
771 					try {
772 						backgroundProcessor.execute(new SoundPlayer(range[0], range[1]));
773 					} catch (Exception ex) {
774 						ex.printStackTrace();
775 				    	JOptionPane.showMessageDialog(
776 				    			MovieEditorDialog.this,
777 								ex.toString(),
778 								"Audio problem",
779 								JOptionPane.ERROR_MESSAGE);
780 
781 					}
782 				}
783 
784 				playTimera[0] = new Timer((int) ((double) 1000/movie.getFramesPerSecond()), new ActionListener() {
785 
786 					@Override
787 					public void actionPerformed(ActionEvent e) {
788 						while (frameEntries[playingColumn].isDeleted) {
789 							++playingColumn;
790 							if (playingColumn>range[1]) {
791 								((Timer) e.getSource()).stop();
792 								return;
793 							}
794 						}
795 
796 						Rectangle visibleRect = timeLineTable.getVisibleRect();
797 						Rectangle playingRect = timeLineTable.getCellRect(0, playingColumn, true);
798 						if (!visibleRect.contains(playingRect)) {
799 							Rectangle scrollTo = new Rectangle(playingRect.x, playingRect.width, visibleRect.width-1, visibleRect.height-1);
800 							timeLineTable.scrollRectToVisible(scrollTo);
801 						}
802 
803 						frameCanvas.repaint();
804 						timeLineTable.repaint();
805 
806 						++playingColumn;
807 						if (playingColumn>range[1]) {
808 							((Timer) e.getSource()).stop();
809 							return;
810 						}
811 					}
812 
813 
814 				}) {
815 					@Override
816 					public void stop() {
817 						super.stop();
818 						playingColumn=-1;
819 						timeLineTable.scrollRectToVisible(timeLineTable.getCellRect(0, focusColumn, true));
820 						frameCanvas.repaint();
821 						timeLineTable.repaint();
822 						playTimera[0] = null;
823 					}
824 				};
825 
826 				playTimera[0].start();
827 				synchronized (playTimera) {
828 					playTimera.notifyAll();
829 				}
830 			}
831 		};
832 		playMenuItem.setAction(playAction);
833 
834 		popup.add(playMenuItem);
835 
836 		timeLineTable.setComponentPopupMenu(popup );
837 
838 		timeLineTable.addMouseListener(new MouseAdapter() {
839 
840 			@Override
841 			public void mouseClicked(MouseEvent e) {
842 				if (playTimera[0]!=null) {
843 					playTimera[0].stop();
844 				}
845 
846 				if (e.getClickCount()==2) {
847 					int hitColumnIndex = timeLineTable.columnAtPoint(e.getPoint());
848 					if (hitColumnIndex!=-1) {
849 						frameEntries[hitColumnIndex].isDeleted=!frameEntries[hitColumnIndex].isDeleted;
850 						timeLineTable.repaint();
851 					}
852 				}
853 			}
854 		});
855 
856 		timeLineTable.setToolTipText("Movie timeline");
857 		timeLineScrollPane.setViewportView(timeLineTable);
858 		timeLineTable.setModel(timeLineTableModel);
859 		timeLineTable.setRowHeight(0, cellHeight+timeLineTable.getRowMargin()*2);
860 		timeLineTable.setRowHeight(1, AUDIO_CELL_HEIGHT+timeLineTable.getRowMargin()*2);
861 		for (int i=0; i<frameEntries.length; ++i) {
862 			timeLineTable.getColumnModel().getColumn(i).setPreferredWidth(cellWidth);
863 			timeLineTable.setValueAt(frameEntries[i], 0, i);
864 			if (hasAudio) {
865 				timeLineTable.setValueAt(frameEntries[i], 1, i);
866 			}
867 		}
868 		timeLineTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
869 		timeLineTable.setTableHeader(null);
870 		timeLineTable.getSelectionModel().setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
871 		timeLineTable.setColumnSelectionAllowed(true);
872 		timeLineTable.setRowSelectionAllowed(false);
873 
874 		TableCellRenderer renderer = new TableCellRenderer() {
875 
876 			@Override
877 			public Component getTableCellRendererComponent(JTable table, Object value, final boolean isSelected, final boolean hasFocus, int row, int column) {
878 
879 				if (hasFocus && column!=focusColumn) {
880 					focusColumn = column;
881 					frameCanvas.repaint();
882 				}
883 
884 				return frameEntries[column].getCellRendererComponent(row, isSelected, hasFocus);
885 			}
886 		};
887 		timeLineTable.setDefaultRenderer(Object.class, renderer);
888 
889 		frameCanvas = new JPanel() {
890 
891 			@Override
892 			public void paintComponent(Graphics g) {
893 				super.paintComponent(g);
894 				Rectangle bounds = getBounds();
895 
896 				try {
897 					Image image = frameEntries[playingColumn==-1 ? focusColumn : playingColumn].getImage();
898 					double xScale = (double) bounds.width/(double) image.getWidth(null);
899 					double yScale = (double) bounds.height/(double) image.getHeight(null);
900 					double scale = Math.min(xScale, yScale);
901 					int scaledWidth = (int) (image.getWidth(null)*scale);
902 					int scaledHeight = (int) (image.getHeight(null)*scale);
903 					if (g instanceof Graphics2D) {
904 				    	((Graphics2D) g).setComposite(AlphaComposite.Src);
905 				    	((Graphics2D) g).setRenderingHint(RenderingHints.KEY_INTERPOLATION,RenderingHints.VALUE_INTERPOLATION_BILINEAR);
906 				    	((Graphics2D) g).setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
907 				    	((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
908 					}
909 					g.drawImage(image, (bounds.width-scaledWidth)/2, (bounds.height-scaledHeight)/2, scaledWidth, scaledHeight, null);
910 				} catch (Exception e) {
911 					e.printStackTrace();
912 					g.clearRect(0, 0, bounds.width, bounds.height);
913 					g.drawString(e.toString(), 10, 20);
914 				}
915 			}
916 
917 		};
918 
919 		frameCanvas.addMouseListener(new MouseAdapter() {
920 
921 			@Override
922 			public void mouseClicked(MouseEvent e) {
923 				if (playTimera[0]!=null) {
924 					playTimera[0].stop();
925 				}
926 			}
927 		});
928 
929 		frameCanvas.setPreferredSize(movie.getFrameDimension());
930 		contentPanel.add(frameCanvas, new GridBagConstraints(0, 0, 7, 1, 0.0,
931 				0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH,
932 				new Insets(1, 1, 1, 1), 0, 0));
933 
934 		normalizeVolumeCheckBox = new JCheckBox();
935 		contentPanel.add(normalizeVolumeCheckBox, new GridBagConstraints(1, 4,
936 				1, 1, 0.0, 0.0, GridBagConstraints.CENTER,
937 				GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
938 		normalizeVolumeCheckBox.setText("Normalize volume (+"+Math.round(Math.log10(NORMALIZED_LEVEL/maxVolume)*20)+" dB)");
939 
940 		timeLineTable.changeSelection(0, 0, false, false);
941 		frameCanvas.repaint();
942 
943 		pack();
944 
945 	}
946 
addUndeleteFramesMenuItem(JPopupMenu popup)947 	void addUndeleteFramesMenuItem(JPopupMenu popup) {
948 		JMenuItem unDeleteFrameMenuItem = new JMenuItem();
949 		Action unDeleteFrameAction = new AbstractAction("Undelete frame(s)") {
950 
951 			@Override
952 			public void actionPerformed(ActionEvent e) {
953 				for (int idx: timeLineTable.getSelectedColumns()) {
954 					frameEntries[idx].isDeleted = false;
955 				}
956 				timeLineTable.repaint();
957 			}
958 		};
959 		unDeleteFrameMenuItem.setAction(unDeleteFrameAction);
960 
961 		popup.add(unDeleteFrameMenuItem);
962 	}
963 
addDeleteFramesMenuItem(JPopupMenu popup)964 	void addDeleteFramesMenuItem(JPopupMenu popup) {
965 		JMenuItem deleteFrameMenuItem = new JMenuItem();
966 		Action deleteFrameAction = new AbstractAction("Delete frame(s)") {
967 
968 			@Override
969 			public void actionPerformed(ActionEvent e) {
970 				for (int idx: timeLineTable.getSelectedColumns()) {
971 					frameEntries[idx].isDeleted = true;
972 				}
973 				timeLineTable.repaint();
974 			}
975 		};
976 
977 		deleteFrameMenuItem.setAction(deleteFrameAction);
978 		popup.add(deleteFrameMenuItem);
979 	}
980 
addRemoveInactivityMenuItem(JPopupMenu popup)981 	void addRemoveInactivityMenuItem(JPopupMenu popup) {
982 		JMenuItem removeInactivityMenuItem = new JMenuItem();
983 		Action deleteFrameAction = new AbstractAction("Remove inactivity") {
984 
985 			@Override
986 			public void actionPerformed(ActionEvent e) {
987 				String msg = "Inactivity interval";
988 				while (true) {
989 					String newVal = JOptionPane.showInputDialog(msg, String.valueOf(inactivityInterval));
990 					if (newVal==null) {
991 						return;
992 					}
993 
994 					try {
995 						inactivityInterval = Double.parseDouble(newVal);
996 						if (inactivityInterval > 0) {
997 							break;
998 						}
999 					} catch (NumberFormatException nfe) {
1000 						// NOP - loop
1001 					}
1002 					msg = "Invalid double value for inactivity interval: "+newVal+", enter valid value";
1003 				}
1004 				int inactivityInFrames = (int) (inactivityInterval*movie.getFramesPerSecond());
1005 				int lastActivity = -inactivityInFrames-1;
1006 				for (int idx: timeLineTable.getSelectedColumns()) {
1007 					if (!frameEntries[idx].isDeleted && frameEntries[idx].frame.isActive()) {
1008 						lastActivity = idx;
1009 					} else if (idx - lastActivity > inactivityInFrames && !frameEntries[idx].frame.isActive()) {
1010 						frameEntries[idx].isDeleted = true;
1011 					}
1012 				}
1013 				timeLineTable.repaint();
1014 			}
1015 		};
1016 
1017 		removeInactivityMenuItem.setAction(deleteFrameAction);
1018 		popup.add(removeInactivityMenuItem);
1019 	}
1020 
1021 	private class SoundPlayer implements Runnable {
1022 
1023 	    private final int BUFFER_SIZE;
1024 	    private AudioInputStream audioStream;
1025 	    private SourceDataLine sourceLine;
1026 		private File audioFile;
1027 
SoundPlayer(int start, int end)1028 	    public SoundPlayer(int start, int end) throws Exception {
1029 
1030 			audioFile = hasAudio ? File.createTempFile("jCaptureRangeAudio", ".wav") : null;
1031 
1032 			BUFFER_SIZE = (int) ((double) numChannels*sampleRate*validBits/(movie.getFramesPerSecond()*8)); // 1 frame buffer.
1033 
1034 			long numFrames=0;
1035 			for (int i = start; i<=end; ++i) {
1036 				if (!frameEntries[i].isDeleted) {
1037 					numFrames+=frameEntries[i].audioSamplesInFrame;
1038 				}
1039 			}
1040 			WavFile newWavFile = audioFile==null ? null : WavFile.newWavFile(audioFile, numChannels, numFrames, validBits, sampleRate);
1041 
1042 			File currentAudio = null;
1043 			WavFile currentWav = null;
1044 
1045 			for (int i=0; i<=end; ++i) {
1046 				if (frameEntries[i].audioFile!=null) {
1047 					if (currentWav!=null) {
1048 						currentWav.close();
1049 					}
1050 
1051 					currentAudio = frameEntries[i].audioFile;
1052 					currentWav = WavFile.openWavFile(currentAudio);
1053 				}
1054 
1055 				if (currentWav!=null) {
1056 					if (normalizeVolumeCheckBox!=null && normalizeVolumeCheckBox.isSelected()) {
1057 						double[][] buf = new double[numChannels][frameEntries[i].audioSamplesInFrame];
1058 						int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame);
1059 						if (read>0 && i>=start && !frameEntries[i].isDeleted) {
1060 							// Normalization
1061 							for (double[] ch: buf) {
1062 								for (int j=0; j<ch.length; ++j) {
1063 									ch[j] = ch[j] * NORMALIZED_LEVEL / maxVolume;
1064 								}
1065 							}
1066 							newWavFile.writeFrames(buf, read);
1067 						}
1068 					} else {
1069 						long[][] buf = new long[numChannels][frameEntries[i].audioSamplesInFrame];
1070 						int read = currentWav.readFrames(buf, frameEntries[i].audioSamplesInFrame);
1071 						if (read>0 && i>=start && !frameEntries[i].isDeleted) {
1072 							newWavFile.writeFrames(buf, read);
1073 						}
1074 					}
1075 				}
1076 			}
1077 
1078 			if (currentWav!=null) {
1079 				currentWav.close();
1080 			}
1081 			if (newWavFile!=null) {
1082 				newWavFile.close();
1083 			}
1084 
1085 			if (audioFile!=null) {
1086 	            audioStream = AudioSystem.getAudioInputStream(audioFile);
1087 		        AudioFormat audioFormat = audioStream.getFormat();
1088 		        DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
1089 	            sourceLine = (SourceDataLine) AudioSystem.getLine(info);
1090 	            sourceLine.open(audioFormat);
1091 			}
1092 		}
1093 
1094 	    @Override
run()1095 	    public void run() {
1096 	    	try {
1097 		        sourceLine.start();
1098 		        synchronized (playTimera) {
1099 			        if (playTimera[0] == null) {
1100 			        	playTimera.wait(100);
1101 			        }
1102 		        }
1103 		        try {
1104 			        byte[] buf = new byte[BUFFER_SIZE];
1105 			        int l;
1106 			        while (playTimera[0]!=null && (l=audioStream.read(buf))!=-1) {
1107 			        	sourceLine.write(buf, 0, l);
1108 			        }
1109 		        } finally {
1110 		        	audioStream.close();
1111 			        sourceLine.drain();
1112 			        sourceLine.close();
1113 			        if (!audioFile.delete()) {
1114 			        	audioFile.deleteOnExit();
1115 			        }
1116 		        }
1117 	    	} catch (Exception e) {
1118 	    		e.printStackTrace();
1119 	    	}
1120 	    }
1121 
1122 	}
1123 
1124 }
1125