source: trunk/src/org/apollo/audio/util/MultiTrackPlaybackController.java@ 1434

Last change on this file since 1434 was 1434, checked in by bln4, 5 years ago

Implementation of ProfileManager. Refactor + additional content for how new profiles are created. The refactoring split out the creation of the default profile from user profiles. Refactoring revealed a long term bug that was causing user profiles to generate with incorrect information. The additional content fixed this bug by introducing the ${USER.NAME} variable, so that the default profile frameset can specify resource locations located in the users resource directory.

org.expeditee.auth.AuthenticatorBrowser
org.expeditee.auth.account.Create
org.expeditee.gui.Browser
org.expeditee.gui.management.ProfileManager
org.expeditee.setting.DirectoryListSetting
org.expeditee.setting.ListSetting
org.expeditee.settings.UserSettings

Implementation of ResourceManager as a core location to get resources from the file system. Also the additional variable ${CURRENT_FRAMESET} to represent the current frameset, so that images can be stored in the directory of the current frameset. This increases portability of framesets.

org.expeditee.gui.FrameIO
org.expeditee.gui.management.ResourceManager
org.expeditee.gui.management.ResourceUtil
Audio:

#NB: Audio used to only operate on a single directory. This has been updated to work in a same way as images. That is: when you ask for a specific resouce, it looks to the user settings to find a sequence of directories to look at in order until it manages to find the desired resource.


There is still need however for a single(ish) source of truth for the .banks and .mastermix file. Therefore these files are now always located in resource-<username>\audio.
org.apollo.agents.MelodySearch
org.apollo.audio.structure.AudioStructureModel
org.apollo.audio.util.MultiTrackPlaybackController
org.apollo.audio.util.SoundDesk
org.apollo.gui.FrameLayoutDaemon
org.apollo.io.AudioPathManager
org.apollo.util.AudioPurger
org.apollo.widgets.FramePlayer
org.apollo.widgets.SampledTrack

Images:

org.expeditee.items.ItemUtils

Frames:

org.expeditee.gui.FrameIO

Fixed a error in the FramePlayer class caused by an incorrect use of toArray().

org.apollo.widgets.FramePlayer


Added several short cut keys to allow for the Play/Pause (Ctrl + P), mute (Ctrl + M) and volume up/down (Ctrl + +/-) when hovering over SampledTrack widgets.

org.apollo.widgets.SampledTrack


Changed the way that Authenticate.login parses the new users profile to be more consistance with other similar places in code.

org.expeditee.auth.account.Authenticate


Encapsulated _body, _surrogateItemsBody and _primaryItemsBody in Frame class. Also changed getBody function to take a boolean flag as to if it should respect the current surrogate mode. If it should then it makes sure that labels have not changed since last time getBody was called.

org.expeditee.gui.Frame

