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

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

Fixed timeline-infer algorithm

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