source: trunk/src_apollo/org/apollo/widgets/SampledTrack.java@ 318

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

Refactored a class name and extended recorder widgets to have a perminant lifetime option (for optimum idea capturing!)

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