source: trunk/src/org/apollo/audio/util/SoundDesk.java@ 1561

Last change on this file since 1561 was 1561, checked in by davidb, 3 years ago

A set of changes that spans three things: beat detection, time stretching; and a debug class motivated by the need to look at a canvas redraw issue most notable when a waveform widget is playing

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