source: trunk/src_apollo/org/apollo/widgets/FramePlayer.java@ 352

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

Replace confusing errors messages with better descriptions

File size: 17.5 KB
Line 
1package org.apollo.widgets;
2
3import java.awt.Color;
4import java.awt.Dimension;
5import java.awt.GridBagConstraints;
6import java.awt.GridBagLayout;
7import java.awt.Insets;
8import java.awt.event.ActionEvent;
9import java.awt.event.ActionListener;
10import java.util.List;
11
12import javax.swing.Icon;
13import javax.swing.JButton;
14import javax.swing.JPanel;
15import javax.swing.JSlider;
16import javax.swing.JToggleButton;
17import javax.swing.event.ChangeEvent;
18import javax.swing.event.ChangeListener;
19
20import org.apollo.audio.ApolloSubjectChangedEvent;
21import org.apollo.audio.SampledAudioManager;
22import org.apollo.audio.util.FrameLayoutDaemon;
23import org.apollo.audio.util.MultiTrackPlaybackController;
24import org.apollo.audio.util.SoundDesk;
25import org.apollo.audio.util.TrackMixSubject;
26import org.apollo.audio.util.MultiTrackPlaybackController.MultitrackLoadListener;
27import org.apollo.io.IconRepository;
28import org.apollo.mvc.Observer;
29import org.apollo.mvc.Subject;
30import org.apollo.mvc.SubjectChangedEvent;
31import org.apollo.util.AudioMath;
32import org.apollo.util.AudioSystemLog;
33import org.expeditee.gui.DisplayIO;
34import org.expeditee.gui.Frame;
35import org.expeditee.gui.FrameGraphics;
36import org.expeditee.items.ItemParentStateChangedEvent;
37import org.expeditee.items.Text;
38import org.expeditee.items.widgets.InteractiveWidget;
39
40/**
41 * This a last minute hack ... should be revised
42 *
43 * @author Brook Novak
44 *
45 */
46public class FramePlayer extends InteractiveWidget
47 implements Observer, MultitrackLoadListener, ActionListener {
48
49 private static final int BUTTON_SIZE = 40;
50
51 private static TrackMixSubject masterMix = null;
52 private static String currentPlayingFrame = null;
53
54 private JButton playPauseButton;
55 private JButton stopButton;
56 private JButton rewindButton;
57 private JToggleButton muteButton;
58 private JToggleButton soloButton;
59 private JSlider volumeSlider;
60
61 private int state = READY;
62 private String abortMessage = null;
63
64 private boolean isUpdatingGUI = false;
65
66 public static String FRAME_PLAYERMASTER_CHANNEL_ID = "#$frameplayer#master$";
67
68 private static final Color LOADING_BORDER_COLOR = new Color(22, 205, 5);
69 //private static final Color FAILED_MESSAGE_COLOR = Color.RED;
70 //private static final Font MESSAGE_FONT = TrackWidgetCommons.FREESPACE_TRACKNAME_FONT;
71
72
73 /** States - mutex in this widget but not really .. for example since can be loading tracks and the graph at the same time... */
74 private static final int READY = 1; // Waiting for user interaction
75 private static final int PLAYBACK_LOADING = 2; // Loading tracks from file/cache to play/resume
76 private static final int PLAYING = 3; // Playing audio.
77
78
79 static {
80 masterMix = SoundDesk.getInstance().getOrCreateMix(FRAME_PLAYERMASTER_CHANNEL_ID);
81 }
82
83 /**
84 * Constructor called by Expeditee.
85 *
86 * @param source
87 * @param args
88 */
89 public FramePlayer(Text source, String[] args) {
90 super(source, new JPanel(new GridBagLayout()),
91 BUTTON_SIZE * 8, BUTTON_SIZE * 8,
92 BUTTON_SIZE, BUTTON_SIZE);
93
94 playPauseButton = new JButton();
95 playPauseButton.addActionListener(this);
96 playPauseButton.setIcon(IconRepository.getIcon("play.png"));
97 playPauseButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
98 playPauseButton.setToolTipText("Play selection / Pause");
99
100 stopButton = new JButton();
101 stopButton.setEnabled(false);
102 stopButton.addActionListener(this);
103 stopButton.setIcon(IconRepository.getIcon("stop.png"));
104 stopButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
105 stopButton.setToolTipText("Stop playback");
106
107 rewindButton = new JButton();
108 rewindButton.addActionListener(this);
109 rewindButton.setIcon(IconRepository.getIcon("rewind.png"));
110 rewindButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
111 rewindButton.setToolTipText("Rewind to start");
112
113 // Icon changes
114 muteButton = new JToggleButton();
115 muteButton.setSelectedIcon(IconRepository.getIcon("volmute.png"));
116 muteButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
117 muteButton.setToolTipText("Toggle mute");
118 muteButton.addChangeListener(new ChangeListener() {
119 public void stateChanged(ChangeEvent e) {
120 if (!FramePlayer.this.isUpdatingGUI) {
121 muteChanged();
122 }
123 }
124 });
125
126 soloButton = new JToggleButton();
127 soloButton.setIcon(IconRepository.getIcon("solo.png"));
128 soloButton.setSelectedIcon(IconRepository.getIcon("soloon.png"));
129 soloButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
130 soloButton.setToolTipText("Toggle solo");
131 soloButton.addChangeListener(new ChangeListener() {
132 public void stateChanged(ChangeEvent e) {
133 if (!FramePlayer.this.isUpdatingGUI) {
134 soloChanged();
135 }
136 }
137 });
138
139
140 final int VOLUME_SPACING = 6;
141 volumeSlider = new JSlider(JSlider.HORIZONTAL);
142 volumeSlider.setMinimum(0);
143 volumeSlider.setMaximum(100);
144 volumeSlider.addChangeListener(new ChangeListener() {
145 public void stateChanged(ChangeEvent e) {
146 if (!FramePlayer.this.isUpdatingGUI) {
147 volumeChanged();
148 }
149 // Update the icons
150 updateButtonGUI();
151 }
152 });
153
154 volumeSlider.setPreferredSize(new Dimension((3 * BUTTON_SIZE) - (2 * VOLUME_SPACING), BUTTON_SIZE));
155
156 GridBagConstraints c = new GridBagConstraints();
157 c.gridx = 0;
158 c.gridy = 0;
159 c.fill = GridBagConstraints.BOTH;
160 _swingComponent.add(playPauseButton, c);
161
162 c = new GridBagConstraints();
163 c.gridx = 1;
164 c.gridy = 0;
165 c.fill = GridBagConstraints.BOTH;
166 _swingComponent.add(stopButton, c);
167
168 c = new GridBagConstraints();
169 c.gridx = 2;
170 c.gridy = 0;
171 c.fill = GridBagConstraints.BOTH;
172 _swingComponent.add(rewindButton, c);
173
174 c = new GridBagConstraints();
175 c.gridx = 3;
176 c.gridy = 0;
177 c.fill = GridBagConstraints.BOTH;
178 _swingComponent.add(soloButton, c);
179
180 c = new GridBagConstraints();
181 c.gridx = 4;
182 c.gridy = 0;
183 c.fill = GridBagConstraints.BOTH;
184 _swingComponent.add(muteButton, c);
185
186 c = new GridBagConstraints();
187 c.gridx = 5;
188 c.gridy = 0;
189 c.fill = GridBagConstraints.BOTH;
190 c.insets = new Insets(0,VOLUME_SPACING,0,VOLUME_SPACING);
191 _swingComponent.add(volumeSlider, c);
192
193 updateButtonGUI();
194 updateBorderColor();
195
196 setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
197
198 }
199
200
201 /**
202 * Sets the GUI and all the popups to reflect the current state.
203 * Must be on the swing thread.
204 *
205 * @param newState
206 * The new state. The same state then it is ignored, unless
207 * its LOADING_TRACK_GRAPH - in that case the graph is reloaded.
208 */
209 private void setState(int newState) {
210 if (this.state == newState) return; // no need to process request.
211
212 switch(newState) {
213 case READY:
214
215 rewindButton.setEnabled(true);
216 stopButton.setEnabled(false);
217 playPauseButton.setEnabled(true);
218 playPauseButton.setIcon(IconRepository.getIcon("play.png"));
219
220 setWidgetEdgeThickness(TrackWidgetCommons.STOPPED_TRACK_EDGE_THICKNESS);
221
222 break;
223
224 case PLAYBACK_LOADING:
225 stopButton.setEnabled(false);
226 rewindButton.setEnabled(false);
227 playPauseButton.setEnabled(false);
228 break;
229
230 case PLAYING:
231 stopButton.setEnabled(true);
232 rewindButton.setEnabled(false);
233 playPauseButton.setEnabled(true);
234 playPauseButton.setIcon(IconRepository.getIcon("pause.png"));
235
236 setWidgetEdgeThickness(TrackWidgetCommons.PLAYING_TRACK_EDGE_THICKNESS);
237
238 break;
239
240 }
241
242 state = newState;
243
244 updateBorderColor();
245
246 invalidateSelf();
247 FrameGraphics.refresh(true);
248 }
249
250 /**
251 * {@inheritDoc}
252 */
253 @Override
254 protected void onParentStateChanged(int eventType) {
255 super.onParentStateChanged(eventType);
256
257
258 //Frame currentFrame = DisplayIO.getCurrentFrame();
259 // String currentFrameName = (currentFrame != null) ? currentFrame.getName() : null;
260
261 switch (eventType) {
262
263 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
264 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
265 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
266 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
267
268 // Setup observers
269 SoundDesk.getInstance().addObserver(this); // for solo
270 MultiTrackPlaybackController.getInstance().addObserver(this); // the core!
271 masterMix.addObserver(this);
272
273 // Evaluate the state of this add set the state accordingly
274 if (currentPlayingFrame != null
275 && MultiTrackPlaybackController.getInstance().isLoading(
276 currentPlayingFrame,
277 masterMix.getChannelID())) {
278
279 // Ensure that am receiving notifiactions:
280 List<String> loaded = MultiTrackPlaybackController.getInstance().attachLoadListener(this);
281 assert(loaded != null);
282
283 setState(PLAYBACK_LOADING);
284
285 } else if (currentPlayingFrame != null
286 && MultiTrackPlaybackController.getInstance().isPlaying(
287 currentPlayingFrame,
288 masterMix.getChannelID())) {
289
290 setState(PLAYING);
291
292 } else {
293 setState(READY);
294 }
295
296 break;
297
298 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
299
300 // Cancel loading of audio
301 if (state == PLAYBACK_LOADING) {
302 assert(currentPlayingFrame != null);
303
304 MultiTrackPlaybackController.getInstance().cancelLoad(
305 currentPlayingFrame,
306 masterMix.getChannelID());
307 }
308
309 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY: // TODO revise - and in sampled track widget
310 case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
311
312 // Remove observers
313 SoundDesk.getInstance().removeObserver(this);
314 MultiTrackPlaybackController.getInstance().removeObserver(this);
315 masterMix.removeObserver(this);
316
317 setState(READY);
318 break;
319
320 }
321 }
322
323
324
325 /**
326 * {@inheritDoc}
327 */
328 public void multiplaybackLoadStatusUpdate(int id, Object state) {
329
330 // NOTE: Could switch to an unloaded state
331 if (this.state != PLAYBACK_LOADING) return;
332
333 switch(id) {
334 case MultitrackLoadListener.LOAD_CANCELLED:
335 setState(READY);
336 break;
337 case MultitrackLoadListener.LOAD_COMPLETE:
338 break;
339 case MultitrackLoadListener.LOAD_FAILED_BAD_GRAPH:
340 abortMessage = "Graph contains loops";
341 ((Exception)state).printStackTrace();
342 break;
343 case MultitrackLoadListener.LOAD_FAILED_GENERIC:
344 abortMessage = "Unexpected error";
345 ((Exception)state).printStackTrace();
346 break;
347 case MultitrackLoadListener.LOAD_FAILED_PLAYBACK:
348 abortMessage = "Unable to aquire sound device";
349 break;
350 case MultitrackLoadListener.NOTHING_TO_PLAY:
351 abortMessage = "Nothing to play"; // could be due to user slecting empty space
352 break;
353 case MultitrackLoadListener.TRACK_LOAD_FAILED_IO:
354 // This is special... the loader does not abort... and it tries to load more.
355 ((Exception)state).printStackTrace();
356 break;
357 case MultitrackLoadListener.TRACK_LOADED:
358 break;
359
360 }
361
362 if (abortMessage != null) {
363 AudioSystemLog.println("Aborted playback - " + abortMessage);
364 setState(READY);
365 }
366
367 }
368
369 /**
370 * {@inheritDoc}
371 */
372 public Subject getObservedSubject() {
373 return null;
374 }
375
376 /**
377 * {@inheritDoc}
378 */
379 public void modelChanged(Subject source, SubjectChangedEvent event) {
380
381 // Synch GUI with track state
382 switch (event.getID()) {
383
384 case ApolloSubjectChangedEvent.PLAYBACK_STARTED:
385
386 if (currentPlayingFrame != null &&
387 MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
388 currentPlayingFrame, masterMix.getChannelID())) {
389 setState(PLAYING);
390 }
391
392 break;
393
394 case ApolloSubjectChangedEvent.PLAYBACK_STOPPED:
395
396 if (state == PLAYING) {
397 //assert(currentPlayingFrame != null);
398
399 // Transition into new state
400 setState(READY);
401
402 }
403 break;
404
405 case ApolloSubjectChangedEvent.MULTIPLAYBACK_LOADING:
406
407 // Adjust state accordingly
408 if (currentPlayingFrame != null &&
409 MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
410 currentPlayingFrame, masterMix.getChannelID()) && state != PLAYBACK_LOADING) {
411 MultiTrackPlaybackController.getInstance().attachLoadListener(this);
412 setState(PLAYBACK_LOADING);
413 }
414 break;
415
416 case ApolloSubjectChangedEvent.VOLUME: // From obseved track mix
417 updateVolume();
418 break;
419
420 case ApolloSubjectChangedEvent.MUTE: // From obseved track mix
421 updateMute();
422 updateBorderColor();
423 break;
424
425 case ApolloSubjectChangedEvent.SOLO_PREFIX_CHANGED: // From mix desk
426 updateSolo();
427 updateBorderColor();
428 break;
429
430
431 }
432
433 }
434
435 /**
436 * {@inheritDoc}
437 */
438 public void setObservedSubject(Subject parent) {
439 }
440
441 /**
442 * {@inheritDoc}
443 */
444 @Override
445 protected String[] getArgs() {
446 return null;
447 }
448
449 /**
450 * {@inheritDoc}
451 */
452 public void actionPerformed(ActionEvent e) {
453
454 if (!(state == PLAYING || state == READY)) return; // safety
455
456 Frame currentFrame = DisplayIO.getCurrentFrame();
457 String currentFrameName = (currentFrame != null) ? currentFrame.getName() : null;
458
459 if (e.getSource() == playPauseButton) {
460
461 if (state == READY && currentFrameName != null) { // comence playback
462
463 int startFrame = -1, endFrame = -1;
464
465 // Resume playback?
466 if (currentPlayingFrame != null &&
467 MultiTrackPlaybackController.getInstance().isMarkedAsPaused(
468 currentPlayingFrame, masterMix.getChannelID())) {
469
470 long runningTime = FrameLayoutDaemon.inferCurrentTotalMSTime();
471 long inferredTotalFrameLength = -1;
472 if (runningTime > 0) {
473 inferredTotalFrameLength = AudioMath.millisecondsToFrames(
474 runningTime,
475 SampledAudioManager.getInstance().getDefaultPlaybackFormat());
476 }
477
478
479 startFrame = MultiTrackPlaybackController.getInstance().getLastSuspendedFrame();
480
481 if (inferredTotalFrameLength > 0 &&
482 startFrame >= 0 && startFrame < inferredTotalFrameLength) {
483 // Work around for playing to end of frame: give biggest value
484 // since it is eventually clamped:
485 endFrame = Integer.MAX_VALUE;
486 }
487 }
488
489 // Play from beginning of selection to end of selection
490 if (startFrame < 0) {
491 startFrame = 0;
492 endFrame = Integer.MAX_VALUE; // see notes about for workaround hack
493 }
494
495 if (startFrame < endFrame) {
496
497 playFrame(this,
498 currentFrameName,
499 false, // TODO: Set appropriatly
500 0,
501 startFrame,
502 endFrame);
503
504 setState(PLAYBACK_LOADING);
505
506 }
507
508 } else { // pause
509
510 MultiTrackPlaybackController.getInstance().setPauseMark(true);
511 MultiTrackPlaybackController.getInstance().stopPlayback();
512
513 }
514
515
516 } else if (e.getSource() == stopButton) {
517 assert(currentPlayingFrame != null);
518
519 if (MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
520 currentPlayingFrame, masterMix.getChannelID())) {
521 MultiTrackPlaybackController.getInstance().setPauseMark(false);
522 MultiTrackPlaybackController.getInstance().stopPlayback();
523 }
524
525
526 } else if (e.getSource() == rewindButton) {
527 assert(state != PLAYING);
528 MultiTrackPlaybackController.getInstance().setPauseMark(false);
529
530 }
531
532 }
533
534
535 private void updateBorderColor() {
536
537 // Get border color currently used
538 Color oldC = getSource().getBorderColor();
539 Color newC = null;
540
541 if (this.state == PLAYBACK_LOADING) {
542
543 newC = LOADING_BORDER_COLOR;
544
545 } else {
546
547 newC = TrackWidgetCommons.getBorderColor(
548 SoundDesk.getInstance().isSolo(masterMix.getChannelID()),
549 masterMix.isMuted());
550
551 }
552
553 // Update the color
554 if (!newC.equals(oldC)) {
555 setWidgetEdgeColor(newC);
556 }
557 }
558
559 /**
560 * Sets the mute icon to represent the current volume value in the slider.
561 * Note: this is not the icon if mute is on.
562 */
563 private void updateButtonGUI() {
564
565 Icon newIcon = null;
566 if (volumeSlider.getValue() <= 25)
567 newIcon = IconRepository.getIcon("vol25.png");
568 else if (volumeSlider.getValue() <= 50)
569 newIcon = IconRepository.getIcon("vol50.png");
570 else if (volumeSlider.getValue() <= 75)
571 newIcon = IconRepository.getIcon("vol75.png");
572 else // maxing
573 newIcon = IconRepository.getIcon("vol100.png");
574
575 muteButton.setIcon(newIcon);
576 }
577
578
579 public void volumeChanged() {
580 masterMix.setVolume(((float)volumeSlider.getValue()) / 100.0f);
581 }
582
583 public void muteChanged() {
584 masterMix.setMuted(muteButton.isSelected());
585 }
586
587 public void soloChanged() {
588 SoundDesk.getInstance().setSoloIDPrefix(soloButton.isSelected() ?
589 masterMix.getChannelID() : null
590 );
591 }
592
593
594 /**
595 * Updates the volume GUI for all views
596 */
597 public void updateVolume() {
598 int volume = (int)(100 * masterMix.getVolume());
599
600 if (volumeSlider.getValue() == volume) return;
601
602 isUpdatingGUI = true;
603
604 volumeSlider.setValue(volume);
605
606 isUpdatingGUI = false;
607 }
608
609
610 /**
611 * Updates the mute button GUI for all views.
612 */
613 public void updateMute() {
614 if (muteButton.isSelected() == masterMix.isMuted()) return;
615
616 isUpdatingGUI = true;
617
618 muteButton.setSelected(masterMix.isMuted());
619
620 isUpdatingGUI = false;
621 }
622
623
624 /**
625 * Updates the solo button GUI for all views.
626 */
627 public void updateSolo() {
628 boolean isSolo = SoundDesk.getInstance().isSolo(masterMix.getChannelID());
629
630 if (soloButton.isSelected() == isSolo) return;
631
632 isUpdatingGUI = true;
633
634 soloButton.setSelected(isSolo);
635
636 isUpdatingGUI = false;
637
638 }
639
640
641 /**
642 * Plays a frame with the FramePlayer mix.
643 *
644 * @see MultiTrackPlaybackController#playFrame(MultitrackLoadListener, String, String, boolean, int, int, int)
645 *
646 */
647 public static void playFrame(
648 MultitrackLoadListener loadListener,
649 String rootFrameName,
650 boolean resume,
651 int relativeStartFrame,
652 int startFrame,
653 int endFrame) {
654
655 if (loadListener == null) throw new NullPointerException("loadListener");
656 if (rootFrameName == null) throw new NullPointerException("rootFrameName");
657
658
659 currentPlayingFrame = rootFrameName;
660
661 MultiTrackPlaybackController.getInstance().playFrame(
662 loadListener,
663 rootFrameName,
664 masterMix.getChannelID(),
665 resume,
666 relativeStartFrame,
667 startFrame,
668 endFrame);
669 }
670
671}
Note: See TracBrowser for help on using the repository browser.