source: trunk/src/org/apollo/widgets/SampledTrack.java@ 1007

Last change on this file since 1007 was 1007, checked in by davidb, 8 years ago

Generalization of audio support to allow playback/mixer to be stereo, plus some edits to comments

File size: 60.6 KB
Line 
1package org.apollo.widgets;
2
3import java.awt.Color;
4import java.awt.FontMetrics;
5import java.awt.Graphics;
6import java.awt.Graphics2D;
7import java.awt.Point;
8import java.awt.Rectangle;
9import java.awt.Shape;
10import java.awt.Stroke;
11import java.awt.event.ActionEvent;
12import java.awt.event.KeyEvent;
13import java.awt.event.KeyListener;
14import java.awt.event.MouseEvent;
15import java.awt.event.MouseListener;
16import java.awt.event.MouseMotionListener;
17import java.awt.geom.Rectangle2D;
18import java.io.File;
19import java.io.IOException;
20import java.lang.reflect.InvocationTargetException;
21import java.util.LinkedList;
22import java.util.List;
23
24import javax.sound.sampled.AudioFormat;
25import javax.sound.sampled.LineUnavailableException;
26import javax.sound.sampled.UnsupportedAudioFileException;
27import javax.swing.SwingUtilities;
28
29import org.apollo.AudioFrameKeyboardActions;
30import org.apollo.AudioFrameMouseActions;
31import org.apollo.audio.ApolloPlaybackMixer;
32import org.apollo.audio.ApolloSubjectChangedEvent;
33import org.apollo.audio.SampledTrackModel;
34import org.apollo.audio.TrackSequence;
35import org.apollo.audio.structure.AudioStructureModel;
36import org.apollo.audio.structure.TrackGraphNode;
37import org.apollo.audio.util.SoundDesk;
38import org.apollo.audio.util.Timeline;
39import org.apollo.audio.util.TrackMixSubject;
40import org.apollo.gui.EditableSampledTrackGraphView;
41import org.apollo.gui.ExpandedTrackManager;
42import org.apollo.gui.FrameLayoutDaemon;
43import org.apollo.gui.PlaybackControlPopup;
44import org.apollo.gui.SampledTrackGraphView;
45import org.apollo.gui.Strokes;
46import org.apollo.gui.SampledTrackGraphView.EffecientInvalidator;
47import org.apollo.io.AudioIO;
48import org.apollo.io.AudioPathManager;
49import org.apollo.io.IconRepository;
50import org.apollo.io.AudioIO.AudioFileLoader;
51import org.apollo.items.EmulatedTextItem;
52import org.apollo.items.EmulatedTextItem.TextChangeListener;
53import org.apollo.mvc.Observer;
54import org.apollo.mvc.Subject;
55import org.apollo.mvc.SubjectChangedEvent;
56import org.apollo.util.AudioMath;
57import org.apollo.util.Mutable;
58import org.apollo.util.PopupReaper;
59import org.apollo.util.TrackModelHandler;
60import org.apollo.util.TrackModelLoadManager;
61import org.apollo.util.TrackNameCreator;
62import org.expeditee.gui.Browser;
63import org.expeditee.gui.DisplayIO;
64import org.expeditee.gui.Frame;
65import org.expeditee.gui.FrameGraphics;
66import org.expeditee.gui.FrameIO;
67import org.expeditee.gui.FrameMouseActions;
68import org.expeditee.gui.MouseEventRouter;
69import org.expeditee.gui.PopupManager;
70import org.expeditee.items.ItemParentStateChangedEvent;
71import org.expeditee.items.ItemUtils;
72import org.expeditee.items.Text;
73import org.expeditee.items.widgets.HeavyDutyInteractiveWidget;
74import org.expeditee.items.widgets.InteractiveWidget;
75import org.expeditee.items.widgets.InteractiveWidgetInitialisationFailedException;
76import org.expeditee.items.widgets.InteractiveWidgetNotAvailableException;
77
78/**
79 * The sampled track widgets in apollo.
80 *
81 * @author Brook Novak
82 *
83 */
84public class SampledTrack extends HeavyDutyInteractiveWidget
85 implements TrackModelHandler, EffecientInvalidator, Observer {
86
87 /** The observered subject. Can be null */
88 private SampledTrackModel trackModel = null;
89
90 /** Used for the loading phase. Can change several times of a lifetime. for example after import and save it becomes local */
91 private String loadFilenameArgument = null; // preproceeds with ARG_IMPORT_TAG if importing.
92
93 private String localFileName; // Immutable - assigned on construction
94
95 /** Used for dumping audio to a temp file when deleted - to free memory.
96 * Protocol: Set when the widget is deleted. Unset when loaded. */
97 private File recoveryFile = null;
98
99 private boolean shouldOmitAudioIndexing = false;
100
101 private EditableSampledTrackGraphView fulltrackView;
102 private EmulatedTextItem nameLabel;
103 private PlaybackPopup playbackControlPopup = null;
104 private TrackMixSubject trackMix; // immutable - like local filename
105
106 /** The amount of load bar that is allocated to file loading */
107 private static final float FILE_LOADING_PERCENT_RANGE = 0.95f;
108
109 /** For parsing arguments */
110 public static final String ARG_IMPORT_TAG = "if=";
111
112 /** For parsing metadata */
113 public static final String META_LOCALNAME_TAG = "ln=";
114
115 private static final Stroke FREESPACE_OUTLINING = Strokes.SOLID_2;
116
117 private static final Color SEMI_TRANSPARENT_FREESPACE_BACKCOLOR = new Color(
118 FREESPACE_BACKCOLOR.getRed(), FREESPACE_BACKCOLOR.getGreen(),
119 FREESPACE_BACKCOLOR.getBlue(), 128);
120
121
122 /** If a track widget has a data string thaty contains META_SHOULD_INDEX_AUDIO_TAG, then
123 * the track should not be indexed for searching the audio.... default isdoes not exist
124 */
125 public static final String META_DONT_INDEX_AUDIO_TAG = "dontindexaudio";
126
127 /**
128 * Constructor used for loading directly from given bytes.
129 *
130 * @param source
131 *
132 * @param audioBytes
133 *
134 * @param format
135 *
136 * @param mixTemplate
137 * Can be null
138 *
139 */
140 private SampledTrack(Text source, byte[] audioBytes, AudioFormat format, TrackMixSubject mixTemplate) {
141
142 super(source, new EditableSampledTrackGraphView(),
143 100, -1,
144 FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT,
145 TrackWidgetCommons.CACHE_DEPTH,
146 true);
147
148 // Must set upon construction - always
149 localFileName = AudioPathManager.generateLocateFileName("wav");
150 updateData(META_LOCALNAME_TAG, META_LOCALNAME_TAG + localFileName);
151
152 trackModel = new SampledTrackModel(audioBytes, format, localFileName);
153
154 // Ensure that the model is marked as modified so that it will save
155 trackModel.setAudioModifiedFlag(true);
156
157 trackModel.setName(getStrippedDataString(TrackWidgetCommons.META_NAME_TAG));
158
159 // Create the immutable mix subject
160 if (mixTemplate == null)
161 trackMix = SoundDesk.getInstance().getOrCreateMix(SoundDesk.createPureLocalChannelID(this));
162
163 else trackMix = SoundDesk.getInstance().createMix(
164 SoundDesk.createPureLocalChannelID(this),
165 mixTemplate.getVolume(),
166 mixTemplate.isMuted());
167
168 // Keep meta as constant as possible for best results
169 updateData(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, TrackWidgetCommons.META_RUNNINGMSTIME_TAG + getRunningMSTimeFromRawAudio());
170
171 createGUI();
172
173 initObservers();
174
175 }
176
177 /**
178 * Constructor called by Expeditee. Eventually loads audio from file.
179 *
180 * @param source
181 *
182 * @param args
183 * Can have a ARG_IMPORT_TAG argument...
184 */
185 public SampledTrack(Text source, String[] args) {
186 super(source, new EditableSampledTrackGraphView(),
187 100, -1,
188 FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT,
189 TrackWidgetCommons.CACHE_DEPTH);
190
191 // Read the metadata
192 localFileName = getStrippedDataString(META_LOCALNAME_TAG);
193
194 // Ensure the local filename is assigned - even if file does not exist...
195 // Also it could be importing a file...
196 if (localFileName == null) {
197 localFileName = AudioPathManager.generateLocateFileName("wav");
198 updateData(META_LOCALNAME_TAG, META_LOCALNAME_TAG + localFileName);
199 }
200
201 trackMix = SoundDesk.getInstance().getOrCreateMix(SoundDesk.createPureLocalChannelID(this));
202
203 loadFilenameArgument = localFileName;
204 if (args != null) { // parse args
205 for (String arg : args) {
206 if (arg != null && arg.length() > ARG_IMPORT_TAG.length()) {
207 loadFilenameArgument = arg;
208 }
209 }
210 }
211
212 createGUI();
213
214 }
215
216 private void initObservers() {
217
218 // Listen for model events
219 trackModel.addObserver(this);
220 trackModel.addObserver(fulltrackView);
221 fulltrackView.setMix(trackMix); // use the same mix/channel for playback
222 ExpandedTrackManager.getInstance().addObserver(this);
223
224 // Show graph as being selected if expanded / pending to expand.
225 if (ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel)) {
226 fulltrackView.setBackColor(new Color(100, 100, 100), new Color(120, 120, 120));
227 } else {
228 fulltrackView.setBackColor(SampledTrackGraphView.DEFAULT_BACKGROUND_COLOR, SampledTrackGraphView.DEFAULT_BACKGROUND_HIGHTLIGHTS_COLOR);
229 }
230
231 }
232
233 private void createGUI() {
234
235 // Set widget as a fixed size widget- using width from last recorded width in the meta data.
236 int width = getStrippedDataInt(TrackWidgetCommons.META_LAST_WIDTH_TAG, -1);
237 if (width >= 0 && width < FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH) {
238 width = FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH;
239 }
240
241 if (width < 0) {
242 setSize(-1, -1,
243 FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT,
244 FrameLayoutDaemon.TRACK_WIDGET_DEFAULT_WIDTH, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
245 } else {
246 setSize(width, width,
247 FrameLayoutDaemon.TRACK_WIDGET_HEIGHT, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT,
248 width, FrameLayoutDaemon.TRACK_WIDGET_HEIGHT);
249 }
250
251 shouldOmitAudioIndexing = containsDataTrimmedIgnoreCase(META_DONT_INDEX_AUDIO_TAG);
252
253 playbackControlPopup = new PlaybackPopup();
254
255 fulltrackView = (EditableSampledTrackGraphView)_swingComponent;
256 fulltrackView.setAlwaysFullView(true);
257
258 /*fulltrackView.setInvalidator(new EffecientInvalidator() {
259 public void onGraphDirty(SampledTrackGraphView graph, Rectangle dirty) {
260 dirty.translate(getX(), getY() - 1);
261 FrameGraphics.invalidateArea(dirty);
262 FrameGraphics.refresh(true);
263 }
264 });*/
265
266 // Auto-show playback popup.
267 // Block messages if the track is expanded
268 fulltrackView.addMouseMotionListener(new MouseMotionListener() {
269
270 public void mouseDragged(MouseEvent e) {
271 if (nameLabel != null) {
272 if (nameLabel.onMouseDragged(e)) {
273 e.consume();
274 }
275 }
276 }
277
278 public void mouseMoved(MouseEvent e) {
279
280 if (nameLabel != null) {
281 nameLabel.onMouseMoved(e, fulltrackView);
282 }
283
284 if (playbackControlPopup == null) {
285 playbackControlPopup = new PlaybackPopup();
286 }
287
288 // Only show popup iff there is nothing expanded / expanding.
289 if (!PopupManager.getInstance().isShowing(playbackControlPopup) &&
290 !ExpandedTrackManager.getInstance().doesExpandedTrackExist()) {
291
292 // Get rid of all popups
293 PopupManager.getInstance().hideAutohidePopups();
294
295 Rectangle animationSource = _swingComponent.getBounds();
296
297 // Determine where popup should show
298 int x = SampledTrack.this.getX();
299 int y = SampledTrack.this.getY() - playbackControlPopup.getHeight() - 2; // by default show above
300
301 // I get sick.dizzy from the popup expanding from the whole thing...
302 animationSource.height = 1;
303 animationSource.width = Math.min(animationSource.width, playbackControlPopup.getWidth());
304
305 if (y < 0) {
306 y = SampledTrack.this.getY() + SampledTrack.this.getHeight() + 2;
307 animationSource.y = y - 2;
308 }
309
310 // Animate the popup
311 PopupManager.getInstance().showPopup(
312 playbackControlPopup,
313 new Point(x, y),
314 fulltrackView,
315 PopupManager.getInstance().new ExpandShrinkAnimator(
316 animationSource,
317 Color.LIGHT_GRAY));
318
319 PopupReaper.getInstance().initPopupLifetime(
320 playbackControlPopup,
321 PopupManager.getInstance().new ExpandShrinkAnimator(
322 animationSource,
323 Color.LIGHT_GRAY),
324 TrackWidgetCommons.POPUP_LIFETIME);
325
326 } else {
327 PopupReaper.getInstance().revivePopup(playbackControlPopup, TrackWidgetCommons.POPUP_LIFETIME);
328 }
329
330 }
331
332 });
333
334 // Expand on double click
335 // Block messages if the track is expanded
336 fulltrackView.addMouseListener(new MouseListener() {
337
338 public void mouseClicked(MouseEvent e) {
339 if (trackModel == null) return;
340
341 if (nameLabel != null) {
342 if (nameLabel.onMouseClicked(e)) {
343 e.consume();
344 return;
345 }
346 }
347
348 if (e.getClickCount() >= 2) {
349
350 expand(e.isControlDown());
351
352 }
353
354 }
355
356 public void mouseEntered(MouseEvent e) {
357 }
358
359 public void mouseExited(MouseEvent e) {
360 }
361
362 public void mousePressed(MouseEvent e) {
363 if (nameLabel != null) {
364 if (nameLabel.onMousePressed(e)) {
365 e.consume();
366 return;
367 }
368 }
369
370 // Consume events if track is selected for expansion
371 if (ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel) &&
372 e.getButton() != MouseEvent.BUTTON1) { // but allow selection only
373 e.consume();
374 }
375 }
376
377 public void mouseReleased(MouseEvent e) {
378 if (nameLabel != null) {
379 if (nameLabel.onMouseReleased(e)) {
380 e.consume();
381 }
382 }
383
384 if (!e.isConsumed() && e.getButton() == MouseEvent.BUTTON2) {
385 if (split(true))
386 e.consume();
387 }
388
389 // Consume events if track is selected for expansion
390 if (ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel)) {
391 e.consume();
392 }
393
394 }
395
396 });
397
398 fulltrackView.addKeyListener(new KeyListener() {
399
400 public void keyPressed(KeyEvent e) {
401 if (!e.isControlDown() && nameLabel != null) {
402 if (nameLabel.onKeyPressed(e, fulltrackView)) {
403 e.consume();
404 }
405 }
406 }
407
408 public void keyReleased(KeyEvent e) {
409 if (!e.isControlDown() && nameLabel != null) {
410 if (nameLabel.onKeyReleased(e, fulltrackView)) {
411 e.consume();
412 }
413 }
414
415 // Toggle pitch-track indexing
416 if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_I) {
417 setShouldOmitIndexAudio(!shouldOmitIndexAudio());
418 FrameGraphics.refresh(true);
419 }
420
421 // Delete-and-Split audio command
422 else if (!e.isControlDown() && e.getKeyCode() == KeyEvent.VK_DELETE) {
423
424 if (split(false)) // try Perform delete-split
425 e.consume();
426
427 }
428 // Convert to linked track
429 else if (e.isControlDown() && e.getKeyCode() == KeyEvent.VK_L) {
430
431 Frame current = DisplayIO.getCurrentFrame();
432
433 if (current != null) {
434
435 String name = getName();
436 if (name == null) name = "Unamed";
437
438 Mutable.Long initTime = getInitiationTimeFromMeta();
439 if (initTime == null) initTime = Mutable.createMutableLong(0);
440
441 Text linkSource = new Text(current.getNextItemID());
442 linkSource.addToData(TrackWidgetCommons.META_NAME_TAG + name);
443 linkSource.setPosition(getPosition());
444 linkSource.setParent(current);
445 LinkedTrack linkedVersion = new LinkedTrack(linkSource, null);
446
447 // Save any changes in the audio - or save for the first time
448 if (trackModel.isAudioModified())
449 saveWidgetData(); // a little lag is OK .. could make smarter if really need to
450
451 // Create a new frame to hold the track
452 Frame newFrame = FrameIO.CreateNewFrame(SampledTrack.this.getFirstCorner());
453
454 // Remove track from current frame
455 removeSelf();
456
457 // Add to new frame
458 newFrame.addAllItems(getItems());
459
460 // Save changes
461 FrameIO.SaveFrame(newFrame);
462
463 // Link it
464 linkedVersion.setLink(newFrame.getName(), null);
465
466 // Add the new link
467 current.addAllItems(linkedVersion.getItems());
468
469 // Ensure initiation times are retained to the exact frame... avoiding loss due to resolution
470 linkedVersion.setInitiationTime(initTime.value);
471
472
473 }
474
475 } else if (e.isControlDown() &&
476 (e.getKeyCode() == KeyEvent.VK_LEFT || e.getKeyCode() == KeyEvent.VK_RIGHT)) {
477 e.consume();
478 AudioFrameKeyboardActions.adjustInitationTime(SampledTrack.this, e.getKeyCode() == KeyEvent.VK_LEFT);
479 } else if (e.isControlDown() &&
480 (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN)) {
481 e.consume();
482 AudioFrameKeyboardActions.adjustVerticlePosition(SampledTrack.this, e.getKeyCode() == KeyEvent.VK_UP);
483 }
484
485 }
486
487 public void keyTyped(KeyEvent e) {
488 }
489
490 });
491
492
493
494 nameLabel = new EmulatedTextItem(_swingComponent, new Point(10, 20));
495 nameLabel.setBackgroundColor(Color.WHITE);
496
497 String metaName = getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
498 if (metaName == null) metaName = "Untitled";
499 nameLabel.setText(metaName);
500
501 nameLabel.addTextChangeListener(new TextChangeListener() { // a little bit loopy!
502
503 public void onTextChanged(Object source, String newLabel) {
504 if (trackModel != null && !nameLabel.getText().equals(trackModel.getName())) {
505 trackModel.setName(nameLabel.getText());
506 SampledTrack.this.updateData(TrackWidgetCommons.META_NAME_TAG, trackModel.getName());
507
508 }
509 }
510
511 });
512
513 // Make sure the above mouse listeners get first serve
514 fulltrackView.reAddListeners();
515
516 // Make sure border color is correct
517 updateBorderColor();
518 setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
519 }
520
521 /**
522 * Creates a {@link SampledTrack} instantly from audio bytes in memory and adds it to a given frame.
523 *
524 * @param audioBytes
525 * The audio samples.
526 *
527 * @param format
528 * The format of the audio samples.
529 *
530 * @param targetFrame
531 * The frame that the widget will reside.
532 *
533 * @param x
534 *
535 * @param yinitTime
536 *
537 * @param name
538 * The name of the new track. If null then a default name will be used.
539 *
540 * @param mixTemplate
541 * The mix data to clone for the new sampled tracks mix.
542 * If null then default mix settings will be used.
543 *
544 * @return The {@link SampledTrack} instance added to the given frame.
545 *
546 */
547 public static SampledTrack createFromMemory(
548 byte[] audioBytes,
549 AudioFormat format,
550 Frame targetFrame,
551 int x, int y,
552 String name,
553 TrackMixSubject mixTemplate) {
554
555 assert (targetFrame != null);
556 assert (audioBytes != null);
557 assert (format != null);
558
559 Text source = new Text(targetFrame.getNextItemID());
560 source.setParent(targetFrame);
561
562
563 long runningtime = AudioMath.framesToMilliseconds(
564 audioBytes.length / format.getFrameSize(), format);
565
566 Timeline tl = FrameLayoutDaemon.getInstance().getTimeline(targetFrame);
567
568 int width = -1;
569 if (tl != null) {
570 width = (int)((double)runningtime / tl.getTimePerPixel());
571 }
572
573 // Clamp width to something reasonable.
574 if (width > 0 && width < FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH) width = FrameLayoutDaemon.MIN_TRACK_WIDGET_WIDTH;
575
576 long initTime = (tl == null) ? 0 : tl.getMSTimeAtX(x);
577
578 source.setPosition(x, y);
579
580 // Setup metadata
581 LinkedList<String> data = new LinkedList<String>();
582
583 data.add(TrackWidgetCommons.META_INITIATIONTIME_TAG + initTime);
584 data.add(TrackWidgetCommons.META_LAST_WIDTH_TAG + width); // although layout manager will handle, just to quick set
585 if (name != null) data.add(TrackWidgetCommons.META_NAME_TAG + name);
586
587 source.setData(data);
588
589 SampledTrack strack = new SampledTrack(source, audioBytes, format, mixTemplate);
590
591 return strack;
592 }
593
594
595 /**
596 * Creates a {@link SampledTrack} from file - which is tyo be imported into apollos
597 *
598 * @param targetFrame
599 * The frame that the widget will reside.
600 *
601 * @param file
602 * The audio file to import
603 *
604 * @return The {@link SampledTrack} instance added to the given frame.
605 *
606 */
607 public static SampledTrack createFromFile(File file, Frame targetFrame, int x, int y) {
608 assert (targetFrame != null);
609 assert (file != null);
610
611
612 String[] args = new String[] {
613 ARG_IMPORT_TAG + file.getAbsolutePath()
614 };
615
616 Text source = new Text(
617 targetFrame.getNextItemID(),
618 ItemUtils.GetTag(ItemUtils.TAG_IWIDGET) + ":" + formatArgs(args));
619
620 source.setParent(targetFrame);
621
622 source.setPosition(x, y);
623
624 long initTime = FrameLayoutDaemon.getInstance().getMSAtX(x, targetFrame);
625
626 // Setup metadata
627 LinkedList<String> data = new LinkedList<String>();
628
629 data.add(TrackWidgetCommons.META_INITIATIONTIME_TAG + initTime);
630 data.add(TrackWidgetCommons.META_LAST_WIDTH_TAG + FrameLayoutDaemon.TRACK_WIDGET_DEFAULT_WIDTH); // once loaded then the layout will handle proper size
631
632 int extIndex = file.getName().lastIndexOf('.');
633 String name = (extIndex > 0) ? file.getName().substring(0, extIndex) : file.getName();
634 data.add(TrackWidgetCommons.META_NAME_TAG + name);
635
636 source.setData(data);
637
638 SampledTrack strack = new SampledTrack(source, args);
639
640 return strack;
641 }
642
643
644 @Override
645 protected String[] getArgs() {
646 return null;
647 }
648
649 @Override
650 protected List<String> getData() {
651
652 List<String> data = new LinkedList<String>();
653
654 data.add(META_LOCALNAME_TAG + localFileName);
655
656 if (shouldOmitAudioIndexing) data.add(META_DONT_INDEX_AUDIO_TAG);
657
658 String lastName = getName();
659
660 if (lastName != null)
661 data.add(TrackWidgetCommons.META_NAME_TAG + lastName);
662
663 data.add(TrackWidgetCommons.META_LAST_WIDTH_TAG + getWidth());
664
665 Mutable.Long initTime = null;
666
667 Frame f = getParentFrame();
668 if (f != null) {
669 TrackGraphNode tinf = AudioStructureModel.getInstance().getTrackGraphInfo(localFileName, f.getName());
670 if (tinf != null) {
671 initTime = Mutable.createMutableLong(tinf.getInitiationTime());
672 }
673 }
674
675 if (initTime == null) initTime = getInitiationTimeFromMeta(); // old meta
676 if (initTime == null) initTime = Mutable.createMutableLong(0L);
677
678 data.add(TrackWidgetCommons.META_INITIATIONTIME_TAG + initTime);
679
680 data.add(TrackWidgetCommons.META_RUNNINGMSTIME_TAG + getRunningMSTimeFromRawAudio());
681
682 return data;
683 }
684
685
686 @Override
687 public InteractiveWidget copy()
688 throws InteractiveWidgetNotAvailableException, InteractiveWidgetInitialisationFailedException {
689
690 if (trackModel == null) {
691 return super.copy();
692
693 } else {
694
695 return SampledTrack.createFromMemory(
696 trackModel.getAllAudioBytesCopy(),
697 trackModel.getFormat(),
698 getSource().getParentOrCurrentFrame(),
699 getX(),
700 getY(),
701 TrackNameCreator.getNameCopy(trackModel.getName()),
702 trackMix);
703 }
704
705 }
706
707 @Override
708 public int getLoadDelayTime() {
709 return 0;
710 }
711
712 @Override
713 protected float loadWidgetData() {
714
715 // Load audio from file
716 File f = null;
717 boolean isImporting = false;
718
719 if (recoveryFile != null) { // are we recovering?
720
721 setLoadScreenMessage("Recovering audio file...");
722
723 if (recoveryFile.exists())
724 f = recoveryFile;
725
726 }
727
728 if (f == null) { // must be importing or loading from local repository
729
730 if (loadFilenameArgument != null && loadFilenameArgument.startsWith(ARG_IMPORT_TAG)) { // importing a file?
731
732 setLoadScreenMessage("Importing audio file...");
733
734 isImporting = true;
735 f = new File(loadFilenameArgument.substring(ARG_IMPORT_TAG.length()));
736
737 } else { // local
738 assert(loadFilenameArgument == null || loadFilenameArgument == localFileName);
739
740 setLoadScreenMessage("Loading audio file...");
741
742 f = new File(
743 AudioPathManager.AUDIO_HOME_DIRECTORY + localFileName);
744 }
745
746 // Nullify
747 if (!f.exists() || !f.isFile()) f = null;
748
749 }
750
751 if (f == null) {
752 if (recoveryFile != null) {
753 setLoadScreenMessage("Recovery file missing");
754 } else {
755 setLoadScreenMessage("File missing");
756 }
757
758 return LOAD_STATE_FAILED;
759 }
760
761 if (trackModel != null && trackModel.getFilepath() != null &&
762 trackModel.equals(f.getPath())) {
763
764 // already have loaded
765 assert(!isImporting);
766
767 } else {
768
769 try {
770
771 trackModel = TrackModelLoadManager.getInstance().load(f.getPath(), localFileName, this, false);
772
773 if (trackModel == null) { // load operation canceled
774 assert(hasCancelBeenRequested());
775 return LOAD_STATE_INCOMPLETED;
776
777 } else if(isImporting) { // ensure that file path is null - since not yet saved
778 trackModel.setFilepath(null);
779
780 } else if (recoveryFile != null) {
781
782 // If recovering - might be recovering an existing track that had been
783 // saved to the repository .. thus re-use the old file
784 trackModel.setFilepath(AudioPathManager.AUDIO_HOME_DIRECTORY + localFileName);
785 }
786
787 } catch (IOException e) {
788 e.printStackTrace();
789 setLoadScreenMessage("Failed to load audio file");
790 return LOAD_STATE_FAILED;
791
792 } catch (UnsupportedAudioFileException e) {
793 e.printStackTrace();
794 setLoadScreenMessage("Format not supported");
795 return LOAD_STATE_FAILED;
796 } catch (OutOfMemoryError e) {
797 e.printStackTrace();
798 setLoadScreenMessage("Out of memory");
799 return LOAD_STATE_FAILED;
800 }
801
802 }
803
804 // If was imported / recovered, then set as being modified
805 if (isImporting || recoveryFile != null) {
806 trackModel.setAudioModifiedFlag(true);
807 }
808
809 // Set the name for this track
810 String name = getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
811 if (name != null)
812 trackModel.setName(name);
813
814 initObservers(); // sets default name if non set
815
816 // If was recovering - get rid of temp data
817 if (recoveryFile != null) {
818 recoveryFile.delete();
819 recoveryFile = null; // ensure that out of a recovery state
820 }
821
822 // Keep meta as constant as possible for best results
823 updateData(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, TrackWidgetCommons.META_RUNNINGMSTIME_TAG + getRunningMSTimeFromRawAudio());
824
825 // Must make sure that this track is on the track graph model
826 try {
827 SwingUtilities.invokeAndWait(new Runnable() {
828
829 public void run() {
830
831 Frame parent = getParentFrame();
832 String pfname = (parent != null) ? parent.getName() : null;
833
834 TrackGraphNode tinf = AudioStructureModel.getInstance().getTrackGraphInfo(localFileName, pfname);
835 if (tinf == null) {
836
837 // Determine new initation time according to position
838 long initTime = (parent != null) ?
839 FrameLayoutDaemon.getInstance().getMSAtX(getX(), parent)
840 : 0;
841
842 // Keep TrackGraphModel consistant
843 AudioStructureModel.getInstance().onTrackWidgetAnchored(
844 localFileName,
845 pfname,
846 initTime,
847 AudioMath.framesToMilliseconds(trackModel.getFrameCount(), trackModel.getFormat()),
848 getName(),
849 getY());
850
851 }
852 }
853 });
854 } catch (InterruptedException e) {
855 e.printStackTrace();
856 } catch (InvocationTargetException e) {
857 e.printStackTrace();
858 }
859
860
861 // Notify layout manager - not really needed but to be extra safe force the daemon
862 // to be super consistant
863 FrameLayoutDaemon.getInstance().forceRecheck();
864
865 return LOAD_STATE_COMPLETED;
866 }
867
868 /**
869 * Used by save manager
870 */
871 public boolean doesNeedSaving() {
872 return (trackModel != null && (trackModel.isAudioModified() || trackModel.getFilepath() == null));
873 }
874
875 /**
876 * Used by save manager
877 */
878 public String getSaveName() {
879
880 if (trackModel != null && trackModel.getName() != null)
881 return "Sampled Track: " + trackModel.getName();
882 else return "A Sampled Track";
883
884 }
885
886 /**
887 * Saves audio if the audio is loaded and modified / unsaved.
888 * Blocking.
889 */
890 public void saveAudio() {
891 if (trackModel != null && trackModel.isAudioModified()) {
892 saveWidgetData();
893 }
894 }
895
896 /**
897 * Saves the audio bytes to file
898 */
899 @Override
900 protected void saveWidgetData() {
901
902 if (trackModel == null) return; // nothing to save
903
904 // If saving for the file time then get a filename
905 if (trackModel.getFilepath() == null) {
906 trackModel.setFilepath(AudioPathManager.AUDIO_HOME_DIRECTORY + localFileName);
907 loadFilenameArgument = localFileName; // set to now local, next load will be local
908 }
909
910 // Save audio bytes.
911 try {
912
913 AudioIO.savePCMAudioToWaveFile(
914 trackModel.getFilepath(),
915 trackModel.getAllAudioBytes(), // Safe: arrays are immutable
916 trackModel.getFormat());
917
918 // Reset modified flag
919 trackModel.setAudioModifiedFlag(false);
920
921 } catch (IOException e) {
922 e.printStackTrace();
923 } catch (UnsupportedAudioFileException e) {
924 e.printStackTrace();
925 }
926
927 // Ensure audio bytes can be collected if this has expired
928 if (isExpired()) {
929
930 try {
931 SwingUtilities.invokeAndWait(new Runnable() {
932 public void run() {
933 releaseMemory(true);
934 }
935 });
936 } catch (InterruptedException e) {
937 e.printStackTrace();
938 } catch (InvocationTargetException e) {
939 e.printStackTrace();
940 }
941
942 }
943
944
945 }
946
947 @Override
948 public void onDelete() {
949 super.onDelete();
950 // Its nice to keep everything layed out as much as possible
951 FrameLayoutDaemon.getInstance().resumeLayout(this);
952 }
953
954 @Override
955 protected void unloadWidgetData() {
956
957 // Still needs to save?
958 if (doesNeedSaving() || trackModel == null)
959 return; // Release memory later when saved
960
961 try {
962 // Release memory - on swing thread to avoid nullified model data wil painting / editing.
963 SwingUtilities.invokeAndWait(new Runnable() {
964 public void run() {
965 releaseMemory(true);
966 }
967 });
968 } catch (InterruptedException e) {
969 e.printStackTrace();
970 } catch (InvocationTargetException e) {
971 e.printStackTrace();
972 }
973
974
975 }
976
977 @Override
978 protected void tempUnloadWidgetData() { // basically this is deleting
979
980 // Need unloading?
981 if (trackModel == null) return;
982
983 // Get rid of old temporary files
984 if (recoveryFile != null && recoveryFile.exists()) {
985 recoveryFile.delete();
986 }
987
988 // Get rid of local file
989 File oldLocalFile = new File(AudioPathManager.AUDIO_HOME_DIRECTORY + localFileName);
990 if (oldLocalFile.isFile() && oldLocalFile.exists()) {
991 try {
992 oldLocalFile.delete();
993 } catch(Exception e) {
994 e.printStackTrace();
995 }
996 }
997
998 // Dump memory to a temp file
999 try {
1000
1001 // Always be unique
1002 String uniqueID = AudioPathManager.generateLocateFileName("wav");
1003 recoveryFile = File.createTempFile("APOLLO_BACKUP" + uniqueID, null);
1004
1005 // To avoid filling up the end-users hardisk, mark file to be
1006 // deleted on exit (if not already deleted).
1007 recoveryFile.deleteOnExit();
1008
1009 } catch (IOException e) {
1010 e.printStackTrace();
1011 return; // cannot dump ... must keep in memory.
1012 }
1013
1014 // Dump audio to file
1015 try {
1016 AudioIO.savePCMAudioToWaveFile(
1017 recoveryFile.getAbsolutePath(),
1018 trackModel.getAllAudioBytes(),
1019 trackModel.getFormat());
1020 } catch (IOException e1) {
1021 e1.printStackTrace();
1022 return; // cannot dump ... must keep in memory.
1023 } catch (UnsupportedAudioFileException e1) {
1024 e1.printStackTrace();
1025 return; // cannot dump ... must keep in memory.
1026 }
1027
1028 try {
1029 // Release memory - on swing thread to avoid nullified model data wil painting / editing.
1030 SwingUtilities.invokeAndWait(new Runnable() {
1031 public void run() {
1032 releaseMemory(false);
1033 }
1034 });
1035 } catch (InterruptedException e) {
1036 e.printStackTrace();
1037 } catch (InvocationTargetException e) {
1038 e.printStackTrace();
1039 }
1040
1041 }
1042
1043 /**
1044 * To be called by the swing thread only
1045 * This is nessessary to avoid runtime memory leaks!!
1046 */
1047 private void releaseMemory(boolean onlyIfExpired) {
1048
1049 if (!onlyIfExpired || (onlyIfExpired && isExpired())) {
1050
1051 if (trackModel != null) {
1052
1053 trackModel.removeObserver(fulltrackView);
1054 trackModel.removeObserver(this);
1055 trackModel = null;
1056
1057 assert (fulltrackView.getObservedSubject() == null);
1058 assert (fulltrackView.getMix() == null);
1059 }
1060
1061 fulltrackView.releaseBuffer();
1062
1063 ExpandedTrackManager.getInstance().removeObserver(this);
1064
1065 }
1066
1067 }
1068
1069 /**
1070 * Global Track model re-use ....
1071 */
1072 public SampledTrackModel getSharedSampledTrackModel(String localfilename) {
1073 if (trackModel != null &&
1074 trackModel.getLocalFilename().equals(localfilename)) {
1075 assert(localfilename.equals(localFileName));
1076 return trackModel;
1077 }
1078 return null;
1079
1080 }
1081
1082 /**
1083 * @see #getAudioFormat()
1084 *
1085 * @return
1086 * The audio bytes for this track widget. Null if not loaded.
1087 */
1088 public byte[] getAudioBytes() {
1089 return (trackModel != null) ? trackModel.getAllAudioBytes() : null;
1090 }
1091
1092 /**
1093 * @see #getAudioBytes()
1094 *
1095 * @return
1096 * The audio format of the audio bytes for this track widget. Null if not loaded.
1097 */
1098 public AudioFormat getAudioFormat() {
1099 return (trackModel != null) ? trackModel.getFormat() : null;
1100 }
1101
1102 /**
1103 * <b>Warning</b> if the path happens to be a path of a recovery file
1104 * then it is deleted if the widget is reloaded.
1105 *
1106 * @return
1107 * A full filepath to load the audio from. Never null. The path
1108 * may lead to a out of date file or no file.
1109 */
1110 public String getLatestSavedAudioPath() {
1111
1112 if (recoveryFile != null && recoveryFile.exists()) {
1113 return recoveryFile.getAbsolutePath();
1114 }
1115
1116 return AudioPathManager.AUDIO_HOME_DIRECTORY + localFileName;
1117
1118
1119 }
1120
1121 public void onGraphDirty(SampledTrackGraphView graph, Rectangle dirty) {
1122 dirty.translate(getX(), getY() - 1);
1123 FrameGraphics.invalidateArea(dirty);
1124 FrameGraphics.refresh(true);
1125 }
1126
1127 public Subject getObservedSubject() {
1128 return null;
1129 }
1130
1131
1132 @Override
1133 protected void onSizeChanged() {
1134 super.onSizeChanged();
1135 // Keep meta as constant as possible for best reults
1136 updateData(TrackWidgetCommons.META_LAST_WIDTH_TAG, TrackWidgetCommons.META_LAST_WIDTH_TAG + getWidth());
1137 }
1138
1139 /**
1140 * Responds to model changed events by updating the GUI ...
1141 */
1142 public void modelChanged(Subject source, SubjectChangedEvent event) {
1143
1144 // If the expansion selection has changed - check to see if was for this
1145 if (source == ExpandedTrackManager.getInstance()) {
1146
1147 // Show graph as being selected if expanded / pending to expand.
1148 if (trackModel != null &&
1149 ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel)) {
1150 fulltrackView.setBackColor(new Color(100, 100, 100), new Color(120, 120, 120));
1151 } else {
1152 fulltrackView.setBackColor(SampledTrackGraphView.DEFAULT_BACKGROUND_COLOR, SampledTrackGraphView.DEFAULT_BACKGROUND_HIGHTLIGHTS_COLOR);
1153 }
1154
1155 return;
1156 }
1157
1158 Frame parent = null;
1159
1160 switch (event.getID()) {
1161
1162 case ApolloSubjectChangedEvent.LOAD_STATUS_REPORT:
1163 // If this widget is loading - then update the load status
1164 if (isInLoadProgress()) {
1165
1166 // If the load has been cancelled, then cancel the loader
1167 if (hasCancelBeenRequested()) {
1168
1169 AudioFileLoader loader = (AudioFileLoader)source;
1170 loader.cancelLoad();
1171
1172 } else {
1173 float perc = ((Float)event.getState()).floatValue();
1174 if (perc > 1.0f) perc = 1.0f;
1175 perc *= FILE_LOADING_PERCENT_RANGE; // Dont stretch load to all of bar - still will have more work to do
1176 updateLoadPercentage(perc);
1177 }
1178
1179 }
1180
1181 break;
1182
1183 case ApolloSubjectChangedEvent.NAME_CHANGED:
1184 if (trackModel != null && !nameLabel.getText().equals(trackModel.getName())) {
1185 nameLabel.setText(trackModel.getName());
1186 }
1187
1188 // Get graph model consistant
1189 parent = getParentFrame();
1190 String pfname = (parent != null) ? parent.getName() : null;
1191
1192 AudioStructureModel.getInstance().onTrackWidgetNameChanged(
1193 localFileName,
1194 pfname,
1195 getName());
1196
1197 break;
1198
1199 case ApolloSubjectChangedEvent.AUDIO_INSERTED:
1200 case ApolloSubjectChangedEvent.AUDIO_REMOVED:
1201
1202 long newRunningTime = getRunningMSTimeFromRawAudio();
1203 assert(newRunningTime > 0);
1204
1205 long oldRunningTime = getRunningMSTimeFromMeta();
1206
1207 // Keep meta as constant as possible for best reults
1208 updateData(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, TrackWidgetCommons.META_RUNNINGMSTIME_TAG + newRunningTime);
1209
1210 if (trackModel != null) {
1211
1212 parent = getParentFrame();
1213
1214 // Keep TrackGraphModel consistant
1215 AudioStructureModel.getInstance().onTrackWidgetAudioEdited(
1216 localFileName,
1217 (parent != null) ? parent.getName() : null,
1218 newRunningTime);
1219
1220 if (trackModel.getSelectionStart() == 0 && oldRunningTime > newRunningTime) {
1221
1222
1223 Mutable.Long inittime = getInitiationTimeFromMeta();
1224 if (inittime == null) inittime = Mutable.createMutableLong(0);
1225 inittime.value += (oldRunningTime - newRunningTime);
1226
1227 updateData(TrackWidgetCommons.META_INITIATIONTIME_TAG,
1228 TrackWidgetCommons.META_INITIATIONTIME_TAG + inittime);
1229
1230 AudioStructureModel.getInstance().onTrackWidgetRemoved(localFileName, (parent != null) ? parent.getName() : null);
1231 AudioStructureModel.getInstance().onTrackWidgetAnchored(localFileName, (parent != null) ? parent.getName() : null,
1232 inittime.value, newRunningTime, getName(), getY());
1233 }
1234
1235 }
1236
1237 break;
1238
1239
1240 }
1241
1242 }
1243
1244 public void setObservedSubject(Subject parent) {
1245 }
1246
1247 @Override
1248 public void paintInFreeSpace(Graphics g) {
1249 paintInFreeSpace(g, false);
1250 }
1251
1252 public void paintInFreeSpace(Graphics g, boolean isAtFinalPass) {
1253
1254 if (isLoaded()) {
1255
1256
1257 if (ExpandedTrackManager.getInstance().isAnyExpandedTrackVisible() &&
1258 !isAtFinalPass) {
1259 // If a expanded track is in view .. then must render lastly
1260 return;
1261 }
1262
1263 // Check to see if dragging over a EditableSampledTrackGraphView
1264 MouseEvent me = MouseEventRouter.getCurrentMouseEvent();
1265 if (me != null) {
1266
1267 if (me.getComponent() != fulltrackView &&
1268 me.getComponent() instanceof EditableSampledTrackGraphView &&
1269 !((EditableSampledTrackGraphView)me.getComponent()).isPlaying()) {
1270
1271
1272 Point containerPoint = SwingUtilities.convertPoint(me.getComponent(),
1273 new Point(0,0), Browser._theBrowser.getContentPane());
1274
1275 Shape clipBackUp = g.getClip();
1276 g.setClip(null);
1277
1278 g.setColor(Color.ORANGE);
1279 ((Graphics2D)g).setStroke(EditableSampledTrackGraphView.GRAPH_BAR_STROKE);
1280 g.drawLine(
1281 containerPoint.x + me.getX(),
1282 containerPoint.y,
1283 containerPoint.x + me.getX(),
1284 containerPoint.y + me.getComponent().getHeight());
1285
1286 FrameGraphics.invalidateArea(new Rectangle(
1287 containerPoint.x + me.getX(),
1288 containerPoint.y,
1289 1,
1290 containerPoint.y + me.getComponent().getHeight() + 1));
1291
1292 // Restore clip
1293 g.setClip(clipBackUp);
1294
1295 g.setColor(SEMI_TRANSPARENT_FREESPACE_BACKCOLOR);
1296 g.fillRect(getX(), getY(), getWidth(), getHeight());
1297
1298 if (isAtFinalPass) { // final pass does not draw the borders... so must manually draw them
1299 g.setColor(Color.BLACK);
1300 ((Graphics2D)g).setStroke(FREESPACE_OUTLINING);
1301 g.drawRect(getX(), getY(), getWidth(), getHeight());
1302 }
1303
1304 return;
1305 }
1306
1307 }
1308
1309
1310
1311 }
1312
1313 super.paintInFreeSpace(g);
1314
1315 if (isLoaded()) {
1316
1317 Shape clipBackUp = g.getClip();
1318 Rectangle tmpClip = (clipBackUp != null) ? clipBackUp.getBounds() :
1319 new Rectangle(0, 0,
1320 Browser._theBrowser.getContentPane().getWidth(),
1321 Browser._theBrowser.getContentPane().getHeight());
1322
1323 g.setClip(tmpClip.intersection(getBounds()));
1324
1325 // Draw the name
1326 String name = getName();
1327 if (name == null) name = "Unnamed";
1328
1329 g.setFont(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
1330 g.setColor(TrackWidgetCommons.FREESPACE_TRACKNAME_TEXT_COLOR);
1331
1332 // Center track name
1333 FontMetrics fm = g.getFontMetrics(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
1334 Rectangle2D rect = fm.getStringBounds(name, g);
1335
1336 g.drawString(
1337 name,
1338 this.getX() + (int)((getWidth() - rect.getWidth()) / 2),
1339 this.getY() + (int)((getHeight() - rect.getHeight()) / 2) + (int)rect.getHeight()
1340 );
1341
1342 g.setClip(clipBackUp);
1343
1344 }
1345
1346 }
1347
1348
1349
1350 @Override
1351 public void paint(Graphics g) {
1352 super.paint(g);
1353
1354 if (isLoaded() && nameLabel != null) {
1355 nameLabel.paint(g);
1356
1357 if (shouldOmitIndexAudio()) {
1358
1359 int shiftOffset = SoundDesk.getInstance().isPlaying(trackMix.getChannelID()) ?
1360 -20 : 0;
1361
1362 IconRepository.getIcon("omitindexed.png").paintIcon(
1363 _swingComponent,
1364 g,
1365 getX() + getWidth() - EditableSampledTrackGraphView.LOCK_ICON_CORNER_OFFSET + shiftOffset,
1366 getY() + EditableSampledTrackGraphView.LOCK_ICON_CORNER_OFFSET - 16);
1367
1368 }
1369 }
1370
1371 }
1372
1373 private boolean ignoreInjection = false;
1374 private MouseEvent lastInsertME = null;
1375
1376 @Override
1377 protected void onParentStateChanged(int eventType) {
1378 super.onParentStateChanged(eventType);
1379
1380 Frame parent = null;
1381
1382 switch (eventType) {
1383
1384 // Logic for injecting audio tracks into EditableSampledTrackGraphView's
1385 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
1386 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
1387
1388 // Resume any layouts suspended by this track widget
1389 FrameLayoutDaemon.getInstance().resumeLayout(this);
1390
1391 if (trackModel != null) {
1392
1393 parent = getParentFrame();
1394
1395 // Determine new initation time according to anchored position...
1396 Mutable.Long initTime = getInitiationTimeFromMeta();
1397
1398 // If the user is restricting-y-axis movement then they might be moving
1399 // this tracks Y-position only for layout reasons as opposed to repositioning
1400 // where in the audio timeline the track should be. This must be accurate and
1401 // avoid loosing the exact initiation time due to pixel-resolutoin issues
1402 if (parent != null) {
1403
1404 boolean inferInitTime = true;
1405
1406 if (AudioFrameMouseActions.isYAxisRestictionOn()) {
1407 Mutable.Long ms = getInitiationTimeFromMeta();
1408 if (ms != null) {
1409 Mutable.Long timex = FrameLayoutDaemon.getInstance().getXAtMS(
1410 ms.value,
1411 parent);
1412 if (timex != null && timex.value == getX()) {
1413 initTime = ms;
1414 inferInitTime = false;
1415 }
1416 }
1417 }
1418
1419 inferInitTime &=
1420 (AudioFrameMouseActions.isMouseAnchoring() || AudioFrameMouseActions.isMouseStamping());
1421
1422 if (inferInitTime)
1423 initTime = Mutable.createMutableLong(FrameLayoutDaemon.getInstance().getMSAtX(getX(), parent));
1424 }
1425
1426 updateData(TrackWidgetCommons.META_INITIATIONTIME_TAG,
1427 TrackWidgetCommons.META_INITIATIONTIME_TAG + initTime);
1428
1429 // Keep TrackGraphModel consistant
1430 AudioStructureModel.getInstance().onTrackWidgetAnchored(
1431 localFileName,
1432 (parent != null) ? parent.getName() : null,
1433 initTime.value,
1434 AudioMath.framesToMilliseconds(trackModel.getFrameCount(), trackModel.getFormat()),
1435 getName(),
1436 getY());
1437
1438
1439 MouseEvent me = MouseEventRouter.getCurrentMouseEvent();
1440
1441 if (!ignoreInjection && me != null && me != lastInsertME &&
1442 (me.getButton() == MouseEvent.BUTTON2 ||
1443 me.getButton() == MouseEvent.BUTTON3)) {
1444
1445 // Although widgets filter out multiple parent changed events per mouse event -
1446 // it also considers the event type, and because this it remove itself from the frame/freespace
1447 // it jumbles up the filtering so that this will be invoked for each corner.
1448 lastInsertME = me;
1449
1450 if (me.getComponent() != fulltrackView &&
1451 me.getComponent() instanceof EditableSampledTrackGraphView) {
1452
1453 EditableSampledTrackGraphView editableSampledTrack =
1454 (EditableSampledTrackGraphView)me.getComponent();
1455
1456 // Inject this into the widget
1457 try {
1458 injectAudio(editableSampledTrack, me.getX(), true);
1459 } catch (IOException ex) {
1460 ex.printStackTrace();
1461 }
1462
1463 }
1464
1465 } else if (!ignoreInjection && me == lastInsertME) {
1466 // Note due to a injection removing this widget while in the midst of
1467 // anchoring, the widget parent event filtering will not work thus
1468 // must keep removing self until the mouse event has done.
1469 removeSelf();
1470 }
1471
1472 // Wakeup the daemon to note that it should recheck -- in the case that a track is
1473 // added to a non-overdubbed frame the audio structure model will not bother
1474 // adding the track until something requests for it.
1475 FrameLayoutDaemon.getInstance().forceRecheck();
1476 }
1477
1478 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
1479 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
1480
1481
1482/*
1483 // Listen for volume or mute changed events
1484 trackMix.addObserver(this);
1485
1486 // Listen for solo events and track sequence creation events
1487 MixDesk.getInstance().addObserver(this);
1488 */
1489
1490 break;
1491
1492
1493 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
1494 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
1495
1496 // If yanking this from the frame into free space suspend the layout daemon
1497 // for this frame
1498 if (MouseEventRouter.getCurrentMouseEvent() != null &&
1499 MouseEventRouter.getCurrentMouseEvent().getButton() == MouseEvent.BUTTON2 &&
1500 MouseEventRouter.getCurrentMouseEvent() != lastInsertME) {
1501 Frame suspended = DisplayIO.getCurrentFrame();
1502 if (suspended != null) {
1503 FrameLayoutDaemon.getInstance().suspendLayout(suspended, this);
1504 }
1505 }
1506
1507 if (trackModel != null) {
1508 parent = getParentFrame();
1509 // Keep TrackGraphModel consistant
1510 AudioStructureModel.getInstance().onTrackWidgetRemoved(localFileName,
1511 (parent != null) ? parent.getName() : null);
1512 }
1513
1514 case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
1515 /*
1516 trackMix.removeObserver(this);
1517 MixDesk.getInstance().removeObserver(this);*/
1518
1519 break;
1520
1521 }
1522
1523 }
1524
1525 /**
1526 * Injects this widgets bytes into a EditableSampledTrackGraphView
1527 * and removes this widget from freespace/parent-frame.
1528 *
1529 * Doesn't inject bytes if the target EditableSampledTrackGraphView is in a playing state.
1530 *
1531 * @param target
1532 * The target EditableSampledTrackGraphView to inject this widgets bytes into
1533 *
1534 * @param graphX
1535 * The X pixel in the target's graph. Must be in valid range.
1536 *
1537 * @param destroySelf
1538 * If want to destroy this widget
1539 *
1540 * @throws IOException
1541 * If the insert failed ... can occur if bytes need to be
1542 * converted into targets format.
1543 */
1544 public void injectAudio(EditableSampledTrackGraphView target, int graphX, boolean destroySelf)
1545 throws IOException {
1546 assert(target != null);
1547 assert(graphX >= 0);
1548 assert(graphX <= target.getWidth());
1549
1550 // Cannot inject into EditableSampledTrackGraphView's while playing, although
1551 // won't break anything, this would be confusing for the user.
1552 if (target.isPlaying()) return;
1553
1554 // Inject the audio at the graph poistion
1555 int insertFramePoint = target.frameAtX(graphX);
1556
1557 // Inject audio
1558 target.insertAudio(
1559 trackModel.getAllAudioBytes(),
1560 trackModel.getFormat(),
1561 insertFramePoint);
1562
1563 // Note: if removed from free space .. then there should be no
1564 // more references to this item therefore the memory will be freed
1565 // eventually after this invoke
1566 if (destroySelf) removeSelf();
1567 }
1568
1569 /**
1570 *
1571 * @return
1572 * The unique loval filename for this track. Auto-assigned - and rememered. Never null.
1573 */
1574 public String getLocalFileName() {
1575 return localFileName;
1576 }
1577
1578 /**
1579 * Determines the running time from the raw audio in memory.
1580 *
1581 * @return
1582 * The running time or this track in MS.
1583 * -1 if not loaded.
1584 */
1585 public long getRunningMSTimeFromRawAudio()
1586 {
1587 if (this.trackModel != null) {
1588 return AudioMath.framesToMilliseconds(trackModel.getFrameCount(), trackModel.getFormat());
1589 }
1590
1591 return -1;
1592 }
1593
1594 /**
1595 * Determines the running time from the meta data.
1596 *
1597 * @return
1598 * The running time or this track in MS.
1599 * -1 if unavilable.
1600 */
1601 public long getRunningMSTimeFromMeta() {
1602 return getStrippedDataLong(TrackWidgetCommons.META_RUNNINGMSTIME_TAG, new Long(-1));
1603 }
1604
1605 /**
1606 * @return
1607 * The name given to this widget... can be null.
1608 */
1609 public String getName() {
1610 if (this.trackModel != null)
1611 return trackModel.getName();
1612 else if (this.nameLabel != null) {
1613 return nameLabel.getText();
1614 }
1615 return getStrippedDataString(TrackWidgetCommons.META_NAME_TAG);
1616 }
1617
1618 /**
1619 * Determines the initiation time from the meta data.
1620 *
1621 * @return
1622 * The initiation time or this track in MS.
1623 * null if unavilable.
1624 */
1625 public Mutable.Long getInitiationTimeFromMeta() {
1626 Long l = getStrippedDataLong(TrackWidgetCommons.META_INITIATIONTIME_TAG, null);
1627 if (l != null) return Mutable.createMutableLong(l.longValue());
1628 return null;
1629 }
1630
1631 /**
1632 * Adjusts the initiation time for this track - to the exact millisecond.
1633 *
1634 * The x-position is updated (which will be eventually done with possibly better
1635 * accuracy if the layout daemon is on)
1636 *
1637 * @param specificInitTime
1638 * The new initiation time for this track in milliseconds
1639 */
1640 public void setInitiationTime(long specificInitTime) {
1641
1642 Frame parent = getParentFrame();
1643
1644 // Update x position if it can
1645 if (parent != null) {
1646 Timeline tl = FrameLayoutDaemon.getInstance().getTimeline(parent);
1647 if (tl != null) {
1648 this.setPosition(tl.getXAtMSTime(specificInitTime), getY());
1649 }
1650 }
1651
1652 updateData(TrackWidgetCommons.META_INITIATIONTIME_TAG,
1653 TrackWidgetCommons.META_INITIATIONTIME_TAG + specificInitTime);
1654
1655 AudioStructureModel.getInstance().onTrackWidgetPositionChanged(
1656 localFileName, (parent != null) ? parent.getName() : null, specificInitTime, getY());
1657
1658 }
1659
1660 /**
1661 * Sets Y position - retaining the init time.
1662 * @param newY
1663 */
1664 public void setYPosition(int newY) {
1665
1666 if (getY() == newY || isFloating()) return;
1667
1668 Frame parent = getParentFrame();
1669
1670 Mutable.Long initTime = getInitiationTimeFromMeta();
1671 if (initTime == null) return;
1672
1673 setPosition(getX(), newY);
1674
1675 AudioStructureModel.getInstance().onTrackWidgetPositionChanged(
1676 localFileName, (parent != null) ? parent.getName() : null, initTime.value, newY);
1677
1678 }
1679
1680
1681 private void updateBorderColor() {
1682
1683 // Get border color currently used
1684 Color oldC = getSource().getBorderColor();
1685
1686 Color newC = TrackWidgetCommons.getBorderColor(
1687 SoundDesk.getInstance().isSolo(trackMix.getChannelID()),
1688 trackMix.isMuted());
1689
1690 // Update the color
1691 if (!newC.equals(oldC)) {
1692 setWidgetEdgeColor(newC);
1693 }
1694 }
1695
1696 /**
1697 * State icons live at the top right corner
1698 *
1699 */
1700 private void invalidateStateIcons() {
1701 this.invalidateSelf(); // TODO
1702 }
1703
1704 private void setShouldOmitIndexAudio(boolean shouldOmit) {
1705 this.shouldOmitAudioIndexing = shouldOmit;
1706 if (!shouldOmit) {
1707 removeData(META_DONT_INDEX_AUDIO_TAG);
1708 } else {
1709 addDataIfCaseInsensitiveNotExists(META_DONT_INDEX_AUDIO_TAG);
1710 }
1711 invalidateStateIcons();
1712 }
1713
1714 public boolean shouldOmitIndexAudio() {
1715 return shouldOmitAudioIndexing;
1716 }
1717
1718// /**
1719// * Invalidates.
1720// * @param shouldLayout
1721// */
1722// private void setShouldLayout(boolean shouldLayout) {
1723//
1724// if (shouldLayout) {
1725// removeData(TrackWidgetCommons.META_OMIT_LAYOUT_TAG);
1726// } else {
1727// addDataIfCaseInsensitiveNotExists(TrackWidgetCommons.META_OMIT_LAYOUT_TAG);
1728// }
1729//
1730// invalidateStateIcons();
1731// }
1732//
1733
1734//
1735// public boolean shouldLayout() {
1736//
1737// return (!containsDataTrimmedIgnoreCase(TrackWidgetCommons.META_OMIT_LAYOUT_TAG));
1738// }
1739
1740 private boolean split(boolean extractSelection) {
1741
1742 // First is operation valid?
1743 if (trackModel != null && trackModel.getSelectionLength() > 1
1744 && !fulltrackView.isPlaying()
1745 && (trackModel.getFrameCount() - trackModel.getSelectionLength()) > EditableSampledTrackGraphView.MIN_FRAME_SELECTION_SIZE) {
1746
1747 // If so... can a slip be performed? i.e. is there unselected audio to
1748 // the left and right of selection
1749 if (trackModel.getSelectionStart() > 0 &&
1750 (trackModel.getSelectionStart() + trackModel.getSelectionLength()) < trackModel.getFrameCount()) {
1751
1752 // Perform split
1753 int rightSideStartFrame = trackModel.getSelectionStart() + trackModel.getSelectionLength();
1754
1755 // Create a new track widget to contain the right-side audio
1756 byte[] rightsideAudio = new byte[(trackModel.getFrameCount() - rightSideStartFrame) * trackModel.getFormat().getFrameSize()];
1757
1758 // Copy bytes into new location
1759 System.arraycopy(
1760 trackModel.getAllAudioBytes(),
1761 rightSideStartFrame * trackModel.getFormat().getFrameSize(),
1762 rightsideAudio,
1763 0, rightsideAudio.length);
1764
1765 byte[] selectedBytes = (extractSelection) ? trackModel.getSelectedFramesCopy() : null;
1766
1767 // Let this widget keep the left-side audio
1768 trackModel.setSelection(trackModel.getSelectionStart(), trackModel.getFrameCount() - trackModel.getSelectionStart());
1769 trackModel.removeSelectedBytes();
1770 trackModel.setSelection(0,0);
1771
1772 // Build the new neighbouring widget
1773 Frame target = getParentFrame();
1774 if (target == null) target = DisplayIO.getCurrentFrame();
1775
1776 // Determine init time
1777 Mutable.Long initTime = getInitiationTimeFromMeta();
1778
1779 if (initTime == null) initTime = Mutable.createMutableLong(0);
1780
1781 initTime.value += AudioMath.framesToMilliseconds(rightSideStartFrame, trackModel.getFormat());
1782
1783 SampledTrack rightSideTrack = SampledTrack.createFromMemory(
1784 rightsideAudio,
1785 trackModel.getFormat(),
1786 target,
1787 0, // X Coord overridden
1788 getY(),
1789 getName() + " part",
1790 trackMix);
1791
1792 // Anchor it
1793 rightSideTrack.ignoreInjection = true;
1794 target.addAllItems(rightSideTrack.getItems());
1795 rightSideTrack.ignoreInjection = false;
1796
1797 // Adjust initiation time to be exact
1798 rightSideTrack.setInitiationTime(initTime.value);
1799
1800 // If extracting audio then attatch it to the cursor
1801 if (selectedBytes != null) {
1802 assert(extractSelection);
1803
1804 SampledTrack extractedTrack = SampledTrack.createFromMemory(
1805 selectedBytes,
1806 trackModel.getFormat(),
1807 target,
1808 getX(), // X Coord overridden
1809 getY(),
1810 getName() + " part",
1811 trackMix);
1812
1813 FrameMouseActions.pickup(extractedTrack.getItems());
1814
1815 }
1816 }
1817
1818 return true;
1819 }
1820
1821 return false;
1822
1823 }
1824 /**
1825 * Doesn't expand if not anchored ...
1826 * @param addToExpand
1827 */
1828 private void expand(boolean addToExpand) {
1829
1830 Frame parent = getParentFrame();
1831 String pfname = (parent != null) ? parent.getName() : null;
1832 if (pfname == null) return;
1833
1834 if (!ExpandedTrackManager.getInstance().isTrackInExpansionSelection(trackModel)) {
1835
1836 if (addToExpand) {
1837
1838 // Show the expanded view for this track once control has been released
1839 ExpandedTrackManager.getInstance().addTrackToSelection(
1840 trackModel,
1841 pfname,
1842 _swingComponent.getBounds(),
1843 trackMix);
1844
1845 } else {
1846
1847 // Get rid of all popups
1848 PopupManager.getInstance().hideAutohidePopups();
1849
1850 int start = trackModel.getSelectionStart();
1851 int length = trackModel.getSelectionLength();
1852
1853 if (length <= 1) {
1854 start = 0;
1855 length = trackModel.getFrameCount();
1856 }
1857
1858 // Show the expanded view for this track
1859 ExpandedTrackManager.getInstance().expandSingleTrack(
1860 trackModel,
1861 _swingComponent.getBounds(),
1862 trackMix,
1863 pfname,
1864 start,
1865 length
1866 );
1867
1868 }
1869
1870 } else {
1871
1872 // Take away track from being selected.
1873 ExpandedTrackManager.getInstance().removeTrackFromSelection(trackModel);
1874 }
1875
1876 }
1877
1878 /**
1879 * The small popup for common actions.
1880 *
1881 * @author Brook Novak
1882 *
1883 */
1884 private class PlaybackPopup extends PlaybackControlPopup implements Observer {
1885
1886 private static final long serialVersionUID = 1L;
1887
1888 public PlaybackPopup() {
1889 miscButton.setActionCommand("expand");
1890 miscButton.setIcon(IconRepository.getIcon("expand.png"));
1891 miscButton.setToolTipText("Expand");
1892
1893 }
1894
1895 @Override
1896 public void onHide() {
1897 super.onHide();
1898
1899 // Listen for volume or mute changed events
1900 trackMix.removeObserver(this);
1901
1902 // Listen for solo events and track sequence creation events
1903 SoundDesk.getInstance().removeObserver(this);
1904 }
1905
1906 @Override
1907 public void onShow() {
1908 super.onShow();
1909 // Listen for volume or mute changed events
1910 trackMix.addObserver(this);
1911
1912 // Listen for solo events and track sequence creation events
1913 SoundDesk.getInstance().addObserver(this);
1914 updateVolume((int)(100 * trackMix.getVolume()));
1915 updateMute(trackMix.isMuted());
1916 updateSolo(SoundDesk.getInstance().isSolo(trackMix.getChannelID()));
1917 }
1918
1919 public void actionPerformed(ActionEvent e) {
1920 if (trackModel == null) return;
1921
1922 if (e.getSource() == playPauseButton) {
1923
1924 try {
1925
1926 if (!SoundDesk.getInstance().isPlaying(trackMix.getChannelID())) { // play / resume
1927
1928 int startFrame = -1, endFrame = -1;
1929
1930 // Resume playback?
1931 if (SoundDesk.getInstance().isPaused(trackMix.getChannelID())) {
1932 startFrame = SoundDesk.getInstance().getLastPlayedFramePosition(trackMix.getChannelID());
1933 if (startFrame >= 0 && startFrame < trackModel.getFrameCount()) {
1934
1935 // The user may have edited the audio track and reselected it
1936 // since the last pause. Thus select an appropriate end frame
1937 endFrame = (trackModel.getSelectionLength() > 1) ?
1938 trackModel.getSelectionStart() + trackModel.getSelectionLength():
1939 trackModel.getFrameCount() - 1;
1940
1941 // Changed selection? it play range invalid?
1942 if (endFrame <= startFrame || startFrame < trackModel.getSelectionStart()) {
1943 startFrame = -1; // Play new selection (see below)
1944
1945 } else if (endFrame >= trackModel.getFrameCount()) {
1946 endFrame = trackModel.getFrameCount() - 1;
1947 }
1948
1949 }
1950 }
1951
1952 // Play from beginning of selection to end of selection
1953 if (startFrame < 0) {
1954 startFrame = trackModel.getSelectionStart();
1955 endFrame = (trackModel.getSelectionLength() > 1) ?
1956 startFrame + trackModel.getSelectionLength():
1957 trackModel.getFrameCount() - 1;
1958 }
1959
1960 // Safety clamp:
1961 if (endFrame >= trackModel.getFrameCount()) {
1962 endFrame = trackModel.getFrameCount() - 1;
1963 }
1964
1965 if (startFrame < endFrame) {
1966 SoundDesk.getInstance().playSampledTrackModel(
1967 trackModel,
1968 trackMix.getChannelID(),
1969 startFrame,
1970 endFrame,
1971 0);
1972 }
1973
1974 } else { // pause
1975
1976 TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
1977
1978 if (ts != null &&
1979 ts.isPlaying()) {
1980
1981 // Mark channel as paused.
1982 SoundDesk.getInstance().setPaused(trackMix.getChannelID(), true);
1983
1984 // Stop playback for this channel
1985 ApolloPlaybackMixer.getInstance().stop(ts);
1986
1987 }
1988
1989 }
1990
1991 } catch (LineUnavailableException e1) {
1992 e1.printStackTrace();
1993 }
1994
1995 } else if (e.getSource() == stopButton) {
1996
1997 TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
1998
1999 // reset any paused mark
2000 SoundDesk.getInstance().setPaused(trackMix.getChannelID(), false);
2001
2002 if (ts != null &&
2003 ts.isPlaying()) {
2004 // Stop playback
2005 ApolloPlaybackMixer.getInstance().stop(ts);
2006 }
2007
2008 } else if (e.getSource() == rewindButton) {
2009
2010 trackModel.setSelection(0, 0);
2011 SoundDesk.getInstance().setPaused(trackMix.getChannelID(), false);
2012
2013 } else if (e.getSource() == miscButton) {
2014 expand(false);
2015 }
2016 }
2017
2018 public Subject getObservedSubject() {
2019 return null;
2020 }
2021
2022 public void setObservedSubject(Subject parent) {
2023 }
2024
2025 /**
2026 * Receives events from the track model OR from the observed track sequence.
2027 */
2028 public void modelChanged(Subject source, SubjectChangedEvent event) {
2029
2030 // Synch GUI with track state
2031 switch (event.getID()) {
2032
2033 case ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED: // from sound desk
2034
2035 if (event.getState().equals(trackMix.getChannelID())) {
2036 // The channel being played is the same as this one ...
2037 // even if the track model is unloaded must enter into a playing state
2038 // if the created track sequence will play
2039 TrackSequence ts = SoundDesk.getInstance().getTrackSequence(trackMix.getChannelID());
2040 assert(ts != null);
2041 assert(!ts.hasFinished());
2042 assert(!ts.isPlaying());
2043 ts.addObserver(this);
2044 }
2045
2046 break;
2047
2048 case ApolloSubjectChangedEvent.PLAYBACK_STARTED: // From observed track sequence
2049 stopButton.setEnabled(true);
2050 rewindButton.setEnabled(false);
2051 playPauseButton.setIcon(IconRepository.getIcon("pause.png"));
2052
2053 invalidateStateIcons();
2054
2055 SampledTrack.this.setWidgetEdgeThickness(TrackWidgetCommons.PLAYING_TRACK_EDGE_THICKNESS);
2056 //FrameGraphics.refresh(true);
2057 break;
2058
2059 case ApolloSubjectChangedEvent.PLAYBACK_STOPPED: // From observed track sequence
2060
2061 invalidateStateIcons();
2062
2063 rewindButton.setEnabled(true);
2064 stopButton.setEnabled(false);
2065 playPauseButton.setIcon(IconRepository.getIcon("play.png"));
2066
2067 // Note:
2068 // No need to remove self from observing the dead track since the track references this
2069 // and will get garbage collected
2070
2071 SampledTrack.this.setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
2072
2073 break;
2074
2075 case ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED: // When stopped or paused
2076 /*
2077 if (ae.getState().equals(trackMix.getChannelID())) {
2078
2079 if (MixDesk.getInstance().isPaused(trackMix.getChannelID())) {
2080 // Do nothing .. the paused mark is set prior to a stop
2081 } else {
2082 // Esnure that the GUI represents a stopped state
2083 stopButton.setEnabled(false);
2084 playPauseButton.setIcon(IconRepository.getIcon("play.png"));
2085 }
2086
2087 }*/
2088
2089 break;
2090
2091 case ApolloSubjectChangedEvent.VOLUME: // From obseved track mix
2092 updateVolume((int)(100 * trackMix.getVolume()));
2093 break;
2094
2095 case ApolloSubjectChangedEvent.MUTE: // From obseved track mix
2096 updateMute(trackMix.isMuted());
2097 updateBorderColor();
2098 break;
2099
2100 case ApolloSubjectChangedEvent.SOLO_PREFIX_CHANGED: // From mix desk
2101 updateSolo(SoundDesk.getInstance().isSolo(trackMix.getChannelID()));
2102 updateBorderColor();
2103 break;
2104 }
2105
2106
2107
2108 }
2109
2110 @Override
2111 protected void volumeChanged() {
2112 trackMix.setVolume(((float)volumeSlider.getValue()) / 100.0f);
2113 }
2114
2115 @Override
2116 protected void muteChanged() {
2117 trackMix.setMuted(muteButton.isSelected());
2118 }
2119
2120 @Override
2121 protected void soloChanged() {
2122 SoundDesk.getInstance().setSoloIDPrefix(soloButton.isSelected() ?
2123 trackMix.getChannelID() : null
2124 );
2125 }
2126
2127 }
2128
2129 @Override
2130 public boolean isWidgetEdgeThicknessAdjustable() {
2131 return false;
2132 }
2133
2134
2135
2136}
Note: See TracBrowser for help on using the repository browser.