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

Last change on this file since 1561 was 1561, checked in by davidb, 3 years ago

A set of changes that spans three things: beat detection, time stretching; and a debug class motivated by the need to look at a canvas redraw issue most notable when a waveform widget is playing

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