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

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

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

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

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

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

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


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

Images:

org.expeditee.items.ItemUtils

Frames:

org.expeditee.gui.FrameIO

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

org.apollo.widgets.FramePlayer


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

org.apollo.widgets.SampledTrack


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

org.expeditee.auth.account.Authenticate


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

org.expeditee.gui.Frame

File size: 38.9 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 TrackSequence ts = new TrackSequence(
750 tmodel.getAllAudioBytes(),
751 tmodel.getFormat(),
752 startFrame,
753 endFrame,
754 relativeInitiationFrame,
755 tmodel); // Important: set as track model so it can be re-used
756
757 boolean succeeded = false;
758
759 try {
760
761 // Whenever the track is stopped, then auto-remove the track sequence and nullify
762 // the ts reference so that the referenced SampledTrackModel can be collected
763 // the the garbage collector
764 ts.addObserver(this);
765
766
767 // Reset pause flag
768 chan.isPaused = false;
769 chan.tseq = ts;
770
771 tseqIndexedChannels.put(ts, chan);
772
773 // Synch TS with mix
774 ts.setSoloFlag(isSolo(channelID));
775 updateMixSettings(chan);
776
777 // Allow listeners to register to the track sequence so that they can
778 // catch playback events for the mix
779 fireSubjectChanged(new SubjectChangedEvent(
780 ApolloSubjectChangedEvent.TRACK_SEQUENCE_CREATED, channelID));
781
782
783 // Commence playback... when its scheduled to
784 ApolloPlaybackMixer.getInstance().play(ts);
785 succeeded = true;
786
787 } finally {
788 if (!succeeded) {
789 chan.tseq = null;
790 tseqIndexedChannels.remove(ts);
791 }
792 }
793
794 }
795
796 /**
797 * Ensures that a channels mix settings are synched as well as all other
798 * channels that depend on it... (e.g. a pure local channel updating its indirect tracks).
799 *
800 * Omittedly, this is way to over the top than it needs to be :(
801 *
802 * @param chan
803 * Must not be null.
804 */
805 private void updateMixSettings(Channel chan) {
806 assert(chan != null);
807
808 // Update track sequence playing on the channel at the moment ..
809 // Only if this track mix is to be used for the track sequence
810 if (chan.tseq != null &&
811 chan.type != ChannelIDType.Master) {
812
813 boolean isMuted;
814 float volume;
815
816 if (chan.type == ChannelIDType.IndirectLocal) {
817
818 String localChannelID = extractLocalChannelID(chan.mix.getChannelID());
819 assert(localChannelID != null); // since type is not master
820
821 Channel baseChan = channels.get(localChannelID);
822
823 if (baseChan != null) { // it is possible for local channels to be missing
824 assert(baseChan.type == ChannelIDType.PureLocal);
825 isMuted = baseChan.mix.isMuted() || chan.mix.isMuted();
826 volume = baseChan.mix.getVolume() * chan.mix.getVolume();
827 } else {
828 isMuted = chan.mix.isMuted();
829 volume = chan.mix.getVolume();
830 }
831
832 // pre-mix with master mix - if has one
833 Channel master = channels.get(chan.masterMixID);
834 if (master != null) {
835 assert(master.type == ChannelIDType.Master);
836 isMuted |= master.mix.isMuted();
837 volume *= master.mix.getVolume();
838 }
839
840 } else {
841 isMuted = chan.mix.isMuted();
842 volume = chan.mix.getVolume();
843 }
844
845 chan.tseq.setMuted(isMuted);
846 chan.tseq.setVolume(volume);
847 }
848
849 // Update any channels that are linking to this (local) channel's mix at the momment
850 if (chan.type == ChannelIDType.PureLocal) {
851
852 List<Channel> lchans = linkedChannels.get(chan.mix.getChannelID());
853
854 if (lchans != null) {
855 for (Channel lchan : lchans) {
856 if (lchan.tseq != null)
857 updateMixSettings(lchan);
858 }
859 }
860
861 // Update any channels that are using this master mix at the moment
862 } else if (chan.type == ChannelIDType.Master) {
863
864 List<Channel> childChans = childChannels.get(chan.mix.getChannelID());
865
866 if (childChans != null) {
867 for (Channel cchan : childChans) {
868 assert(cchan.masterMixID.equals(chan.mix.getChannelID()));
869 assert(cchan.type == ChannelIDType.IndirectLocal);
870
871 // Is playing? i.e. do need to update?
872 if (cchan.tseq != null)
873 updateMixSettings(cchan);
874 }
875 }
876 }
877 }
878
879 public Subject getObservedSubject() {
880 return null;
881 }
882 public void setObservedSubject(Subject parent) {
883 }
884
885 /**
886 * Synchs the track sequences with the mixes.
887 * Assumes event is a ApolloSubjectChangedEvent and source
888 * is a TrackMixSubject
889 */
890 public void modelChanged(Subject source, SubjectChangedEvent event) {
891
892 if (source instanceof TrackMixSubject) {
893
894 TrackMixSubject tmix = (TrackMixSubject)source;
895
896 Channel chan = channels.get(tmix.getChannelID());
897 assert(chan != null);
898 assert(chan.mix == tmix);
899
900 updateMixSettings(chan);
901
902 } else if (source instanceof TrackSequence) {
903
904 if (event.getID() == ApolloSubjectChangedEvent.PLAYBACK_STOPPED) {
905
906 Channel chan = tseqIndexedChannels.get(source);
907 if (chan != null) {
908 freeChannel(chan, (TrackSequence)source);
909 }
910 }
911 }
912
913 }
914
915 public void freeChannels(String channelPrefix) {
916 assert (channelPrefix != null);
917 assert (channelPrefix.length() > 0);
918
919 for (Channel chan : channels.values()) {
920
921 if (chan.tseq != null && chan.mix.getChannelID().startsWith(channelPrefix)) {
922
923 freeChannel(chan, chan.tseq);
924 }
925 }
926
927 }
928
929 private void freeChannel(Channel chan, TrackSequence ts) {
930 assert (chan != null);
931 assert (ts != null);
932
933 chan.stoppedFramePosition = (chan.tseq != null) ? chan.tseq.getSuspendedFrame() : -1;
934 chan.tseq = null; // Free SampledTrackModel reference for garbage collection
935 tseqIndexedChannels.remove(ts); // " ditto "
936
937 }
938
939 /**
940 * Creates a <i>pure local</i> channel ID.
941 *
942 * @param sampledTrackWidget
943 * The widget to create the ID from. Must not be null.
944 *
945 * @return
946 * The channel ID... never null.
947 */
948 public static String createPureLocalChannelID(SampledTrack sampledTrackWidget) {
949 assert(sampledTrackWidget != null);
950 return createPureLocalChannelID(sampledTrackWidget.getLocalFileName());
951 }
952
953 /**
954 * Creates a <i>pure local</i> channel ID.
955 *
956 * @param tinf
957 * The TrackGraphInfo to create the ID from. Must not be null.
958 *
959 * @return
960 * The channel ID... never null.
961 */
962 public static String createPureLocalChannelID(TrackGraphNode tinf) {
963 assert(tinf != null);
964 return createPureLocalChannelID(tinf.getLocalFilename());
965 }
966
967 /**
968 * Creates a <i>pure local</i> channel ID.
969 *
970 * @param localFilename
971 * The local filename to create the channel ID from
972 *
973 * @return
974 * The channel ID... never null.
975 */
976 public static String createPureLocalChannelID(String localFilename) {
977 assert(localFilename != null && localFilename.length() > 0);
978 return localFilename + LOCAL_CHANNEL_ID_TAG;
979 }
980
981 /**
982 * Creates an <i>indirect</i> local channel id from a given virtual path.
983 *
984 * @param virtualPath
985 * The virtual path to create the id from. Must not be null.
986 *
987 * @param localFilename
988 * The local filename that the virtual path points to. Must not be null.
989 *
990 * @return
991 * An indirect local channel id... Never null.
992 */
993 public static String createIndirectLocalChannelID(List<String> virtualPath, String localFilename) {
994 assert(virtualPath != null);
995 assert(localFilename != null);
996
997 // Build prefix
998 StringBuilder sb = new StringBuilder();
999
1000 for (String virtualName : virtualPath) {
1001 sb.append(virtualName);
1002 sb.append(CHANNEL_ID_SEPORATOR);
1003 }
1004
1005 sb.append(createPureLocalChannelID(localFilename));
1006
1007 return sb.toString();
1008 }
1009
1010 public static String createMasterChannelID(LinkedTrack lwidget) {
1011 return lwidget.getVirtualFilename();
1012 }
1013 /**
1014 * Creates a master channel ID from a frame.
1015 *
1016 * @param frame
1017 * The frame to create the master mix ID from.
1018 * Must not be null.
1019 *
1020 * @return
1021 * The master mix for the given frame.
1022 */
1023 public static String createMasterChannelID(Frame frame) {
1024 assert(frame != null);
1025 return frame.getName();
1026 }
1027
1028 /**
1029 * Creates a master channel ID from a OverdubbedFrame.
1030 *
1031 * @param odFrame
1032 * The frame to create the master mix ID from.
1033 * Must not be null.
1034 *
1035 * @return
1036 * The master mix for the given odFrame.
1037 */
1038 public static String createMasterChannelID(OverdubbedFrame odFrame) {
1039 assert(odFrame != null);
1040 return odFrame.getFrameName();
1041 }
1042
1043 /**
1044 * Creates a master channel ID from a LinkedTracksGraphInfo.
1045 *
1046 * @param ltracks
1047 * The linked track to create the master mix ID from.
1048 * Must not be null.
1049 *
1050 * @return
1051 * The master mix for the given ltracks.
1052 */
1053 public static String createMasterChannelID(LinkedTracksGraphNode ltracks) {
1054 assert(ltracks != null);
1055 return ltracks.getVirtualFilename();
1056 }
1057
1058
1059 /**
1060 * Recursively creates channel IDs from a OverdubbedFrame. I.e. for all its
1061 * desending tracks. Linked tracks are exluded and only used for recursing.
1062 *
1063 * <b>Exlcudes the ID for odFrame (the master channel)</b>
1064 *
1065 * @param odFrame
1066 * The frame to start recursivly building the ID's from.
1067 *
1068 * @return
1069 * The list of linked ID's (to actual tracks) from this frame...
1070 * Never null. Can be empty if there are no tracks from the
1071 * given frame.
1072 */
1073 public static List<String> createChannelIDs(OverdubbedFrame odFrame) {
1074 assert(odFrame != null);
1075
1076 Stack<String> virtualPath = new Stack<String>();
1077 virtualPath.push(createMasterChannelID(odFrame)); // Master mix
1078 List<String> ids = new LinkedList<String>();
1079 createChannelIDs(odFrame, ids, new Stack<OverdubbedFrame>(), virtualPath);
1080 assert(virtualPath.size() == 1);
1081
1082 return ids;
1083 }
1084
1085 /**
1086 * Recursively creates channel IDs from a LinkedTracksGraphInfo. I.e. for all its
1087 * desending tracks. Linked tracks are exluded and only used for recursing.
1088 *
1089 * <b>Exlcudes the ID for ltracks (the master channel)</b>
1090 *
1091 * <b>Note:</b> calling {@link #createChannelIDs(OverdubbedFrame)} with the links
1092 * OverdubbedFrame is not the equivelent of calling this... because it uses a
1093 * different master channel ID.
1094 *
1095 * @param ltracks
1096 * The link to start recursivly building the ID's from.
1097 *
1098 * @return
1099 * The list of linked ID's (to actual tracks) from this frame...
1100 * Never null. Can be empty if there are no tracks from the
1101 * given ltracks.
1102 */
1103 public static List<String> createChannelIDs(LinkedTracksGraphNode ltracks) {
1104 assert(ltracks != null);
1105
1106 Stack<String> virtualPath = new Stack<String>();
1107 virtualPath.push(createMasterChannelID(ltracks));
1108 List<String> ids = new LinkedList<String>();
1109 createChannelIDs(ltracks.getLinkedFrame(), ids, new Stack<OverdubbedFrame>(), virtualPath);
1110 assert(virtualPath.size() == 1);
1111
1112 return ids;
1113 }
1114
1115 /**
1116 * Recursively creates channel ID.
1117 * @param current
1118 * The root at which to recuse at.
1119 *
1120 * @param ids
1121 * A list of strings to populate with channel ids.
1122 *
1123 * @param visited
1124 * Non-null Empty.
1125 *
1126 * @param virtualPath
1127 * Preload with IDs if needed.
1128 */
1129 private static void createChannelIDs(
1130 OverdubbedFrame current,
1131 List<String> ids,
1132 Stack<OverdubbedFrame> visited,
1133 Stack<String> virtualPath) {
1134
1135 // Avoid recursion .. never can be to safe.
1136 if (visited.contains(current)) return;
1137 visited.push(current);
1138
1139 if (current.trackExists()) {
1140
1141 // Build prefix
1142 StringBuilder prefixBuilder = new StringBuilder();
1143
1144 for (String virtualName : virtualPath) {
1145 prefixBuilder.append(virtualName);
1146 prefixBuilder.append(CHANNEL_ID_SEPORATOR);
1147 }
1148
1149 String prefix = prefixBuilder.toString();
1150 assert(prefix.length() > 0);
1151 assert(prefix.charAt(prefix.length() - 1) == CHANNEL_ID_SEPORATOR);
1152
1153 // Create id's
1154 for (TrackGraphNode tinf : current.getUnmodifiableTracks()) {
1155 String id = prefix + createPureLocalChannelID(tinf);
1156 assert(!ids.contains(id));
1157 ids.add(id);
1158 }
1159 }
1160
1161 // Recurse
1162 for (LinkedTracksGraphNode ltinf : current.getUnmodifiableLinkedTracks()) {
1163 virtualPath.push(ltinf.getVirtualFilename()); // build vpath
1164 createChannelIDs(ltinf.getLinkedFrame(), ids, visited, virtualPath);
1165 virtualPath.pop(); // maintain vpath
1166 }
1167
1168 visited.pop();
1169
1170 }
1171
1172
1173 /**
1174 * Extracts a pure local channelID...
1175 * Thus for a given channel ID made up of virtual links its all the virtual
1176 * ids are stripped and the remaining local channel ID is returned.
1177 *
1178 * @param channelID
1179 * The channel ID to extract the local channel ID from.
1180 * Must not be null or empty.
1181 *
1182 * @return
1183 * The local channel ID. Null if channelID does not refer to a local channel ID:
1184 * in that case the channel ID would either be invalid or be a pure link channel.
1185 *
1186 */
1187 public static String extractLocalChannelID(String channelID) {
1188 assert (channelID != null);
1189 assert(channelID.length() > 0);
1190
1191 if (channelID.charAt(channelID.length() - 1) == LOCAL_CHANNEL_ID_TAG) {
1192
1193 // Starting at the end... search bacward for the start or a seporator
1194 int i = channelID.length() - 2;
1195 while (i > 0 && channelID.charAt(i) != CHANNEL_ID_SEPORATOR) i--;
1196
1197 if (channelID.charAt(i) == CHANNEL_ID_SEPORATOR) i++;
1198
1199 if (((channelID.length() - 1) - i) == 0) return null;
1200
1201 return channelID.substring(i, channelID.length()); // include the local tag
1202
1203 }
1204
1205 // No local part
1206 return null;
1207 }
1208
1209 /**
1210 * Pure local or indirect local Channel IDs are encoded with local track names,
1211 *
1212 * @param channelID
1213 * The channel ID to extract the local filename from.
1214 *
1215 * @return
1216 * The local filename from the ID. Null if the channel ID is not a
1217 * pure local / indirect local id.
1218 *
1219 */
1220 public static String extractLocalFilename(String channelID) {
1221 String localID = extractLocalChannelID(channelID);
1222 if (localID != null && localID.length() > 1)
1223 return localID.substring(0, localID.length() - 1);
1224
1225 return null;
1226 }
1227
1228 /**
1229 * Extracts the master ID from a channel ID.
1230 * If the channel id is a pure local ID then it will return channelID.
1231 *
1232 * @param channelID
1233 * The channel id to extract the master id from. Must not be null.
1234 *
1235 * @return
1236 * The master ID. Null if channelID is null or invalid in some way...
1237 */
1238 public static String extractMasterChannelID(String channelID) {
1239 assert (channelID != null);
1240
1241 // master id = top-level id... that is, a pure virtual name, framename or local name
1242
1243 int i;
1244 for (i = 0; i < channelID.length(); i++) {
1245 if (channelID.charAt(i) == CHANNEL_ID_SEPORATOR) break;
1246 }
1247
1248 if (i > 1) {
1249 return channelID.substring(0, i);
1250 }
1251
1252 return null;
1253
1254 }
1255
1256 /**
1257 * Gets the classification of a channel ID.
1258 *
1259 * @see {@link ChannelIDType} for info on the channel types
1260 *
1261 * @param channelID
1262 * The channel id to classify. Must not be null or empty.
1263 *
1264 * @return
1265 * Either the classification for the given ID or Null if the ID is invalid.
1266 */
1267 public static ChannelIDType getChannelIDType(String channelID) {
1268 assert (channelID != null);
1269 assert (channelID.length() > 0);
1270
1271 if (channelID.indexOf(CHANNEL_ID_SEPORATOR) >= 0) {
1272
1273 if (channelID.charAt(channelID.length() - 1) != LOCAL_CHANNEL_ID_TAG)
1274 return null; // Invalid!
1275
1276 return ChannelIDType.IndirectLocal;
1277
1278 } else if (channelID.charAt(channelID.length() - 1) == LOCAL_CHANNEL_ID_TAG) {
1279 return ChannelIDType.PureLocal;
1280 }
1281
1282 return ChannelIDType.Master;
1283
1284 }
1285
1286 /**
1287 * There is only ever one mix per channel, and one channel per mix
1288 * (i.e. 1 to 1 relationship).
1289 *
1290 * @author Brook Novak
1291 *
1292 */
1293 private class Channel {
1294
1295 ChannelIDType type; // "immutable" never null.
1296
1297 /** "immutable" - never null */
1298 TrackMixSubject mix;
1299
1300 /** Apart from the systems master mix - this is a pre-mix to aggregate
1301 * with this channels actual/indirect mix. For example a channel that is
1302 * linked might need to mix with its top-level link mix...
1303 * Man that is a bad explanation!
1304 *
1305 * Only meaningful for indirect local channels - otherwise is just the same
1306 * as the TrackMixSubject channel ID.
1307 **/
1308 String masterMixID; // never null, itself can be a mastermix
1309
1310 TrackSequence tseq; // resets to null to avoid holding expensive state ref (track model)
1311
1312 // Used for pausing / resuming
1313 int stoppedFramePosition = -1;
1314
1315 // A mark used outside of this package
1316 boolean isPaused = false;
1317
1318 /**
1319 * Consturctor.
1320 *
1321 * @param mixSub
1322 * @param isUsingLocalMix
1323 *
1324 * @throws IllegalArgumentException
1325 * If mixSub's channel ID is invalid.
1326 */
1327 Channel(TrackMixSubject mixSub) {
1328 assert(mixSub != null);
1329 mix = mixSub;
1330 tseq = null;
1331 this.type = getChannelIDType(mixSub.getChannelID());
1332
1333 if (type == null)
1334 throw new IllegalArgumentException("mixSub contains bad channel ID");
1335
1336 masterMixID = extractMasterChannelID(mixSub.getChannelID());
1337
1338 assert(integrity());
1339 }
1340
1341 private boolean integrity() {
1342
1343 assert(mix != null);
1344 assert(type != null);
1345 assert(masterMixID != null);
1346
1347 if(type == ChannelIDType.Master) {
1348 assert(masterMixID.equals(mix.getChannelID()));
1349 assert(tseq == null);
1350 } else if(type == ChannelIDType.PureLocal) {
1351 assert(masterMixID.equals(mix.getChannelID()));
1352 } else if(type == ChannelIDType.IndirectLocal) {
1353 assert(!masterMixID.equals(mix.getChannelID())); // must have two parts at least
1354 }
1355
1356 return true;
1357 }
1358 }
1359
1360 /**
1361 * Channel IDs have different classifications.
1362 *
1363 * @author Brook Novak
1364 *
1365 */
1366 public enum ChannelIDType {
1367
1368 /**
1369 * Pure local channels have mixes that can be used by {@link ChannelIDType#IndirectLocal}
1370 * channels.
1371 */
1372 PureLocal, // never links to local mixes .. since is local.
1373
1374 /**
1375 * Indirect local channels are channels that have their own mix, but can also use a
1376 * pure local mix instead. Furthermore, they always pre-mix with a
1377 * {@link ChannelIDType#Master} channel.
1378 */
1379 IndirectLocal, // Combined of virtual/frame ids and local ids...
1380
1381 /**
1382 * Master channels are essentially channels containing mix information. They
1383 * can be linked by many {@link ChannelIDType#IndirectLocal} channels - where the
1384 * linked channels combine their curent mix with it's master mix.
1385 *
1386 * This is seperate from the systems master mix which is awlays applied in the
1387 * {@link ApolloPlaybackMixer} for every track.
1388 *
1389 */
1390 Master, // i.e. a channel that is purely used for mixing with child channels
1391 }
1392
1393}
Note: See TracBrowser for help on using the repository browser.