source: trunk/src_apollo/org/apollo/audio/util/MultiTrackPlaybackController.java@ 315

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

Apollo spin-off added

File size: 36.0 KB
Line 
1package org.apollo.audio.util;
2
3import java.util.HashMap;
4import java.util.HashSet;
5import java.util.LinkedList;
6import java.util.List;
7import java.util.Map;
8import java.util.Set;
9
10import javax.sound.sampled.LineUnavailableException;
11import javax.swing.SwingUtilities;
12
13import org.apollo.audio.ApolloPlaybackMixer;
14import org.apollo.audio.ApolloSubjectChangedEvent;
15import org.apollo.audio.SampledAudioManager;
16import org.apollo.audio.SampledTrackModel;
17import org.apollo.audio.TrackSequence;
18import org.apollo.audio.structure.AbsoluteTrackNode;
19import org.apollo.audio.structure.AudioStructureModel;
20import org.apollo.audio.structure.OverdubbedFrame;
21import org.apollo.audio.structure.TrackGraphLoopException;
22import org.apollo.audio.structure.TrackGraphNode;
23import org.apollo.io.AudioPathManager;
24import org.apollo.io.AudioIO.AudioFileLoader;
25import org.apollo.mvc.AbstractSubject;
26import org.apollo.mvc.Observer;
27import org.apollo.mvc.Subject;
28import org.apollo.mvc.SubjectChangedEvent;
29import org.apollo.util.AudioMath;
30import org.apollo.util.StringEx;
31import org.apollo.util.TrackModelHandler;
32import org.apollo.util.TrackModelLoadManager;
33import org.expeditee.gui.DisplayIO;
34import org.expeditee.gui.DisplayIOObserver;
35
36
37public class MultiTrackPlaybackController
38 extends AbstractSubject
39 implements TrackModelHandler, Observer, DisplayIOObserver {
40
41 /** Can be a framename or virtual filename (e.g. from a linked track) */
42 private String rootFrameName = null; // Shared resource
43
44 private String masterChannelID = null; // Shared resource
45
46 private boolean markedAsPaused = false; // by user convention .. just centralized model data
47
48 // Track-sequence like data for multitrack
49 private int suspendedFramePosition = 0;
50 private long initiationFramePosition = 0;
51 private int startFramePosition = 0;
52 private int endFramePosition = 0;
53
54 private boolean canInstantlyResume = false;
55
56 /** A flag set when playback is commenced and resets when the first track begins to play.
57 * .. or if the commence failed.*/
58 private boolean isPlaybackPending = false;
59
60 private MultiTrackPlaybackLoader loaderThread = null;
61
62 /** The tracks that are being loaded, played or stopped ... kept for resuming / fast reloading */
63 private List<Overdub> currentOverdubs = new LinkedList<Overdub>(); // SHARED RESOURCE
64 private Set<TrackSequence> currentTrackSequences = null;
65
66 private OverdubbedFrame currentODFrame = null;
67
68 private int cacheExpiryCounter = 0;
69 private static final int CACHE_DEPTH = 5;
70
71 /**
72 * Singleton design pattern
73 */
74 private static MultiTrackPlaybackController instance = new MultiTrackPlaybackController();
75 public static MultiTrackPlaybackController getInstance() {
76 return instance;
77 }
78
79 /**
80 * Singleton constructor:
81 * Sets up perminant observed subjects.
82 */
83 private MultiTrackPlaybackController() {
84
85 // Since track groups are cached .. for a awhile
86 TrackModelLoadManager.getInstance().addTrackModelHandler(this);
87
88 // Dynamically adjust group of tracks while loading or playing
89 AudioStructureModel.getInstance().addObserver(this);
90
91 // After a certain amount of frame changes since the last multi playback has occured
92 // the cached track models are freed to stop consuming all the memory.
93 DisplayIO.addDisplayIOObserver(this);
94
95 // For whenever a track sequence is created - must observe the created track sequences...
96 SoundDesk.getInstance().addObserver(this);
97 }
98
99
100 /**
101 * {@inheritDoc}
102 */
103 public Subject getObservedSubject() {
104 return null; // many!
105 }
106
107 /**
108 * {@inheritDoc}
109 */
110 public void modelChanged(Subject source, SubjectChangedEvent event) {
111
112 TrackSequence ts;
113
114 switch (event.getID()) {
115 case ApolloSubjectChangedEvent.GRAPH_TRACK_REMOVED: // TODO: Remove while loading!
116
117
118 if (isPlaying()) {
119 // Does the removed track belong to the current group?
120 TrackGraphNode tnode = (TrackGraphNode)event.getState();
121
122 synchronized(currentOverdubs) {
123
124 for (Overdub od : currentOverdubs) {
125
126 if (od.getTrackModel().getLocalFilename().equals(tnode.getLocalFilename())) {
127
128 TrackSequence trackSeq =
129 SoundDesk.getInstance().getTrackSequence(od.getChannelID());
130
131 if (trackSeq != null) { // && trackSeq.isPlaying()) {
132 ApolloPlaybackMixer.getInstance().stop(trackSeq);
133 }
134
135 }
136
137 }
138
139 }
140
141 }
142
143// Fall through
144 case ApolloSubjectChangedEvent.GRAPH_LINKED_TRACK_REMOVED: // TODO: Remove while loading!
145
146 if (event.getID() != ApolloSubjectChangedEvent.GRAPH_TRACK_REMOVED) {
147 if (isPlaying()) {
148
149 // Does the removed track belong to the current group?
150 String virtualFilename = (String)event.getState();
151
152 synchronized(currentOverdubs) {
153
154 for (Overdub od : currentOverdubs) {
155
156 if (od.getChannelID().indexOf(virtualFilename) >= 0) { // TODO: THIS IS TEMP - NOT RIGHT!! - 99% OK.. - Must revise this all anyway
157
158 TrackSequence trackSeq =
159 SoundDesk.getInstance().getTrackSequence(od.getChannelID());
160
161 if (trackSeq != null) { // && trackSeq.isPlaying()) {
162 ApolloPlaybackMixer.getInstance().stop(trackSeq);
163 }
164
165 }
166
167 }
168
169 }
170
171 }
172 }
173
174 // Fall through
175 case ApolloSubjectChangedEvent.GRAPH_LINKED_TRACK_ADDED:
176 case ApolloSubjectChangedEvent.GRAPH_TRACK_ADDED:
177 case ApolloSubjectChangedEvent.GRAPH_TRACK_EDITED:
178
179 // TODO: FINER CONTROL: If change is event related to the current group
180 // of overdubs and if it can be managed.
181 this.canInstantlyResume = false;
182
183 break;
184
185 case ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED:
186 assert(source == SoundDesk.getInstance());
187
188 String channelID = (String) event.getState();
189
190 if (!isPlaybackPending) break;
191 boolean doesBelong = false;
192
193 // Does the track sequence belong to this set?
194 synchronized(currentOverdubs) {
195 for (Overdub od : currentOverdubs) {
196 if (od.getChannelID().equals(channelID)) {
197 doesBelong = true;
198 break;
199 }
200 }
201 }
202
203 if (!doesBelong) break;
204
205 ts = SoundDesk.getInstance().getTrackSequence(channelID);
206 assert(ts != null);
207
208 if (currentTrackSequences == null)
209 currentTrackSequences = new HashSet<TrackSequence>();
210
211 assert(!currentTrackSequences.contains(ts));
212
213 ts.addObserver(this);
214 currentTrackSequences.add(ts);
215
216 // Note: if fails to start payback or is stopped before is playing
217 // then it does not need to unregister that the subject will be eventually freed.
218
219 // the currentTrackSequences set is nullified if the playback fails anyway...
220 break;
221
222 case ApolloSubjectChangedEvent.PLAYBACK_STARTED:
223 // Can get many of these over time in one playback call... so make sure that a event is raised
224 // on the first event received...
225 if (isPlaybackPending) {
226 isPlaybackPending = false;
227 initiationFramePosition = ((TrackSequence)source).getInitiationFrame();
228 suspendedFramePosition = 0;
229 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.PLAYBACK_STARTED));
230 }
231 break;
232
233 // Keep track of what is/isn't playing. Note that currentTrackSequences is cleared explicity
234 // event before stop events occur when playback is commences while already playing back
235 case ApolloSubjectChangedEvent.PLAYBACK_STOPPED:
236
237 if (currentTrackSequences != null && !currentTrackSequences.isEmpty()) {
238
239 currentTrackSequences.remove(source);
240
241 ts = (TrackSequence)source;
242 // Calculate suspended frame position .. this could be the last track stopped that actually has started playback
243 if (ts.getCurrentFrame() > ts.getStartFrame()) {
244 int susFrame = ts.getSuspendedFrame() + ts.getRelativeInitiationFrame() + startFramePosition;
245
246 if (susFrame > suspendedFramePosition)
247 suspendedFramePosition = susFrame;
248
249 }
250
251 if (currentTrackSequences.isEmpty()) {
252 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.PLAYBACK_STOPPED));
253 }
254 }
255
256 break;
257
258
259 }
260
261
262 }
263
264
265 /**
266 * {@inheritDoc}
267 */
268 public void setObservedSubject(Subject parent) {
269 // Ignore: many subjects observed
270 }
271
272 /**
273 * {@inheritDoc}
274 */
275 public SampledTrackModel getSharedSampledTrackModel(String localfilename) {
276 if (localfilename == null) return null;
277
278 // The track group is updated at every loaded track
279 synchronized(currentOverdubs) {
280 // Search cached tracks
281 for (Overdub od : currentOverdubs) {
282 if (od.getTrackModel().getFilepath() != null &&
283 localfilename.equals(od.getTrackModel().getLocalFilename())) {
284 return od.getTrackModel();
285 }
286 }
287 }
288
289 return null;
290 }
291
292 /**
293 * {@inheritDoc}
294 */
295 public void frameChanged() {
296 if (hasTrackCacheExpired() || isPlaying() || isLoading() || DisplayIO.getCurrentFrame() == null) return; // already expired - or is playing - so don't expire!
297
298 String currentFrameName = DisplayIO.getCurrentFrame().getName();
299
300 // Free track group every so often..
301
302 if (currentODFrame != null && rootFrameName != null &&
303 currentODFrame.getFrameName().equals(this.rootFrameName)) {
304 // Reset if traversed through a frame that belongs to the current group
305 if (currentODFrame.getChild(currentFrameName) != null) {
306 cacheExpiryCounter = CACHE_DEPTH; // reset
307 return;
308 }
309 }
310
311 if (currentFrameName.equals(rootFrameName)) {
312 cacheExpiryCounter = CACHE_DEPTH; // resets cache - ok since cache has no expired at this point
313 } else {
314 cacheExpiryCounter--;
315 if (hasTrackCacheExpired()) {
316 freeCurrentTrackGroup();
317 }
318 }
319 }
320
321 /**
322 * @return
323 * True if there are no cached tracks
324 */
325 private boolean hasTrackCacheExpired() {
326 return cacheExpiryCounter <= 0;
327 }
328
329 /**
330 * Releases all resource consumed by this. Stops any threads. Non-blocking.
331 * Intention: for shutting down Apollo. But can use if need to get rid of
332 * any resources consumed by this.
333 *
334 * Note that this does not attempt to stop any tracks being played.
335 *
336 */
337 public void releaseResources() {
338
339 if (isLoading())
340 loaderThread.cancel();
341
342 if (isPlaying())
343 stopPlayback();
344 }
345
346 /**
347 *
348 * @return
349 * True if currently loading tracks.
350 */
351 public boolean isLoading() {
352 return (loaderThread != null && !loaderThread.loadFinished);
353 }
354
355 /**
356 *
357 * @param rootFrameName
358 *
359 * @param masterMix
360 *
361 * @return
362 * True if currently loading tracks with the given mix/frame.
363 */
364 public boolean isLoading(String rootFrameName, String masterMix) {
365 return (isLoading() && isCurrentPlaybackSubject(rootFrameName, masterMix));
366 }
367
368 /**
369 *
370 * @return
371 * True if currently playing
372 */
373 public boolean isPlaying() {
374 return currentTrackSequences != null && !currentTrackSequences.isEmpty();
375 }
376
377 /**
378 *
379 * @param rootFrameName
380 *
381 * @param masterMix
382 *
383 * @return
384 * True if currently playing the given mix/frame.
385 */
386 public boolean isPlaying(String rootFrameName, String masterMix) {
387 return (isPlaying() && isCurrentPlaybackSubject(rootFrameName, masterMix));
388
389 }
390
391 /**
392 * Sets a paused mark - centralized paused info for multiple viewers.
393 * Fires a {@link ApolloSubjectChangedEvent#PAUSE_MARK_CHANGED} event.
394 *
395 * @param isMarked
396 * True if to become marked. False to reset.
397 */
398 public void setPauseMark(boolean isMarked) {
399 this.markedAsPaused = isMarked;
400 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED));
401 }
402
403 /**
404 * @return
405 * True if the multplayback controller is marked as paused.
406 */
407 public boolean isMarkedAsPaused() {
408 return this.markedAsPaused;
409 }
410
411 /**
412 *
413 * @param rootFrameName
414 *
415 * @param masterMix
416 *
417 * @return
418 * True if currently playing the given mix/frame.
419 */
420 public boolean isMarkedAsPaused(String rootFrameName, String masterMix) {
421 return (markedAsPaused && isCurrentPlaybackSubject(rootFrameName, masterMix));
422 }
423
424 public boolean isCurrentPlaybackSubject(String rootFrameName, String masterMix) {
425 return (StringEx.equals(this.rootFrameName, rootFrameName) &&
426 StringEx.equals(this.masterChannelID, masterMix));
427 }
428
429
430 /**
431 * In order to explicitly release the references of track models kept in the cache.
432 * Note that releasing is handled internally.
433 *
434 * If the track group is currently playing then nothing will result from this call
435 *
436 */
437 public void freeCurrentTrackGroup() {
438 if (isPlaying()) return;
439
440 synchronized(currentOverdubs) {
441 currentOverdubs.clear();
442 }
443 cacheExpiryCounter = -1;
444 }
445
446
447 /**
448 * Adds a listener to the loader for recieving notifications about the load progress.
449 *
450 * @param listener
451 * The listener to attack. Must not be null.
452 * If already on the listener list then it wont be added twice
453 *
454 * @return
455 * Null if did not attack due to not loading.
456 * Otherwise all of the currently loaded tracks.
457 * Note, may still receive notifications for some of the returned
458 * loadeds tracks ... as the events could currently on the swing queue
459 */
460 public List<String> attachLoadListener(MultitrackLoadListener listener) {
461 assert(listener != null);
462 if (isLoading()) {
463
464 List<String> loaded = new LinkedList<String>();
465
466 loaderThread.addLoadListener(listener);
467 synchronized (currentOverdubs) {
468
469 for (Overdub od : currentOverdubs) {
470 loaded.add(od.getTrackModel().getLocalFilename());
471 }
472 }
473
474 return loaded;
475 }
476
477 return null;
478 }
479
480 /**
481 * Cancels loading phase of playback for the given frame/mix .. if
482 * there is anything currently loading.
483 *
484 * @param rootFrameName
485 *
486 * @param masterMix
487 *
488 */
489 public void cancelLoad(String rootFrameName, String masterMix) {
490 if (isCurrentPlaybackSubject(rootFrameName, masterMix) && isLoading()) {
491 loaderThread.cancel();
492 }
493 }
494
495 /**
496 * Stops the tracks from playing as soon as possible.
497 */
498 public void stopPlayback() {
499 ApolloPlaybackMixer.getInstance().stop(currentTrackSequences);
500 // Notes: eventually their stop events will be invoked on this thread.
501
502 }
503
504
505 /**
506 * Asynchronously plays a frame of a linked track. Must be invoked from the swing thread.
507 * Non-blocking - but sends feedback messages <i>on a dedicated thread</i> via the
508 * given load listener.
509 *
510 * Raises {@link ApolloSubjectChangedEvent#MULTIPLAYBACK_LOADING} event when loading comences.
511 * Raises playback events on success.
512 *
513 * If this is currentlly in a loading state it will be cancelled and a the playback
514 * call will halt until its cancelled (in acceptable amount of time for the user to interactive with).
515 *
516 * @param listener
517 * The listener used for callbacks of load progress.
518 * Must never be null.
519 *
520 * @param rootFrameName
521 * The root frame from where all the tracks are to be loaded from.
522 * Must never be null.
523 *
524 * @param masterMixID
525 * The master mix ID.
526 *
527 * @param resume
528 * True if resuming - the last-played files will be resumed instantly iff
529 * the last played set of tracks don't require reloading... you can resume with
530 * false but by resuming with when you want to resume it could load faster.
531 *
532 * @param relativeStartFrame
533 * The start frame from when all the tracks should commence in the playback mixer.
534 * Must be positive.
535 *
536 * @param startFrame
537 * The start from from <i>within</i> the group of tracks when playback should begin.
538 * Must be positive. Clamped.
539 *
540 * @param endFrame
541 * Must be larger than start from. Clamped.
542 *
543 */
544 public void playFrame(
545 MultitrackLoadListener listener,
546 String rootFrameName,
547 String masterMixID,
548 boolean resume,
549 int relativeStartFrame,
550 int startFrame,
551 int endFrame) {
552
553 assert(relativeStartFrame >= 0);
554 assert(startFrame >= 0);
555 assert(endFrame > startFrame);
556 assert(listener != null);
557 assert(rootFrameName != null);
558 assert(masterMixID != null);
559
560 // Check if curently loading:
561 if(loaderThread != null) {
562 loaderThread.cancel(); // non blocking
563 // NOTES: If the load thread is finished and there is an event waiting to be proccessed
564 // from the load thread (sometime after this event has finished proccessing) then explicitly
565 // cancel so that the waiting play event will definitly be aborted.
566
567 // Also: cannot wait on it to finish because it may be waiting on some things to
568 // proccess on the swing thread... must leave it to die in its won time.
569 }
570
571 // Check if currently playing
572 if (isPlaying()) {
573 stopPlayback();
574 }
575
576 // Must clear the current track sequences even though eventually they will be removed
577 // due to the stop call... because in order for them to remove a away event on the queue behind
578 // this call will do so... hence it is impossible to wait on the stop events to raise since
579 // they are waiting for this operation to finish. Thus must explicity clear the track sequences
580 // in order for playback to commence:
581 if (currentTrackSequences != null && !currentTrackSequences.isEmpty()) {
582 currentTrackSequences.clear();
583 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.PLAYBACK_STOPPED));
584 }
585
586 this.rootFrameName = rootFrameName;
587 this.masterChannelID = masterMixID;
588
589 List<MultitrackLoadListener> loadListeners = new LinkedList<MultitrackLoadListener>();
590 loadListeners.add(listener);
591
592 // Note that the a group of track resuming may have to reload due to the cache expiring
593 if (resume
594 && !hasTrackCacheExpired()
595 && canInstantlyResume
596 && isCurrentPlaybackSubject(rootFrameName, masterMixID)) {
597
598 // Notify listener that load phase has instnatly completed
599 notifyListeners(loadListeners, MultitrackLoadListener.LOAD_COMPLETE, null, false);
600
601 // Play back ..s
602 commencePlayback(relativeStartFrame, startFrame, endFrame, loadListeners);
603
604
605 } else {
606
607 // Load track and their start positions
608 loaderThread = new MultiTrackPlaybackLoader(relativeStartFrame, startFrame, endFrame, loadListeners);
609
610 // Notify when in loading state.
611 fireSubjectChanged(new SubjectChangedEvent(ApolloSubjectChangedEvent.MULTIPLAYBACK_LOADING));
612
613 loaderThread.start(); // will call commencePlayback once loaded.
614
615 }
616
617 }
618
619
620 /**
621 * @return
622 * The last frame within all of the tracks that was rendered.
623 * Can be make no sence if the audio structure has dramatically changed.
624 */
625 public int getLastSuspendedFrame() {
626 return this.suspendedFramePosition;
627 }
628
629 /**
630 * @return
631 * The last time a group of overdubs were played this is the first frame
632 * <i>within the apollo mixers timeline that the group began.</i>
633 */
634 public long getLastInitiationFrame() {
635 return this.initiationFramePosition;
636 }
637
638 /**
639 * @return
640 * The last time a group of overdubs were played this is the starting frame from
641 * <i>within the track-groups combined timeline.</i>
642 */
643 public int getLastStartFrame() {
644 return this.startFramePosition;
645 }
646
647 /**
648 * @return
649 * The last time a group of overdubs were played this is the ending frame from
650 * <i>within the track-groups combined timeline.</i>
651 */
652 public int getLastEndFrame() {
653 return this.endFramePosition;
654 }
655
656 /**
657 * Begins playback for the current set of overdubs.
658 *
659 * Must not be in a playing state. MUST BE ON THE SWING THREAD.
660 *
661 * @param relativeStartFrame
662 * The start frame from when all the tracks should commence in the playback mixer.
663 * Must be positive.
664 *
665 * @param startFrame
666 * The start from from <i>within</i> the group of tracks when playback should begin.
667 * Must be positive. Clamped.
668 *
669 * @param endFrame
670 * Must be larger than start from. Clamped.
671 *
672 * @param loadListeners
673 * The listeners to receive status reports. Must not be shared. Must not be null or empty.
674 */
675 private void commencePlayback(
676 int relativeStartFrame,
677 int startFrame,
678 int endFrame,
679 List<MultitrackLoadListener> loadListeners) {
680
681 assert(!isPlaying());
682 assert(!isPlaybackPending);
683 assert(loadListeners != null);
684 assert(!loadListeners.isEmpty());
685
686 startFramePosition = startFrame;
687 endFramePosition = endFrame;
688
689 // If got to this stage then instant resume is defiitly supported.
690 canInstantlyResume = true;
691
692 // Reset the cache now that playing
693 cacheExpiryCounter = CACHE_DEPTH;
694
695 // Construct a list that is not shared by other threads and that
696 // only contains overdubs in the playing range.
697 List<Overdub> dubList = new LinkedList<Overdub>();
698
699
700 long totalFrames = 0;
701
702 synchronized(currentOverdubs) {
703
704 // Prepare each overdub - defining start, end and initation times.
705 for (Overdub od : currentOverdubs) {
706
707 // Keep track of total frames in track for clamping later
708 long odEndFrame = od.getABSInitiationFrame() + od.getTrackModel().getFrameCount();
709 if (odEndFrame > totalFrames) totalFrames = odEndFrame;
710
711 if (startFrame >= (od.getABSInitiationFrame() + od.getTrackModel().getFrameCount()) ||
712 endFrame <= od.getABSInitiationFrame()) {
713 // Exclude this track - its not in range
714
715 } else { // play this track - it is in range
716
717 od.relativeInitiationFrame = (int)(od.getABSInitiationFrame() - startFrame);
718
719 od.startFrame = (int)((startFrame > od.getABSInitiationFrame()) ?
720 startFrame - od.getABSInitiationFrame() : 0);
721
722 od.endFrame = (int)((endFrame < odEndFrame) ?
723 endFrame - od.getABSInitiationFrame() : od.getTrackModel().getFrameCount() - 1);
724
725 if (od.startFrame < od.endFrame)
726 // Include in playlist
727 dubList.add(od);
728 }
729
730 }
731
732 }
733
734 // Clamp:
735 if (startFramePosition > totalFrames)
736 startFramePosition = (int)totalFrames;
737
738 if (endFramePosition > totalFrames)
739 endFramePosition = (int)totalFrames;
740
741 if (dubList.isEmpty()) {
742
743 notifyListeners(loadListeners, MultitrackLoadListener.NOTHING_TO_PLAY, null, false);
744
745 } else {
746
747 boolean succeeded = false;
748 try {
749
750 // Commence playback...
751 isPlaybackPending = true;
752 SoundDesk.getInstance().playOverdubs(dubList); // fires events which are relayed to observers
753 succeeded = true;
754
755 } catch (LineUnavailableException e) {
756
757 notifyListeners(loadListeners, MultitrackLoadListener.LOAD_FAILED_PLAYBACK, e, false);
758
759 } finally {
760
761 if (!succeeded) { // reset flag and clear track sequence list
762 currentTrackSequences = null;
763 isPlaybackPending = false;
764 }
765 }
766 }
767 }
768
769 /**
770 * Notifies the load listeners .. must notify on swing thread to
771 * remember to pass flag in need to.
772 *
773 * @param listeners
774 * when accessed if invoteLaterOnSwing
775 *
776 * @param id
777 *
778 * @param state
779 *
780 * @param invoteLaterOnSwing
781 */
782 private void notifyListeners(
783 List<MultitrackLoadListener> listeners,
784 int id,
785 Object state,
786 boolean invoteLaterOnSwing) {
787
788 if (invoteLaterOnSwing) {
789
790 final List<MultitrackLoadListener> listeners1 = listeners;
791 final int id1 = id;
792 final Object state1 = state;
793 SwingUtilities.invokeLater(new Runnable() {
794 public void run() {
795 synchronized(listeners1) {
796 notifyListeners(listeners1, id1, state1, false);
797 }
798 }
799 });
800
801 } else {
802
803 for (MultitrackLoadListener listener : listeners)
804 listener.multiplaybackLoadStatusUpdate(id, state);
805 }
806 }
807
808 /**
809 * Loads all tracks reachable from a given frame.
810 *
811 * @author Brook Novak
812 *
813 */
814 private class MultiTrackPlaybackLoader extends Thread {
815
816 /** Set to true once finished running. */
817 private boolean cancelRequested = false;
818 private boolean loadFinished = false; // Once playback has commenced - or failed/aborted... not like isAlive
819
820 private OverdubbedFrame rootODFrame = null;
821 private int relativeStartFrame;
822 private int startFrame;
823 private int endFrame;
824 private List<MultitrackLoadListener> loadListeners = null; // reference immutable, contents not. Shared resource
825
826 MultiTrackPlaybackLoader(int relativeStartFrame,
827 int startFrame,
828 int endFrame,
829 List<MultitrackLoadListener> loadListeners) {
830
831 super("Multitrack Loader");
832
833 this.loadListeners = loadListeners;
834 this.relativeStartFrame = relativeStartFrame;
835 this.startFrame = startFrame;
836 this.endFrame = endFrame;
837 }
838
839 /**
840 * Cancels the load stage.
841 * NOTE: May actually succeed... but when it comes to commencing the playback then
842 * then the play will abort.
843 *
844 */
845 public void cancel() {
846 cancelRequested = true;
847 }
848
849 public void addLoadListener(MultitrackLoadListener ll) {
850 synchronized(loadListeners) {
851 if (!loadListeners.contains(ll)) loadListeners.add(ll);
852 }
853 }
854
855 public void run() {
856
857 assert(rootFrameName != null);
858 assert(masterChannelID != null);
859
860 synchronized(loadListeners) {
861 assert(loadListeners != null);
862 assert(!loadListeners.isEmpty());
863 }
864
865 boolean hasSucceeded = false;
866
867 try {
868
869 if (cancelRequested) {
870 notifyListeners(loadListeners,
871 MultitrackLoadListener.LOAD_CANCELLED,
872 null, true);
873 return;
874 }
875
876 // First fetch the graph for the rootframe to play
877
878 boolean hasUpdated = false;
879 do {
880 try {
881 AudioStructureModel.getInstance().waitOnUpdates();
882 hasUpdated = true;
883 } catch (InterruptedException e) {
884 e.printStackTrace();
885 continue;
886 }
887 } while (!hasUpdated);
888
889 if (cancelRequested) {
890 notifyListeners(loadListeners,
891 MultitrackLoadListener.LOAD_CANCELLED,
892 null, true);
893 return;
894 }
895
896 boolean hasFetched = false;
897 do {
898 try {
899 rootODFrame = AudioStructureModel.getInstance().fetchGraph(rootFrameName);
900 hasFetched = true;
901 } catch (InterruptedException e) { // cancelled
902 /* Consume */
903 } catch (TrackGraphLoopException e) { // contains loop
904 notifyListeners(loadListeners, MultitrackLoadListener.LOAD_FAILED_BAD_GRAPH, e, true);
905 return;
906
907 }
908 } while (!hasFetched);
909
910 if (cancelRequested) {
911 notifyListeners(loadListeners,
912 MultitrackLoadListener.LOAD_CANCELLED,
913 null, true);
914 return;
915 }
916
917 // Was there anything to play... (or does the frame even exist?)
918 if (rootODFrame == null) {
919 notifyListeners(loadListeners, MultitrackLoadListener.NOTHING_TO_PLAY, null, true);
920 return;
921 }
922
923 // Get the absolute layout of the track graph.... i.e. a flattened view with
924 // all absolute initiation times stating from ms time 0.
925 ABSTrackGraphRetreiver absTrackGraphRetreiver = new ABSTrackGraphRetreiver(
926 rootODFrame, masterChannelID);
927
928 try {
929 SwingUtilities.invokeAndWait(absTrackGraphRetreiver);
930 } catch (Exception e) {
931 notifyListeners(loadListeners, MultitrackLoadListener.LOAD_FAILED_GENERIC, e, true);
932 return;
933 }
934
935 if (cancelRequested) {
936 notifyListeners(loadListeners,
937 MultitrackLoadListener.LOAD_CANCELLED,
938 null, true);
939 return;
940 }
941
942 assert(absTrackGraphRetreiver.absGraph != null);
943
944 // There are no track to play...
945 if (absTrackGraphRetreiver.absGraph.isEmpty()) {
946 notifyListeners(loadListeners, MultitrackLoadListener.NOTHING_TO_PLAY, null, true);
947 return;
948 }
949
950 // First pass: get the list of all filenames to load...
951
952 // Throw away tracks in the current track group that aren't in the new track group...
953 // keeping tracks that are already loaded.
954 // For the tracks that are kept ... re-create there overdubbed frame info
955 List<Overdub> transferred = new LinkedList<Overdub>();
956
957 synchronized(currentOverdubs) {
958
959 // Important to chck while locking currentOverdubs since this thread could be
960 // cancelled and another one of these threads loading and wanting to also change the
961 // current overdubs.
962 if (cancelRequested) {
963 notifyListeners(loadListeners,
964 MultitrackLoadListener.LOAD_CANCELLED,
965 null, true);
966 return;
967 }
968
969 for (Overdub od : currentOverdubs) {
970
971 for (int i = 0; i < absTrackGraphRetreiver.absGraph.size(); i++) {
972
973 AbsoluteTrackNode absNode = absTrackGraphRetreiver.absGraph.get(i);
974
975 if (od.getTrackModel().getLocalFilename().equals(absNode.getTrackNode().getLocalFilename())) {
976 transferred.add(new Overdub(
977 od.getTrackModel(),
978 absNode.getChannelID(),
979 AudioMath.millisecondsToFrames(absNode.getABSStartTime(),
980 SampledAudioManager.getInstance().getDefaultPlaybackFormat())));
981 absTrackGraphRetreiver.absGraph.remove(i);
982 i --;
983 }
984
985 }
986 }
987
988 currentOverdubs.clear();
989 currentOverdubs.addAll(transferred);
990
991 }
992
993 // Notify load handlers of transferred tracks
994 for (Overdub od : transferred) {
995 notifyListeners(loadListeners, MultitrackLoadListener.TRACK_LOADED,
996 od.getTrackModel().getLocalFilename(), true);
997 }
998
999 // Go through and load each track one by one.
1000 // Note: loadedTrackModels exludes transferred models because they have been dealt with
1001 // already. Maps Localfilename - > track model
1002 Map<String, SampledTrackModel> loadedTrackModels = new HashMap<String, SampledTrackModel>();
1003
1004 // Load the tracks one by one... Update the track group incrementally
1005 for (AbsoluteTrackNode absNode : absTrackGraphRetreiver.absGraph) {
1006
1007 SampledTrackModel stm = loadedTrackModels.get(absNode.getTrackNode().getLocalFilename());
1008
1009 if (stm != null) {
1010 assert(stm.getLocalFilename().equals(absNode.getTrackNode().getLocalFilename()));
1011 } else { // must load / retreive from somewhere the new track model
1012
1013 try {
1014 stm = TrackModelLoadManager.getInstance().load(
1015 AudioPathManager.AUDIO_HOME_DIRECTORY + absNode.getTrackNode().getLocalFilename(),
1016 absNode.getTrackNode().getLocalFilename(),
1017 new Observer() { // opps, my confusing design pattern gone wrong!
1018
1019 public Subject getObservedSubject() {
1020 return null;
1021 }
1022
1023 /**
1024 * Cancel the load operation if a cancel request is opening
1025 */
1026 public void modelChanged(Subject source, SubjectChangedEvent event) {
1027 assert(event.getID() == ApolloSubjectChangedEvent.LOAD_STATUS_REPORT);
1028 if (cancelRequested) {
1029 ((AudioFileLoader)source).cancelLoad();
1030 }
1031 }
1032
1033 public void setObservedSubject(Subject parent) {
1034 }
1035
1036 }, // No need to observe
1037 true); // Search around in expeditee memory for the track
1038
1039 } catch (Exception e) {
1040 e.printStackTrace();
1041 notifyListeners(loadListeners,
1042 MultitrackLoadListener.TRACK_LOAD_FAILED_IO,
1043 absNode.getTrackNode().getLocalFilename(),
1044 true);
1045 continue;
1046 }
1047
1048 // stm must only be null if a cancel was actually requested.
1049 assert(stm != null || (stm == null && cancelRequested));
1050
1051 if (stm == null || cancelRequested) {
1052 notifyListeners(loadListeners,
1053 MultitrackLoadListener.LOAD_CANCELLED,
1054 null, true);
1055 return;
1056 }
1057
1058 }
1059
1060 synchronized(currentOverdubs) {
1061 // Important to chck while locking currentOverdubs since this thread could be
1062 // cancelled and another one of these threads loading and wanting to also change the
1063 // current overdubs.
1064 if (cancelRequested) {
1065 notifyListeners(loadListeners,
1066 MultitrackLoadListener.LOAD_CANCELLED,
1067 null, true);
1068 return;
1069 }
1070 currentOverdubs.add(new Overdub(stm, absNode.getChannelID(), AudioMath.millisecondsToFrames(absNode.getABSStartTime(),
1071 SampledAudioManager.getInstance().getDefaultPlaybackFormat())));
1072 }
1073
1074 // Notify of loaded track
1075 notifyListeners(loadListeners,
1076 MultitrackLoadListener.TRACK_LOADED,
1077 absNode.getTrackNode().getLocalFilename(),
1078 true);
1079
1080 } // load next track
1081
1082 // It is possible that all of the loads failed that now left with
1083 // nothing to play... so do a check before continuing
1084 boolean isEmpty = false;
1085 synchronized(currentOverdubs) {
1086 isEmpty = currentOverdubs.isEmpty();
1087 }
1088
1089 if (isEmpty) {
1090 notifyListeners(loadListeners, MultitrackLoadListener.NOTHING_TO_PLAY, null, true);
1091 return;
1092 }
1093
1094 hasSucceeded = true;
1095
1096 // Reset the cache now that playing (safety precaution)
1097 cacheExpiryCounter = CACHE_DEPTH;
1098
1099 } finally {
1100 // Set flag for load currect state
1101 if (!hasSucceeded) loadFinished = true;
1102 }
1103
1104 // Must commence on the swing thread
1105 SwingUtilities.invokeLater(new MultiTrackPlaybackCommencer());
1106 }
1107
1108 /**
1109 * Commences playback from the swing thread.
1110 * @author Brook Novak
1111 */
1112 private class MultiTrackPlaybackCommencer implements Runnable {
1113
1114
1115 public void run() {
1116
1117 try {
1118
1119 // Note: loadListeners etc.. won't change since this is inner class created from
1120 // parent...
1121 assert(loadListeners != null);
1122 assert(!loadListeners.isEmpty());
1123
1124 // This is called from the loader thread once set to play.
1125 // However during the time when the load thread scheduled this on the AWT Queue
1126 // and this actually running another AWT Event might have either directly started playback
1127 // OR began loading a new set of tracks... either all must discard this redundant request
1128 if (isPlaying() || MultiTrackPlaybackLoader.this != loaderThread || cancelRequested) {
1129 // Notice the reference compare with loaderThread: since there could be another
1130 // thread now starting... which implies that this should actually cancel!
1131
1132 notifyListeners(loadListeners,
1133 MultitrackLoadListener.LOAD_CANCELLED,
1134 null, false);
1135
1136 return; // discard redundant request
1137 }
1138
1139 } finally {
1140 // Set load state for c
1141 loadFinished = true;
1142 }
1143
1144 // Remmember root frame of new playback
1145 currentODFrame = rootODFrame;
1146
1147 commencePlayback(relativeStartFrame, startFrame, endFrame, loadListeners);
1148
1149
1150
1151 }
1152
1153 }
1154
1155 /**
1156 * Retreives the overdub initiation times / channel ids from the swing thread.
1157 *
1158 * @author Brook Novak
1159 */
1160 private class ABSTrackGraphRetreiver implements Runnable {
1161
1162 private String masterMixID;
1163 private OverdubbedFrame rootODFrame;
1164 private List<AbsoluteTrackNode> absGraph = null;
1165
1166 ABSTrackGraphRetreiver(OverdubbedFrame rootODFrame, String masterMixID) {
1167 this.rootODFrame = rootODFrame;
1168 this.masterMixID = masterMixID;
1169 }
1170
1171 public void run() {
1172 assert(rootODFrame != null);
1173 assert(masterMixID != null);
1174 absGraph = rootODFrame.getAbsoluteTrackLayoutDeep(masterMixID);
1175 }
1176 }
1177
1178 }
1179
1180 /**
1181 * A callback interface.
1182 *
1183 * @author Brook Novak
1184 */
1185 public interface MultitrackLoadListener {
1186
1187 /**
1188 * Due to explicit cancel. New playback group request.
1189 * Graph Model change while loading (after fetch).
1190 * Load/play operation aborted.
1191 */
1192 public static final int LOAD_CANCELLED = 1;
1193
1194 /**
1195 * Failed due to IO issue. State = localfilename of failed track
1196 * Load/play operation <b.not</b> aborted - it will play what it can, if
1197 * thereends up being nothing to play then evenetually a {@link #NOTHING_TO_PLAY}
1198 * event will be raised.
1199 */
1200 public static final int TRACK_LOAD_FAILED_IO = 2;
1201
1202 /**
1203 * Failed due to graph containing loop. State = loop exception
1204 * Load/play operation aborted
1205 */
1206 public static final int LOAD_FAILED_BAD_GRAPH = 6;
1207
1208 /**
1209 * Failed due to playback issue. State = exception
1210 * Load/play operation aborted
1211 */
1212 public static final int LOAD_FAILED_PLAYBACK = 3;
1213
1214 /**
1215 * All overdubs are loaded into memory and are about to play.
1216 */
1217 public static final int LOAD_COMPLETE = 4;
1218
1219 /**
1220 * Load/play operation aborted because there is nothing to play.
1221 */
1222 public static final int NOTHING_TO_PLAY = 7;
1223
1224 /**
1225 * Failed - generic case. State = exception
1226 * Load/play operation aborted
1227 */
1228 public static final int LOAD_FAILED_GENERIC = 8;
1229
1230 /**
1231 * A track has been loaded. State = localfilename
1232 */
1233 public static final int TRACK_LOADED = 9;
1234
1235 /**
1236 * A callback method that <i>is invoked from the swing thread</i>
1237 *
1238 * @param id
1239 * A code that describes the event being raised.
1240 * For example {@link #TRACK_LOADED}
1241 *
1242 * @param state
1243 * Any state information passed. See id documentations for specific info.
1244 */
1245 public void multiplaybackLoadStatusUpdate(int id, Object state);
1246
1247 }
1248
1249 public OverdubbedFrame getCurrentODFrame() {
1250 return currentODFrame;
1251 }
1252
1253 public String getCurrentMasterChannelID() {
1254 return masterChannelID;
1255 }
1256
1257
1258
1259
1260}
Note: See TracBrowser for help on using the repository browser.