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

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

First cut at supporting time-stretch

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