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

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

Improved widget linking... track-link coversion command

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