source: trunk/src_apollo/org/apollo/audio/util/SoundDesk.java@ 318

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

Refactored a class name and extended recorder widgets to have a perminant lifetime option (for optimum idea capturing!)

File size: 41.0 KB
Line 
1package org.apollo.audio.util;
2
3import java.util.ArrayList;
4import java.util.HashMap;
5import java.util.HashSet;
6import java.util.LinkedList;
7import java.util.List;
8import java.util.Set;
9import java.util.Stack;
10
11import javax.sound.sampled.LineUnavailableException;
12
13import org.apollo.audio.ApolloPlaybackMixer;
14import org.apollo.audio.ApolloSubjectChangedEvent;
15import org.apollo.audio.SampledTrackModel;
16import org.apollo.audio.TrackSequence;
17import org.apollo.audio.structure.LinkedTracksGraphNode;
18import org.apollo.audio.structure.OverdubbedFrame;
19import org.apollo.audio.structure.TrackGraphNode;
20import org.apollo.io.AudioPathManager;
21import org.apollo.io.MixIO;
22import org.apollo.mvc.AbstractSubject;
23import org.apollo.mvc.Observer;
24import org.apollo.mvc.Subject;
25import org.apollo.mvc.SubjectChangedEvent;
26import org.apollo.widgets.LinkedTrack;
27import org.apollo.widgets.SampledTrack;
28import org.expeditee.gui.Frame;
29
30/**
31 * A high level tool using the Apollo API providing the following features:
32 *
33 * <ul>
34 * <li> Playback / resume of track models
35 * <li> Playback / resume of linked tracks
36 * <li> Pause / Resume control
37 * <li> Solo tracks
38 * <li> Central mix repository
39 * <li> Saving and Loading of mixes
40 * <li> Saving and loading of master mix
41 * </ul>
42 *
43 * Can think of this as the bridge between the Apollo API and Expeditee widgets.
44 *
45 * By Apollo API I mean the {@link org.apollo.audio} package.
46 *
47 * @author Brook Novak
48 *
49 */
50public class SoundDesk extends AbstractSubject implements Observer {
51
52 /** Channel ID (All types) -> Channel. */
53 private HashMap<String, Channel> channels = new HashMap<String, Channel>();
54
55 /** TrackSequence indexed Channel Set. */
56 private HashMap<TrackSequence, Channel> tseqIndexedChannels = new HashMap<TrackSequence, Channel>();
57
58 /** Channel ID (Pure local) -> List of channels indirectly using the channel with channel-ID. */
59 private HashMap<String, LinkedList<Channel>> linkedChannels = new HashMap<String, LinkedList<Channel>>();
60
61 /** Channel ID (Master) -> List of channels using the master mix. */
62 private HashMap<String, LinkedList<Channel>> childChannels = new HashMap<String, LinkedList<Channel>>();
63
64 /** A whole branch of mixes can be soloed (even if not created yet) thif the prefix matches
65 * the solo prefix
66 **/
67 private String soloIDPrefix = null;
68
69 /** Where the mixes are saved to. */
70 private static final String MIX_BANKS_FILEPATH = AudioPathManager.AUDIO_HOME_DIRECTORY + ".banks";
71
72 private static final String MASTER_MIX_FILEPATH = AudioPathManager.AUDIO_HOME_DIRECTORY + ".mastermix";
73
74 private static final String NO_SOLO_PREFIX = "~#SOLOOFF#~";
75
76 private static final char CHANNEL_ID_SEPORATOR = 2357;
77 private static final char LOCAL_CHANNEL_ID_TAG = 3421;
78
79 /**
80 * Singleton design pattern.
81 */
82 private static SoundDesk instance = new SoundDesk();
83 public static SoundDesk getInstance() {
84 return instance;
85 }
86
87 /**
88 * Singleton constructor - loads the mixes from file.
89 */
90 private SoundDesk() {
91 loadMasterMix();
92 loadMixes();
93 }
94
95 private void loadMasterMix() {
96
97 TrackMixSubject mmix = MixIO.loadMasterMix(MASTER_MIX_FILEPATH);
98
99 if (mmix != null) {
100
101 ApolloPlaybackMixer.getInstance().setMasterMute(mmix.isMuted());
102
103 ApolloPlaybackMixer.getInstance().setMasterVolume(mmix.getVolume());
104
105 soloIDPrefix = mmix.getChannelID().equals(NO_SOLO_PREFIX) ?
106 null : mmix.getChannelID();
107
108 }
109
110 }
111
112 private void loadMixes() {
113
114
115 List<TrackMixSubject> banks = new LinkedList<TrackMixSubject>();
116 List<Boolean> usingLocalFlags = new LinkedList<Boolean>();
117
118 if (MixIO.loadBanks(banks, usingLocalFlags, MIX_BANKS_FILEPATH)) {
119
120 for (int i = 0; i < banks.size(); i++) {
121
122 TrackMixSubject mix = banks.get(i);
123 assert(mix != null);
124
125 Boolean flag = usingLocalFlags.get(i);
126 assert(flag != null);
127
128 createChannel(mix, flag);
129 }
130
131 }
132 }
133
134 public void saveMixes() {
135
136 ArrayList<TrackMixSubject> banks = new ArrayList<TrackMixSubject>(channels.size());
137 ArrayList<Boolean> usingLocalFlags = new ArrayList<Boolean>(channels.size());
138
139 for (Channel chan : channels.values()) {
140 banks.add(chan.mix);
141 usingLocalFlags.add(new Boolean(chan.isUsingLocalMix));
142 }
143
144 MixIO.saveBanks(MIX_BANKS_FILEPATH, banks, usingLocalFlags);
145 }
146
147 public void saveMasterMix() {
148 String id = soloIDPrefix;
149 if (id == null) id = NO_SOLO_PREFIX;
150
151 MixIO.saveMasterMix(MASTER_MIX_FILEPATH,
152 new TrackMixSubject(id,
153 ApolloPlaybackMixer.getInstance().getMasterVolume(),
154 ApolloPlaybackMixer.getInstance().isMasterMuteOn()));
155 }
156
157 /**
158 * In order to solo a track sequence, use this to solo a group of
159 * track sequences depending of their id starts with the given prefix.
160 * Not that the effect is applied immediatly and the ApolloPlaybackMixer
161 * mode changes to solo-on/off accordingly.
162 *
163 * Raises a {@link ApolloSubjectChangedEvent#SOLO_PREFIX_CHANGED} event if changed.
164 *
165 * @param channelIDPrefix
166 * The solo ID. Null for no solo.
167 */
168 public void setSoloIDPrefix(String channelIDPrefix) {
169
170 if (this.soloIDPrefix == channelIDPrefix) return;
171
172 this.soloIDPrefix = channelIDPrefix;
173
174 // Set flags
175 for (Channel ch : channels.values()) {
176 if (ch.tseq != null) {
177 if (channelIDPrefix == null) ch.tseq.setSoloFlag(false);
178 else ch.tseq.setSoloFlag(ch.mix.getChannelID().startsWith(channelIDPrefix));
179 }
180 }
181
182 // Note, any track sequences create in the future will have their solo flag set
183 // according to the current soloIDPrefix.
184
185 // Set solo state
186 ApolloPlaybackMixer.getInstance().setSoloEnable(channelIDPrefix != null);
187
188 // Notify observers
189 fireSubjectChanged(new SubjectChangedEvent(
190 ApolloSubjectChangedEvent.SOLO_PREFIX_CHANGED));
191 }
192
193 /**
194 * Determines whether a track id is part of the current solo group.
195 *
196 * @param channelID
197 * Must not be null.
198 *
199 * @return
200 * True if the given id is in the solo group.
201 *
202 * @throws NullPointerException
203 * If mixID is null.
204 */
205 public boolean isSolo(String channelID) {
206 if (channelID == null) throw new NullPointerException("channelID");
207 if (soloIDPrefix == null) return false;
208 return channelID.startsWith(soloIDPrefix);
209 }
210
211 /**
212 * Determines if a specific mix is playing.
213 *
214 * @param channelID
215 * The id to test - must not be null.
216 *
217 * @return
218 * True if a channel with the given ID is playing.
219 *
220 * @throws NullPointerException
221 * If channelID is null.
222 */
223 public boolean isPlaying(String channelID) {
224 TrackSequence ts = getTrackSequence(channelID);
225 return ts != null && ts.isPlaying();
226 }
227
228 /**
229 * Gets the last frame psoition that a given chanel was played ... that is,
230 * the last frame position <i>within the track sequences bytes</i>. I.e. not
231 * refering to the software mixers timeline.
232 *
233 * Note that this is specific to the audio bytes in the tracksequence that
234 * was last played with the mix.
235 *
236 * @param channelID
237 * Must not be null.
238 *
239 * @return
240 * The last stopped frame position for the given channel ID.
241 * negative if does not exist or has not been played.
242 *
243 * @throws NullPointerException
244 * If channelID is null
245 */
246 public int getLastPlayedFramePosition(String channelID) {
247 if (channelID == null) throw new NullPointerException("channelID");
248
249 Channel chan = channels.get(channelID);
250
251 if (chan != null) return chan.stoppedFramePosition;
252 return -1;
253 }
254
255 /**
256 * Checks if a channel has been marked as being paused.
257 *
258 * A channel can be marked as paused via {@link #setPaused(String, boolean)}.
259 * The mark is reset whenever a channel is next played (or explicitly reset).
260 *
261 * Using this flag in conjunction with {@link #getLastPlayedFramePosition(String)}
262 * pause/resume functionality can be acheived.
263 *
264 * @param channelID
265 * Must not be null.
266 *
267 * @return
268 * True if the channel exists and is marked as paused.
269 *
270 * @throws NullPointerException
271 * If channelID is null
272 */
273 public boolean isPaused(String channelID) {
274 if (channelID == null) throw new NullPointerException("channelID");
275
276 Channel chan = channels.get(channelID);
277
278 if (chan != null) return chan.isPaused;
279 return false;
280 }
281
282 /**
283 * Marks a channel as paused / not-paused.
284 *
285 * The mark of a channel auto-resets whenever the channel is next played.
286 *
287 * Raises a {@link ApolloSubjectChangedEvent#PAUSE_MARK_CHANGED} event
288 * if true is returned.
289 *
290 * @param channelID
291 * Must not be null.
292 *
293 * @param isPaused
294 * True to mark as paused. False to reset mark.
295 *
296 * @return
297 * True if the mark was set. False if it wasn't set because the
298 * channel did not exist or was already the same.
299 *
300 * @throws NullPointerException
301 * If channelID is null
302 */
303 public boolean setPaused(String channelID, boolean isPaused) {
304 if (channelID == null) throw new NullPointerException("channelID");
305
306 Channel chan = channels.get(channelID);
307
308 if (chan != null && chan.isPaused != isPaused) {
309
310 chan.isPaused = isPaused;
311
312 fireSubjectChanged(new SubjectChangedEvent(
313 ApolloSubjectChangedEvent.PAUSE_MARK_CHANGED, channelID));
314
315 return true;
316 }
317
318 return false;
319 }
320
321 /**
322 * Gets a track sequence assoicated with the track id.
323 *
324 * @param channelID
325 * Must not be null.
326 *
327 * @return
328 * The track sequence assoicated with the channelID. Null if none exists.
329 *
330 * @throws NullPointerException
331 * If channelID is null.
332 */
333 public TrackSequence getTrackSequence(String channelID) {
334 if (channelID == null) throw new NullPointerException("channelID");
335 Channel chan = channels.get(channelID);
336 return (chan == null) ? null : chan.tseq;
337 }
338
339 /**
340 *
341 * @param channelID
342 * A non-null indirect cahnnel id.
343 *
344 * @param isUsingLocalMix
345 * The flag to set. True to use the local mix settings instead of thwe indrect local
346 * mix... otherwise set to false to override the lcal mix settings and use its own.
347 *
348 * @return
349 * True if set. False if cahnnel does not exist.
350 *
351 * @throws IllegalArgumentException
352 * If the channel exists and the type is not an indirect local channel.
353 *
354 * @throws NullPointerException
355 * If channelID is null.
356 */
357 public boolean setUseLocalTrackMix(String channelID, boolean isUsingLocalMix) {
358 if (channelID == null) throw new NullPointerException("channelID");
359 Channel chan = channels.get(channelID);
360
361
362 if (chan != null) {
363
364 if (chan.type != ChannelIDType.IndirectLocal)
365 throw new IllegalArgumentException("The channel ID \"" + channelID + "\" is no an indirect local ID");
366
367 chan.isUsingLocalMix = isUsingLocalMix;
368 updateMixSettings(chan);
369 return true;
370 }
371
372 return false;
373 }
374
375 /**
376 * Creates a empty mix: adding it to the mix set as well as observing it.
377 *
378 * Assumes that the id has not been created yet.
379 *
380 * @param channelID
381 * The id to create the mix with
382 *
383 * @return
384 * The empty mix - never null.
385 *
386 * @throws IllegalArgumentException
387 * If channelID is invalid.
388 */
389 private Channel createDefaultChannel(String channelID) {
390 return createChannel(channelID, 1.0f, false, true); // defaults
391 }
392
393 /**
394 * Creates a mix. Note that this will be saved in the mix batch file.
395 *
396 * @param channelID
397 * A unique <i>unused</i> channel ID for the mix. Must not be null.
398 *
399 * @param volume
400 * The volume for the new mix. Between 0 and 1. Clamped.
401 *
402 * @param isMuted
403 * Whether the mute of the mix is on or off.
404 *
405 * @param isUsingLocalMix
406 * True if the channel using the mix should instead use the local mix .. this only
407 * applies to {@link ChannelIDType#IndirectLocal} channels.
408 *
409 * @return
410 * The created mix. Null if already exists.
411 *
412 * @throws IllegalArgumentException
413 * If channelID is invalid.
414 */
415 public TrackMixSubject createMix(String channelID, float volume, boolean isMuted, boolean isUsingLocalMix) {
416 if (channels.containsKey(channelID)) return null;
417 return createChannel(channelID, volume, isMuted, isUsingLocalMix).mix;
418 }
419
420 /**
421 *
422 * @param channelID
423 *
424 * @param volume
425 *
426 * @param isMuted
427 *
428 * @param isUsingLocalMix
429 *
430 * @return
431 *
432 * @throws IllegalArgumentException
433 * If channelID is invalid.
434 */
435 private Channel createChannel(String channelID, float volume, boolean isMuted, boolean isUsingLocalMix) {
436 assert(!channels.containsKey(channelID));
437 return createChannel(new TrackMixSubject(channelID, volume, isMuted), isUsingLocalMix);
438 }
439
440 /**
441 * @see #createChannel(String, float, boolean)
442 *
443 * @param mix
444 * The mix to create.
445 *
446 * @return
447 * NULL if the mix already exists
448 *
449 * @throws IllegalArgumentException
450 * If the mix's channel ID is invalid.
451 */
452 private Channel createChannel(TrackMixSubject mix, boolean isUsingLocalMix) {
453 assert(mix != null);
454 if (channels.containsKey(mix.getChannelID())) return null;
455
456 Channel chan = new Channel(mix, isUsingLocalMix); // throws IllegalArgumentException
457
458 channels.put(mix.getChannelID(), chan);
459 chan.mix.addObserver(this); // Keep track sequence data synched with mix
460
461 // If the channel is an indirect local channel then update the linked track map
462 if (chan.type == ChannelIDType.IndirectLocal) {
463
464 // Get the local channel ID that this channel points to
465 String local = extractLocalChannelID(mix.getChannelID());
466 assert(local != null);
467
468 LinkedList<Channel> linked = linkedChannels.get(local);
469
470 // Add the channel to the linked list ....
471 if (linked == null) {
472 linked = new LinkedList<Channel>();
473 linkedChannels.put(local, linked);
474 }
475
476 if (!linked.contains(chan)) {
477 linked.add(chan);
478 }
479
480 String parent = extractMasterChannelID(mix.getChannelID());
481 assert(parent != null);
482
483 LinkedList<Channel> children = childChannels.get(parent);
484 if (children != null && !children.contains(chan))
485 children.add(chan);
486
487 } else if (chan.type == ChannelIDType.Master) {
488
489 LinkedList<Channel> children = new LinkedList<Channel>();
490
491 for (Channel c : channels.values()) {
492 if (c == chan) continue;
493 if (c.masterMixID.equals(chan.mix.getChannelID())) {
494 assert(c.type == ChannelIDType.IndirectLocal);
495 children.add(c);
496 }
497 }
498
499 childChannels.put(chan.mix.getChannelID(), children);
500
501 }
502
503
504 return chan;
505 }
506
507 /**
508 * Either gets an existing from memory or creates a new mix for the given ID.
509 * If had to create, default values of the mix are chosen (non-muted and full volume)
510 *
511 * @param channelID
512 * The ID of the mix to get / create. Must not be null.
513 *
514 * @return
515 * The retreived/constructed mix. Never null.
516 *
517 * @throws NullPointerException
518 * If id is null.
519 *
520 * @throws IllegalArgumentException
521 * If channelID is invalid.
522 */
523 public TrackMixSubject getOrCreateMix(String channelID) {
524
525 if (channelID == null) throw new NullPointerException("channelID");
526
527 TrackMixSubject tmix = getMix(channelID);
528
529 if (tmix == null) {
530 Channel chan = createDefaultChannel(channelID); // throws illegal arg
531 return chan.mix;
532
533 } else return tmix;
534 }
535
536 /**
537 * Retreives a mix.
538 *
539 * @param channelID
540 * The ID of the mix to get. Can be null.
541 *
542 * @return
543 * The Mix that is assoicated with the given ID.
544 * Null if no mix exists OR if id was null.
545 *
546 */
547 public TrackMixSubject getMix(String channelID) {
548 if (channelID == null) return null;
549
550 Channel chan = channels.get(channelID);
551 if (chan != null) return chan.mix;
552 return null;
553 }
554
555 /**
556 * Raises {@link ApolloSubjectChangedEvent#TRACK_SEQUENCE_CREATED} events
557 * for each overdub
558 *
559 * @param overdubs
560 * A non-null list of overdubs. Must not have overdubbs sharing
561 * channels...
562 *
563 * @return
564 * The set of track sequences that were created for playing the overdubs.
565 * Empty if there were none to play if the given overdub list is empty or
566 * if none of them are in playing range.
567 *
568 * @throws LineUnavailableException
569 * If failed to get data line to output device.
570 *
571 * @throws NullPointerException
572 * if overdubs is null.
573 *
574 * @throws IllegalArgumentException
575 * if overdubs contains a null reference.
576 * If overdubs channelID is invalid or a master channel
577 * If contains two or more overdubs using the same channel.
578 * If overdubs startFrame larger or equal to endFrame.
579 * Or overdubs startFrame is not in audio range.
580 * Or overdubs end frame is not in audio range
581 *
582 * @throws IllegalStateException
583 * If a channel with the overdubs channelID is already playing.
584 */
585 public Set<TrackSequence> playOverdubs(
586 List<Overdub> overdubs)
587 throws LineUnavailableException {
588
589 if (overdubs == null) throw new NullPointerException("overdubs");
590
591 Set<Channel> toPlay = new HashSet<Channel>(); // used for checking for dup channels
592 Set<TrackSequence> tracSeqs = new HashSet<TrackSequence>();
593
594 // Anything to play?
595 if (overdubs.isEmpty())
596 return tracSeqs;
597
598 boolean succeeded = false;
599
600 try {
601
602 // Get or create channels from the given IDs.
603 for (Overdub od : overdubs) {
604
605 if (od == null)
606 throw new IllegalArgumentException("overdubs contains a null reference");
607
608 Channel chan = channels.get(od.getChannelID());
609
610
611 if (chan == null) { // Create
612 chan = createDefaultChannel(od.getChannelID()); // Throws IllegalArgumentException
613
614 } else if (toPlay.contains(chan)) { // dissallow re-use of channels within a playback group...
615 throw new IllegalArgumentException("Duplicate use of channel in multiplayback request");
616 }
617
618
619 // TODO: Remove
620 System.out.println("TODO: Remove.. when unlock full mixing capabilities");
621 chan.isUsingLocalMix = true;
622
623
624
625
626 // Check that the channel ID is not a master mix:
627 if (chan.type == ChannelIDType.Master)
628 throw new IllegalArgumentException(
629 "Master channels do not directly playback tracks (channelID = "
630 + od.getChannelID() + ")");
631
632 // Check that the target channel is not already playing
633 if (chan.tseq != null && chan.tseq.isPlaying())
634 throw new IllegalStateException("The channel " + od.getChannelID() + " is already in use");
635
636 // Create the (new) track sequence to associate the mix with
637 TrackSequence ts = new TrackSequence( // throws exceptions
638 od.getTrackModel().getAllAudioBytes(),
639 od.getTrackModel().getFormat(),
640 od.getStartFrame(),
641 od.getEndFrame(),
642 od.getRelativeInitiationFrame(),
643 od.getTrackModel()); // Important: set as track model so it can be re-used
644
645 ts.addObserver(this);
646
647 // Reset pause flag
648 chan.isPaused = false;
649 chan.tseq = ts;
650
651 tseqIndexedChannels.put(ts, chan);
652 tracSeqs.add(ts);
653
654 // Synch TS with mix
655 ts.setSoloFlag(isSolo(od.getChannelID()));
656 updateMixSettings(chan);
657
658 // Allow listeners to register to the track sequence so that they can
659 // catch playback events for the mix
660 fireSubjectChanged(new SubjectChangedEvent(
661 ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED, od.getChannelID()));
662
663 }
664
665
666 // if (!tracSeqs.isEmpty()) { // if nothing is in range can become empty
667 // Commence playback... when its scheduled to
668 ApolloPlaybackMixer.getInstance().playSynchronized(tracSeqs);
669 //}
670
671 succeeded = true;
672
673 } finally {
674
675 // Did fail in any way?
676 if (!succeeded) {
677
678 // Ensure that track sequences are all removed since some may not be able
679 // to be erased by observation... (modelchanged)
680 for (Channel chan : toPlay) {
681 if (chan.tseq != null &&
682 !chan.tseq.isPlaying()) { // if is created but has not started
683 tseqIndexedChannels.remove(chan.tseq);
684 chan.tseq = null;
685 }
686 }
687
688 }
689 }
690
691 return tracSeqs;
692 }
693
694 /**
695 * Raises an {@link ApolloSubjectChangedEvent#TRACK_SEQUENCE_CREATED} event
696 * just before playback commences. Note that the event can occur but the track sequence
697 * may fail to playback.
698 *
699 * @param tmodel
700 * The track model to play. Must not be null.
701 *
702 * @param channelID
703 * The channel ID to use for the playback. Note that a channel can only be
704 * in use one at a time
705 *
706 * @param startFrame
707 * The frame <i>within the track</i> where to start playback. Inclusive.
708 *
709 * @param endFrame
710 * The frame <i>within the track</i> where to end playback. Inclusive.
711 *
712 * @param relativeInitiationFrame
713 * Where to initiate the frame. This is relative to where the track should start playing
714 * in frames, when queued in the track graph.
715 *
716 * @throws LineUnavailableException
717 * If failed to get data line to output device
718 *
719 * @throws NullPointerException
720 * if channelID or tmodel is null.
721 *
722 * @throws IllegalArgumentException
723 * If channelID is empty, or invalid, or a master channel
724 * If startFrame larger or equal to endFrame.
725 * Or startFrame is not in audio range.
726 * Or end frame is not in audio range
727 *
728 * @throws IllegalStateException
729 * If the channel with the given channelID is already playing.
730 *
731 */
732 public void playSampledTrackModel(
733 SampledTrackModel tmodel,
734 String channelID,
735 int startFrame,
736 int endFrame,
737 int relativeInitiationFrame)
738 throws LineUnavailableException {
739
740 if (tmodel == null) throw new NullPointerException("tmodel");
741 if (channelID == null) throw new NullPointerException("channelID");
742 if (channelID.length() == 0) throw new IllegalArgumentException("channelID");
743
744 // Get or create a channel from the given ID.
745 Channel chan = channels.get(channelID);
746
747 if (chan == null) { // Create
748 chan = createDefaultChannel(channelID); // Throws IllegalArgumentException
749 }
750
751
752 System.out.println("TODO: Remove.. when unlock full mixing capabilities");
753 chan.isUsingLocalMix = true; // TODO: Remove
754
755
756 // Check that the channel ID is not a master mix:
757 if (chan.type == ChannelIDType.Master)
758 throw new IllegalArgumentException(
759 "Master channels do not directly playback tracks (channelID = "
760 + channelID + ")");
761
762 // Check that the mix is not already playing
763 if (chan.tseq != null && chan.tseq.isPlaying())
764 throw new IllegalStateException("The channel " + channelID + " is already in use");
765
766 // Create the (new) track sequence to associate the mix with
767 TrackSequence ts = new TrackSequence(
768 tmodel.getAllAudioBytes(),
769 tmodel.getFormat(),
770 startFrame,
771 endFrame,
772 relativeInitiationFrame,
773 tmodel); // Important: set as track model so it can be re-used
774
775 boolean succeeded = false;
776
777 try {
778
779 // Whenever the track is stopped, then auto-remove the track sequence and nullify
780 // the ts reference so that the referenced SampledTrackModel can be collected
781 // the the garbage collector
782 ts.addObserver(this);
783
784
785 // Reset pause flag
786 chan.isPaused = false;
787 chan.tseq = ts;
788
789 tseqIndexedChannels.put(ts, chan);
790
791 // Synch TS with mix
792 ts.setSoloFlag(isSolo(channelID));
793 updateMixSettings(chan);
794
795 // Allow listeners to register to the track sequence so that they can
796 // catch playback events for the mix
797 fireSubjectChanged(new SubjectChangedEvent(
798 ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED, channelID));
799
800
801 // Commence playback... when its scheduled to
802 ApolloPlaybackMixer.getInstance().play(ts);
803 succeeded = true;
804
805 } finally {
806 if (!succeeded) {
807 chan.tseq = null;
808 tseqIndexedChannels.remove(ts);
809 }
810 }
811
812 }
813
814 /**
815 * Ensures that a channels mix settings are synched as well as all other
816 * channels that depend on it... (e.g. a pure local channel updating its indirect tracks).
817 *
818 * Omittedly, this is way to over the top than it needs to be :(
819 *
820 * @param chan
821 * Must not be null.
822 */
823 private void updateMixSettings(Channel chan) {
824 assert(chan != null);
825
826 // Update track sequence playing on the channel at the moment ..
827 // Only if this track mix is to be used for the track sequence
828 if (chan.tseq != null &&
829 chan.type != ChannelIDType.Master) {
830
831 boolean isMuted;
832 float volume;
833 if (chan.type == ChannelIDType.IndirectLocal && chan.isUsingLocalMix) {
834
835 String localChannelID = extractLocalChannelID(chan.mix.getChannelID());
836 assert(localChannelID != null); // since type is not master
837
838 Channel baseChan = channels.get(localChannelID);
839 if (baseChan != null) { // it is possible for local channels to be missing
840 assert(baseChan.type == ChannelIDType.PureLocal);
841 isMuted = baseChan.mix.isMuted();
842 volume = baseChan.mix.getVolume();
843 } else {
844 isMuted = chan.mix.isMuted();
845 volume = chan.mix.getVolume();
846 }
847
848 } else {
849 // pre-mix with master mix - if has one
850 isMuted = chan.mix.isMuted();
851 volume = chan.mix.getVolume();
852 }
853
854
855 if (chan.type == ChannelIDType.IndirectLocal) {
856 Channel master = channels.get(chan.masterMixID);
857
858 if (master != null) {
859 assert(master.type == ChannelIDType.Master);
860 isMuted |= master.mix.isMuted();
861 volume *= master.mix.getVolume();
862 }
863 }
864
865 chan.tseq.setMuted(isMuted);
866 chan.tseq.setVolume(volume);
867 }
868
869 // Update any channels that are linking to this (local) channel's mix at the momment
870 if (chan.type == ChannelIDType.PureLocal) {
871
872 List<Channel> lchans = linkedChannels.get(chan.mix.getChannelID());
873
874 if (lchans != null) {
875 for (Channel lchan : lchans) {
876 if (lchan.isUsingLocalMix && lchan.tseq != null) {
877
878 // pre-mix with master mix - if has one
879 boolean isMuted = chan.mix.isMuted();
880 float volume = chan.mix.getVolume();
881
882 Channel master = channels.get(lchan.masterMixID);
883
884 if (master != null) {
885 assert(master.type == ChannelIDType.Master);
886 isMuted |= master.mix.isMuted();
887 volume *= master.mix.getVolume();
888 }
889
890 lchan.tseq.setMuted(isMuted);
891 lchan.tseq.setVolume(volume);
892
893 }
894 }
895 }
896
897 // Update any channels that are using this master mix at the moment
898 } else if (chan.type == ChannelIDType.Master) {
899
900 List<Channel> childChans = childChannels.get(chan.mix.getChannelID());
901
902 if (childChans != null) {
903 for (Channel cchan : childChans) {
904 assert(cchan.masterMixID.equals(chan.mix.getChannelID()));
905 assert(cchan.type == ChannelIDType.IndirectLocal);
906
907 // Is playing? i.e. do need to update?
908 if (cchan.tseq == null) continue;
909
910 TrackMixSubject baseMix = null;
911
912 // If the child channel using this master mix is also using a local mix then
913 // retreive the local mix ....
914 if (cchan.isUsingLocalMix) {
915
916 String localChannelID = extractLocalChannelID(cchan.mix.getChannelID());
917 assert(localChannelID != null); // since type is not master
918
919 Channel baseChan = channels.get(localChannelID);
920 if (baseChan != null) { // it is possible for local channels to be missing
921 assert(baseChan.type == ChannelIDType.PureLocal);
922 baseMix = baseChan.mix;
923 }
924
925
926 } else {
927 baseMix = cchan.mix;
928 }
929
930 boolean isMuted = chan.mix.isMuted() ;
931 float volume = chan.mix.getVolume();
932
933 if (baseMix != null) {
934 isMuted |= baseMix.isMuted();
935 volume *= baseMix.getVolume();
936 }
937
938 cchan.tseq.setMuted(isMuted);
939 cchan.tseq.setVolume(volume);
940
941 }
942 }
943
944 }
945
946 }
947
948 public Subject getObservedSubject() {
949 return null;
950 }
951 public void setObservedSubject(Subject parent) {
952 }
953
954 /**
955 * Synchs the track sequences with the mixes.
956 * Assumes event is a ApolloSubjectChangedEvent and source
957 * is a TrackMixSubject
958 */
959 public void modelChanged(Subject source, SubjectChangedEvent event) {
960
961 if (source instanceof TrackMixSubject) {
962
963 TrackMixSubject tmix = (TrackMixSubject)source;
964
965 Channel chan = channels.get(tmix.getChannelID());
966 assert(chan != null);
967 assert(chan.mix == tmix);
968
969 updateMixSettings(chan);
970
971 } else if (source instanceof TrackSequence) {
972
973 if (event.getID() == ApolloSubjectChangedEvent.PLAYBACK_STOPPED) {
974
975 Channel chan = tseqIndexedChannels.get(source);
976 if (chan != null) {
977 freeChannel(chan, (TrackSequence)source);
978 }
979 }
980 }
981
982 }
983
984 public void freeChannels(String channelPrefix) {
985 assert (channelPrefix != null);
986 assert (channelPrefix.length() > 0);
987
988 for (Channel chan : channels.values()) {
989
990 if (chan.tseq != null && chan.mix.getChannelID().startsWith(channelPrefix)) {
991
992 freeChannel(chan, chan.tseq);
993 }
994 }
995
996 }
997
998 private void freeChannel(Channel chan, TrackSequence ts) {
999 assert (chan != null);
1000 assert (ts != null);
1001
1002 chan.stoppedFramePosition = (chan.tseq != null) ? chan.tseq.getSuspendedFrame() : -1;
1003 chan.tseq = null; // Free SampledTrackModel reference for garbage collection
1004 tseqIndexedChannels.remove(ts); // " ditto "
1005
1006 }
1007
1008 /**
1009 * Creates a <i>pure local</i> channel ID.
1010 *
1011 * @param sampledTrackWidget
1012 * The widget to create the ID from. Must not be null.
1013 *
1014 * @return
1015 * The channel ID... never null.
1016 */
1017 public static String createPureLocalChannelID(SampledTrack sampledTrackWidget) {
1018 assert(sampledTrackWidget != null);
1019 return createPureLocalChannelID(sampledTrackWidget.getLocalFileName());
1020 }
1021
1022 /**
1023 * Creates a <i>pure local</i> channel ID.
1024 *
1025 * @param tinf
1026 * The TrackGraphInfo to create the ID from. Must not be null.
1027 *
1028 * @return
1029 * The channel ID... never null.
1030 */
1031 public static String createPureLocalChannelID(TrackGraphNode tinf) {
1032 assert(tinf != null);
1033 return createPureLocalChannelID(tinf.getLocalFilename());
1034 }
1035
1036 /**
1037 * Creates a <i>pure local</i> channel ID.
1038 *
1039 * @param localFilename
1040 * The local filename to create the channel ID from
1041 *
1042 * @return
1043 * The channel ID... never null.
1044 */
1045 public static String createPureLocalChannelID(String localFilename) {
1046 assert(localFilename != null && localFilename.length() > 0);
1047 return localFilename + LOCAL_CHANNEL_ID_TAG;
1048 }
1049
1050 /**
1051 * Creates an <i>indirect</i> local channel id from a given virtual path.
1052 *
1053 * @param virtualPath
1054 * The virtual path to create the id from. Must not be null.
1055 *
1056 * @param localFilename
1057 * The local filename that the virtual path points to. Must not be null.
1058 *
1059 * @return
1060 * An indirect local channel id... Never null.
1061 */
1062 public static String createIndirectLocalChannelID(List<String> virtualPath, String localFilename) {
1063 assert(virtualPath != null);
1064 assert(localFilename != null);
1065
1066 // Build prefix
1067 StringBuilder sb = new StringBuilder();
1068
1069 for (String virtualName : virtualPath) {
1070 sb.append(virtualName);
1071 sb.append(CHANNEL_ID_SEPORATOR);
1072 }
1073
1074 sb.append(createPureLocalChannelID(localFilename));
1075
1076 return sb.toString();
1077 }
1078
1079 public static String createMasterChannelID(LinkedTrack lwidget) {
1080 return lwidget.getVirtualFilename();
1081 }
1082 /**
1083 * Creates a master channel ID from a frame.
1084 *
1085 * @param frame
1086 * The frame to create the master mix ID from.
1087 * Must not be null.
1088 *
1089 * @return
1090 * The master mix for the given frame.
1091 */
1092 public static String createMasterChannelID(Frame frame) {
1093 assert(frame != null);
1094 return frame.getName();
1095 }
1096
1097 /**
1098 * Creates a master channel ID from a OverdubbedFrame.
1099 *
1100 * @param odFrame
1101 * The frame to create the master mix ID from.
1102 * Must not be null.
1103 *
1104 * @return
1105 * The master mix for the given odFrame.
1106 */
1107 public static String createMasterChannelID(OverdubbedFrame odFrame) {
1108 assert(odFrame != null);
1109 return odFrame.getFrameName();
1110 }
1111
1112 /**
1113 * Creates a master channel ID from a LinkedTracksGraphInfo.
1114 *
1115 * @param ltracks
1116 * The linked track to create the master mix ID from.
1117 * Must not be null.
1118 *
1119 * @return
1120 * The master mix for the given ltracks.
1121 */
1122 public static String createMasterChannelID(LinkedTracksGraphNode ltracks) {
1123 assert(ltracks != null);
1124 return ltracks.getVirtualFilename();
1125 }
1126
1127
1128 /**
1129 * Recursively creates channel IDs from a OverdubbedFrame. I.e. for all its
1130 * desending tracks. Linked tracks are exluded and only used for recursing.
1131 *
1132 * <b>Exlcudes the ID for odFrame (the master channel)</b>
1133 *
1134 * @param odFrame
1135 * The frame to start recursivly building the ID's from.
1136 *
1137 * @return
1138 * The list of linked ID's (to actual tracks) from this frame...
1139 * Never null. Can be empty if there are no tracks from the
1140 * given frame.
1141 */
1142 public static List<String> createChannelIDs(OverdubbedFrame odFrame) {
1143 assert(odFrame != null);
1144
1145 Stack<String> virtualPath = new Stack<String>();
1146 virtualPath.push(createMasterChannelID(odFrame)); // Master mix
1147 List<String> ids = new LinkedList<String>();
1148 createChannelIDs(odFrame, ids, new Stack<OverdubbedFrame>(), virtualPath);
1149 assert(virtualPath.size() == 1);
1150
1151 return ids;
1152 }
1153
1154 /**
1155 * Recursively creates channel IDs from a LinkedTracksGraphInfo. I.e. for all its
1156 * desending tracks. Linked tracks are exluded and only used for recursing.
1157 *
1158 * <b>Exlcudes the ID for ltracks (the master channel)</b>
1159 *
1160 * <b>Note:</b> calling {@link #createChannelIDs(OverdubbedFrame)} with the links
1161 * OverdubbedFrame is not the equivelent of calling this... because it uses a
1162 * different master channel ID.
1163 *
1164 * @param ltracks
1165 * The link to start recursivly building the ID's from.
1166 *
1167 * @return
1168 * The list of linked ID's (to actual tracks) from this frame...
1169 * Never null. Can be empty if there are no tracks from the
1170 * given ltracks.
1171 */
1172 public static List<String> createChannelIDs(LinkedTracksGraphNode ltracks) {
1173 assert(ltracks != null);
1174
1175 Stack<String> virtualPath = new Stack<String>();
1176 virtualPath.push(createMasterChannelID(ltracks));
1177 List<String> ids = new LinkedList<String>();
1178 createChannelIDs(ltracks.getLinkedFrame(), ids, new Stack<OverdubbedFrame>(), virtualPath);
1179 assert(virtualPath.size() == 1);
1180
1181 return ids;
1182 }
1183
1184 /**
1185 * Recursively creates channel ID.
1186 * @param current
1187 * The root at which to recuse at.
1188 *
1189 * @param ids
1190 * A list of strings to populate with channel ids.
1191 *
1192 * @param visited
1193 * Non-null Empty.
1194 *
1195 * @param virtualPath
1196 * Preload with IDs if needed.
1197 */
1198 private static void createChannelIDs(
1199 OverdubbedFrame current,
1200 List<String> ids,
1201 Stack<OverdubbedFrame> visited,
1202 Stack<String> virtualPath) {
1203
1204 // Avoid recursion .. never can be to safe.
1205 if (visited.contains(current)) return;
1206 visited.push(current);
1207
1208 if (current.trackExists()) {
1209
1210 // Build prefix
1211 StringBuilder prefixBuilder = new StringBuilder();
1212
1213 for (String virtualName : virtualPath) {
1214 prefixBuilder.append(virtualName);
1215 prefixBuilder.append(CHANNEL_ID_SEPORATOR);
1216 }
1217
1218 String prefix = prefixBuilder.toString();
1219 assert(prefix.length() > 0);
1220 assert(prefix.charAt(prefix.length() - 1) == CHANNEL_ID_SEPORATOR);
1221
1222 // Create id's
1223 for (TrackGraphNode tinf : current.getUnmodifiableTracks()) {
1224 String id = prefix + createPureLocalChannelID(tinf);
1225 assert(!ids.contains(id));
1226 ids.add(id);
1227 }
1228 }
1229
1230 // Recurse
1231 for (LinkedTracksGraphNode ltinf : current.getUnmodifiableLinkedTracks()) {
1232 virtualPath.push(ltinf.getVirtualFilename()); // build vpath
1233 createChannelIDs(ltinf.getLinkedFrame(), ids, visited, virtualPath);
1234 virtualPath.pop(); // maintain vpath
1235 }
1236
1237 visited.pop();
1238
1239 }
1240
1241
1242 /**
1243 * Extracts a pure local channelID...
1244 * Thus for a given channel ID made up of virtual links its all the virtual
1245 * ids are stripped and the remaining local channel ID is returned.
1246 *
1247 * @param channelID
1248 * The channel ID to extract the local channel ID from.
1249 * Must not be null or empty.
1250 *
1251 * @return
1252 * The local channel ID. Null if channelID does not refer to a local channel ID:
1253 * in that case the channel ID would either be invalid or be a pure link channel.
1254 *
1255 */
1256 public static String extractLocalChannelID(String channelID) {
1257 assert (channelID != null);
1258 assert(channelID.length() > 0);
1259
1260 if (channelID.charAt(channelID.length() - 1) == LOCAL_CHANNEL_ID_TAG) {
1261
1262 // Starting at the end... search bacward for the start or a seporator
1263 int i = channelID.length() - 2;
1264 while (i > 0 && channelID.charAt(i) != CHANNEL_ID_SEPORATOR) i--;
1265
1266 if (channelID.charAt(i) == CHANNEL_ID_SEPORATOR) i++;
1267
1268 if (((channelID.length() - 1) - i) == 0) return null;
1269
1270 return channelID.substring(i, channelID.length()); // include the local tag
1271
1272 }
1273
1274 // No local part
1275 return null;
1276 }
1277
1278 /**
1279 * Pure local or indirect local Channel IDs are encoded with local track names,
1280 *
1281 * @param channelID
1282 * The channel ID to extract the local filename from.
1283 *
1284 * @return
1285 * The local filename from the ID. Null if the channel ID is not a
1286 * pure local / indirect local id.
1287 *
1288 */
1289 public static String extractLocalFilename(String channelID) {
1290 String localID = extractLocalChannelID(channelID);
1291 if (localID != null && localID.length() > 1)
1292 return localID.substring(0, localID.length() - 1);
1293
1294 return null;
1295 }
1296
1297 /**
1298 * Extracts the master ID from a channel ID.
1299 * If the channel id is a pure local ID then it will return channelID.
1300 *
1301 * @param channelID
1302 * The channel id to extract the master id from. Must not be null.
1303 *
1304 * @return
1305 * The master ID. Null if channelID is null or invalid in some way...
1306 */
1307 public static String extractMasterChannelID(String channelID) {
1308 assert (channelID != null);
1309
1310 // master id = top-level id... that is, a pure virtual name, framename or local name
1311
1312 int i;
1313 for (i = 0; i < channelID.length(); i++) {
1314 if (channelID.charAt(i) == CHANNEL_ID_SEPORATOR) break;
1315 }
1316
1317 if (i > 1) {
1318 return channelID.substring(0, i);
1319 }
1320
1321 return null;
1322
1323 }
1324
1325 /**
1326 * Gets the classification of a channel ID.
1327 *
1328 * @see {@link ChannelIDType} for info on the channel types
1329 *
1330 * @param channelID
1331 * The channel id to classify. Must not be null or empty.
1332 *
1333 * @return
1334 * Either the classification for the given ID or Null if the ID is invalid.
1335 */
1336 public static ChannelIDType getChannelIDType(String channelID) {
1337 assert (channelID != null);
1338 assert (channelID.length() > 0);
1339
1340 if (channelID.indexOf(CHANNEL_ID_SEPORATOR) >= 0) {
1341
1342 if (channelID.charAt(channelID.length() - 1) != LOCAL_CHANNEL_ID_TAG)
1343 return null; // Invalid!
1344
1345 return ChannelIDType.IndirectLocal;
1346
1347 } else if (channelID.charAt(channelID.length() - 1) == LOCAL_CHANNEL_ID_TAG) {
1348 return ChannelIDType.PureLocal;
1349 }
1350
1351 return ChannelIDType.Master;
1352
1353 }
1354
1355 /**
1356 * There is only ever one mix per channel, and one channel per mix
1357 * (i.e. 1 to 1 relationship).
1358 *
1359 * @author Brook Novak
1360 *
1361 */
1362 private class Channel {
1363
1364 ChannelIDType type; // "immutable" never null.
1365
1366 /** "immutable" - never null */
1367 TrackMixSubject mix;
1368
1369 /** Meaningless for channels that are a pure local mix or master */
1370 boolean isUsingLocalMix;
1371
1372 /** Apart from the systems master mix - this is a pre-mix to aggregate
1373 * with this channels actual/indirect mix. For example a channel that is
1374 * linked might need to mix with its top-level link mix...
1375 * Man that is a bad explanation!
1376 *
1377 * Only meaningful for indirect local channels - otherwise is just the same
1378 * as the TrackMixSubject channel ID.
1379 **/
1380 String masterMixID; // never null, itself can be amastermix
1381
1382 TrackSequence tseq; // resets to null to avoid holding expensive state ref (track model)
1383
1384 // Used for pausing / resuming
1385 int stoppedFramePosition = -1;
1386
1387 // A mark used outside of this package
1388 boolean isPaused = false;
1389
1390 /**
1391 * Consturctor.
1392 *
1393 * @param mixSub
1394 * @param isUsingLocalMix
1395 *
1396 * @throws IllegalArgumentException
1397 * If mixSub's channel ID is invlaid.
1398 */
1399 Channel(TrackMixSubject mixSub, boolean isUsingLocalMix) {
1400 assert(mixSub != null);
1401 mix = mixSub;
1402 tseq = null;
1403 this.isUsingLocalMix = isUsingLocalMix;
1404 this.type = getChannelIDType(mixSub.getChannelID());
1405
1406 if (type == null)
1407 throw new IllegalArgumentException("mixSub contains bad channel ID");
1408
1409 masterMixID = extractMasterChannelID(mixSub.getChannelID());
1410
1411 assert(integrity());
1412 }
1413
1414 private boolean integrity() {
1415
1416 assert(mix != null);
1417 assert(type != null);
1418 assert(masterMixID != null);
1419
1420 if(type == ChannelIDType.Master) {
1421 assert(masterMixID.equals(mix.getChannelID()));
1422 //assert(!isUsingLocalMix);
1423 assert(tseq == null);
1424 } else if(type == ChannelIDType.PureLocal) {
1425 assert(masterMixID.equals(mix.getChannelID()));
1426 } else if(type == ChannelIDType.IndirectLocal) {
1427 assert(!masterMixID.equals(mix.getChannelID())); // must have two parts at least
1428 }
1429
1430 return true;
1431 }
1432 }
1433
1434 /**
1435 * Channel IDs have different classifications.
1436 *
1437 * @author Brook Novak
1438 *
1439 */
1440 public enum ChannelIDType {
1441
1442 /**
1443 * Pure local channels have mixes that can be used by {@link ChannelIDType#IndirectLocal}
1444 * channels.
1445 */
1446 PureLocal, // never links to local mixes .. since is local.
1447
1448 /**
1449 * Indirect local channels are channels that have their own mix, but can also use a
1450 * pure local mix instead. Furthermore, they always pre-mix with a
1451 * {@link ChannelIDType#Master} channel.
1452 */
1453 IndirectLocal, // Combined of virtual/frame ids and local ids...
1454
1455 /**
1456 * Master channels are essentially channels containing mix information. They
1457 * can be linked by many {@link ChannelIDType#IndirectLocal} channels - where the
1458 * linked channels combine their curent mix with it's master mix.
1459 *
1460 * This is seperate from the systems master mix which is awlays applied in the
1461 * {@link ApolloPlaybackMixer} for every track.
1462 *
1463 */
1464 Master, // i.e. a channel that is purely used for mixing with child channels
1465 }
1466
1467}
Note: See TracBrowser for help on using the repository browser.