File size: 36.1 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;
11
12import org.apollo.audio.ApolloPlaybackMixer;
13import org.apollo.audio.ApolloSubjectChangedEvent;
14import org.apollo.audio.SampledAudioManager;
15import org.apollo.audio.SampledTrackModel;
16import org.apollo.audio.TrackSequence;
17import org.apollo.audio.structure.AbsoluteTrackNode;
18import org.apollo.audio.structure.AudioStructureModel;
19import org.apollo.audio.structure.OverdubbedFrame;
20import org.apollo.audio.structure.TrackGraphLoopException;
21import org.apollo.audio.structure.TrackGraphNode;
22import org.apollo.io.AudioIO.AudioFileLoader;
23import org.apollo.mvc.AbstractSubject;
24import org.apollo.mvc.Observer;
25import org.apollo.mvc.Subject;
26import org.apollo.mvc.SubjectChangedEvent;
27import org.apollo.util.AudioMath;
28import org.apollo.util.StringEx;
29import org.apollo.util.TrackModelHandler;
30import org.apollo.util.TrackModelLoadManager;
31import org.expeditee.core.BlockingRunnable;
32import org.expeditee.gio.EcosystemManager;
33import org.expeditee.gui.DisplayController;
34import org.expeditee.gui.DisplayObserver;
35import org.expeditee.gui.management.ResourceManager;
36
37public class MultiTrackPlaybackController
38 extends AbstractSubject
39 implements TrackModelHandler, Observer, DisplayObserver {
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 DisplayController.addDisplayObserver(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() || DisplayController.getCurrentFrame() == null) return; // already expired - or is playing - so don't expire!
298
299 String currentFrameName = DisplayController.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 EcosystemManager.getMiscManager().runOnGIOThread(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 EcosystemManager.getMiscManager().runOnGIOThread(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 String filepath = ResourceManager.getAudioResource(absNode.getTrackNode().getLocalFilename(), DisplayController.getCurrentFrame()).getAbsolutePath();
1007 stm = TrackModelLoadManager.getInstance().load(
1008 filepath,
1009 absNode.getTrackNode().getLocalFilename(),
1010 new Observer() { // opps, my confusing design pattern gone wrong!
1011
1012 public Subject getObservedSubject() {
1013 return null;
1014 }
1015
1016 /**
1017 * Cancel the load operation if a cancel request is opening
1018 */
1019 public void modelChanged(Subject source, SubjectChangedEvent event) {
1020 assert(event.getID() == ApolloSubjectChangedEvent.LOAD_STATUS_REPORT);
1021 if (cancelRequested) {
1022 ((AudioFileLoader)source).cancelLoad();
1023 }
1024 }
1025
1026 public void setObservedSubject(Subject parent) {
1027 }
1028
1029 }, // No need to observe
1030 true); // Search around in expeditee memory for the track
1031
1032 } catch (Exception e) {
1033 e.printStackTrace();
1034 notifyListeners(loadListeners,
1035 MultitrackLoadListener.TRACK_LOAD_FAILED_IO,
1036 absNode.getTrackNode().getLocalFilename(),
1037 true);
1038 continue;
1039 }
1040
1041 // stm must only be null if a cancel was actually requested.
1042 assert(stm != null || (stm == null && cancelRequested));
1043
1044 if (stm == null || cancelRequested) {
1045 notifyListeners(loadListeners,
1046 MultitrackLoadListener.LOAD_CANCELLED,
1047 null, true);
1048 return;
1049 }
1050
1051 }
1052
1053 synchronized(currentOverdubs) {
1054 // Important to chck while locking currentOverdubs since this thread could be
1055 // cancelled and another one of these threads loading and wanting to also change the
1056 // current overdubs.
1057 if (cancelRequested) {
1058 notifyListeners(loadListeners,
1059 MultitrackLoadListener.LOAD_CANCELLED,
1060 null, true);
1061 return;
1062 }
1063 currentOverdubs.add(new Overdub(stm, absNode.getChannelID(), AudioMath.millisecondsToFrames(absNode.getABSStartTime(),
1064 SampledAudioManager.getInstance().getDefaultPlaybackFormat())));
1065 }
1066
1067 // Notify of loaded track
1068 notifyListeners(loadListeners,
1069 MultitrackLoadListener.TRACK_LOADED,
1070 absNode.getTrackNode().getLocalFilename(),
1071 true);
1072
1073 } // load next track
1074
1075 // It is possible that all of the loads failed that now left with
1076 // nothing to play... so do a check before continuing
1077 boolean isEmpty = false;
1078 synchronized(currentOverdubs) {
1079 isEmpty = currentOverdubs.isEmpty();
1080 }
1081
1082 if (isEmpty) {
1083 notifyListeners(loadListeners, MultitrackLoadListener.NOTHING_TO_PLAY, null, true);
1084 return;
1085 }
1086
1087 hasSucceeded = true;
1088
1089 // Reset the cache now that playing (safety precaution)
1090 cacheExpiryCounter = CACHE_DEPTH;
1091
1092 } finally {
1093 // Set flag for load currect state
1094 if (!hasSucceeded) loadFinished = true;
1095 }
1096
1097 // Must commence on the swing thread
1098 EcosystemManager.getMiscManager().runOnGIOThread(new MultiTrackPlaybackCommencer());
1099 }
1100
1101 /**
1102 * Commences playback from the swing thread.
1103 * @author Brook Novak
1104 */
1105 private class MultiTrackPlaybackCommencer implements Runnable {
1106
1107
1108 public void run() {
1109
1110 try {
1111
1112 // Note: loadListeners etc.. won't change since this is inner class created from
1113 // parent...
1114 assert(loadListeners != null);
1115 assert(!loadListeners.isEmpty());
1116
1117 // This is called from the loader thread once set to play.
1118 // However during the time when the load thread scheduled this on the AWT Queue
1119 // and this actually running another AWT Event might have either directly started playback
1120 // OR began loading a new set of tracks... either all must discard this redundant request
1121 if (isPlaying() || MultiTrackPlaybackLoader.this != loaderThread || cancelRequested) {
1122 // Notice the reference compare with loaderThread: since there could be another
1123 // thread now starting... which implies that this should actually cancel!
1124
1125 notifyListeners(loadListeners,
1126 MultitrackLoadListener.LOAD_CANCELLED,
1127 null, false);
1128
1129 return; // discard redundant request
1130 }
1131
1132 } finally {
1133 // Set load state for c
1134 loadFinished = true;
1135 }
1136
1137
1138 // Notify listener that load phase has completed
1139 notifyListeners(loadListeners, MultitrackLoadListener.LOAD_COMPLETE, null, false);
1140
1141 // Remmember root frame of new playback
1142 currentODFrame = rootODFrame;
1143
1144 commencePlayback(startFrame, endFrame, loadListeners);
1145
1146
1147
1148 }
1149
1150 }
1151
1152 /**
1153 * Retreives the overdub initiation times / channel ids from the swing thread.
1154 *
1155 * @author Brook Novak
1156 */
1157 private class ABSTrackGraphRetreiver extends BlockingRunnable {
1158
1159 private String masterMixID;
1160 private OverdubbedFrame rootODFrame;
1161 private List<AbsoluteTrackNode> absGraph = null;
1162
1163 ABSTrackGraphRetreiver(OverdubbedFrame rootODFrame, String masterMixID) {
1164 this.rootODFrame = rootODFrame;
1165 this.masterMixID = masterMixID;
1166 }
1167
1168 public void execute() {
1169 assert(rootODFrame != null);
1170 assert(masterMixID != null);
1171 absGraph = rootODFrame.getAbsoluteTrackLayoutDeep(masterMixID);
1172 }
1173 }
1174
1175 }
1176
1177 /**
1178 * A callback interface.
1179 *
1180 * @author Brook Novak
1181 */
1182 public interface MultitrackLoadListener {
1183
1184 /**
1185 * Due to explicit cancel. New playback group request.
1186 * Graph Model change while loading (after fetch).
1187 * Load/play operation aborted.
1188 */
1189 public static final int LOAD_CANCELLED = 1;
1190
1191 /**
1192 * Failed due to IO issue. State = localfilename of failed track
1193 * Load/play operation <b.not</b> aborted - it will play what it can, if
1194 * thereends up being nothing to play then evenetually a {@link #NOTHING_TO_PLAY}
1195 * event will be raised.
1196 */
1197 public static final int TRACK_LOAD_FAILED_IO = 2;
1198
1199 /**
1200 * Failed due to graph containing loop. State = loop exception
1201 * Load/play operation aborted
1202 */
1203 public static final int LOAD_FAILED_BAD_GRAPH = 6;
1204
1205 /**
1206 * Failed due to playback issue. State = exception
1207 * Load/play operation aborted
1208 */
1209 public static final int LOAD_FAILED_PLAYBACK = 3;
1210
1211 /**
1212 * All overdubs are loaded into memory and are about to play.
1213 */
1214 public static final int LOAD_COMPLETE = 4;
1215
1216 /**
1217 * Load/play operation aborted because there is nothing to play.
1218 */
1219 public static final int NOTHING_TO_PLAY = 7;
1220
1221 /**
1222 * Failed - generic case. State = exception
1223 * Load/play operation aborted
1224 */
1225 public static final int LOAD_FAILED_GENERIC = 8;
1226
1227 /**
1228 * A track has been loaded. State = localfilename
1229 */
1230 public static final int TRACK_LOADED = 9;
1231
1232 /**
1233 * A callback method that <i>is invoked from the swing thread</i>
1234 *
1235 * @param id
1236 * A code that describes the event being raised.
1237 * For example {@link #TRACK_LOADED}
1238 *
1239 * @param state
1240 * Any state information passed. See id documentations for specific info.
1241 */
1242 public void multiplaybackLoadStatusUpdate(int id, Object state);
1243
1244 }
1245
1246 public OverdubbedFrame getCurrentODFrame() {
1247 return currentODFrame;
1248 }
1249
1250 public String getCurrentMasterChannelID() {
1251 return masterChannelID;
1252 }
1253
1254
1255
1256
1257}
Note: See TracBrowser for help on using the repository browser.