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

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

Small fixes

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