source: trunk/src_apollo/org/apollo/widgets/SampleRecorder.java@ 315

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

Apollo spin-off added

File size: 26.5 KB
Line 
1package org.apollo.widgets;
2
3import java.awt.Color;
4import java.awt.Dimension;
5import java.awt.FontMetrics;
6import java.awt.Graphics;
7import java.awt.Graphics2D;
8import java.awt.GridBagConstraints;
9import java.awt.GridBagLayout;
10import java.awt.Rectangle;
11import java.awt.event.ActionEvent;
12import java.awt.event.ActionListener;
13import java.awt.geom.AffineTransform;
14import java.awt.geom.Rectangle2D;
15import java.awt.image.BufferedImage;
16import java.io.ByteArrayOutputStream;
17import java.io.IOException;
18import java.io.PipedInputStream;
19
20import javax.sound.sampled.AudioFormat;
21import javax.sound.sampled.LineUnavailableException;
22import javax.swing.JButton;
23import javax.swing.JLabel;
24import javax.swing.JPanel;
25import javax.swing.JSpinner;
26import javax.swing.SpinnerModel;
27import javax.swing.SpinnerNumberModel;
28import javax.swing.SwingUtilities;
29
30import org.apollo.audio.ApolloSubjectChangedEvent;
31import org.apollo.audio.AudioCapturePipe;
32import org.apollo.audio.RecordManager;
33import org.apollo.audio.SampledAudioManager;
34import org.apollo.audio.util.FrameLayoutDaemon;
35import org.apollo.audio.util.MultiTrackPlaybackController;
36import org.apollo.audio.util.SoundDesk;
37import org.apollo.audio.util.Timeline;
38import org.apollo.audio.util.MultiTrackPlaybackController.MultitrackLoadListener;
39import org.apollo.gui.PeakTroughWaveFormRenderer;
40import org.apollo.gui.WaveFormRenderer;
41import org.apollo.io.IconRepository;
42import org.apollo.mvc.Observer;
43import org.apollo.mvc.Subject;
44import org.apollo.mvc.SubjectChangedEvent;
45import org.apollo.util.AudioMath;
46import org.apollo.util.AudioSystemLog;
47import org.apollo.util.TrackNameCreator;
48import org.expeditee.gui.DisplayIO;
49import org.expeditee.gui.Frame;
50import org.expeditee.gui.FrameGraphics;
51import org.expeditee.items.ItemParentStateChangedEvent;
52import org.expeditee.items.Text;
53import org.expeditee.items.widgets.InteractiveWidget;
54
55public class SampleRecorder extends InteractiveWidget
56 implements ActionListener, Observer, MultitrackLoadListener {
57
58 private enum WidgetState {
59 Ready, // Waiting to record
60 CountingDown,
61 LoadingPlayback,
62 Recording, // ...
63 Finalizing, // Capture stopped, reading last bytes from pipe
64 Finished // Removed from expeditee world, spawned audio track widget in place of...
65 }
66
67 private Subject observeredSubject = null;
68
69 private boolean shouldPlayback = false;
70 private WidgetState state = null;
71 private long recordStartTime = 0; // system time
72 private RecordedByteReader audioByteReader = null;
73 private CountDownTimer countdownThread = null;
74 private String playbackFrameName;
75 private long initiationTime = -1;
76
77 private JButton recordButton;
78 private JButton recordSynchedButton; // record while playing
79 private JButton stopButton;
80 private JSpinner countDownSpinner;
81 private JLabel countDownSpinnerLabel;
82 private JLabel statusLabel;
83
84 private AnimatedSampleGraph sampleGraph;
85 private boolean hasExplicityStopped = false;
86
87 private final static int BUTTON_HEIGHT = 50;
88 private final static int LABEL_HEIGHT = 30;
89 private final static int COUNTDOWN_SETTINGS_HEIGHT = 30;
90 private final static int HORO_SPACING = 2;
91 private final static int VERT_SPACING = 0;
92
93 private final static int MAX_COUNTDOWN_TIME = 60;
94
95 private static final Color GRAPH_BACKCOLOR = Color.BLACK;
96 private static final Color GRAPH_WAVECOLOR = Color.GREEN;
97
98 private final static int RENDER_POINTS_PER_SECOND = 20; // how many points to render each second
99
100 public SampleRecorder(Text source, String[] args) {
101 super(source, new JPanel(new GridBagLayout()),
102 AnimatedSampleGraph.GRAPH_WIDTH + (2 * HORO_SPACING),
103 AnimatedSampleGraph.GRAPH_WIDTH + (2 * HORO_SPACING),
104 COUNTDOWN_SETTINGS_HEIGHT + BUTTON_HEIGHT + LABEL_HEIGHT + AnimatedSampleGraph.GRAPH_HEIGHT + (4 * VERT_SPACING),
105 COUNTDOWN_SETTINGS_HEIGHT + BUTTON_HEIGHT + LABEL_HEIGHT + AnimatedSampleGraph.GRAPH_HEIGHT + (4 * VERT_SPACING));
106
107 // Create gui layout
108 recordButton = new JButton();
109 recordButton.setIcon(IconRepository.getIcon("record.png"));
110 recordButton.addActionListener(this);
111 recordButton.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH / 2, BUTTON_HEIGHT));
112
113 recordSynchedButton = new JButton();
114 recordSynchedButton.setIcon(IconRepository.getIcon("recordtime.png"));
115 recordSynchedButton.addActionListener(this);
116 recordSynchedButton.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH / 2, BUTTON_HEIGHT));
117
118 stopButton = new JButton();
119 stopButton.setIcon(IconRepository.getIcon("stop.png"));
120 stopButton.addActionListener(this);
121 stopButton.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH, BUTTON_HEIGHT));
122 stopButton.setVisible(false);
123
124 sampleGraph = new AnimatedSampleGraph();
125
126 statusLabel = new JLabel();
127 statusLabel.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH, LABEL_HEIGHT));
128 statusLabel.setHorizontalAlignment(JLabel.CENTER);
129 statusLabel.setVerticalAlignment(JLabel.CENTER);
130
131 SpinnerModel model =
132 new SpinnerNumberModel(0, 0, MAX_COUNTDOWN_TIME, 1);
133 countDownSpinner = new JSpinner(model);
134 countDownSpinner.setPreferredSize(new Dimension(50, COUNTDOWN_SETTINGS_HEIGHT));
135
136 countDownSpinnerLabel = new JLabel("Count down:");
137 countDownSpinnerLabel.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH - 50, COUNTDOWN_SETTINGS_HEIGHT));
138
139 // Layout GUI
140 GridBagConstraints c;
141
142 JPanel countdownPane = new JPanel(new GridBagLayout());
143 countdownPane.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH, COUNTDOWN_SETTINGS_HEIGHT));
144
145 c = new GridBagConstraints();
146 c.gridx = 0;
147 c.gridy = 0;
148 c.weightx = 1.0f;
149 c.fill = GridBagConstraints.BOTH;
150 countdownPane.add(countDownSpinnerLabel, c);
151
152 c = new GridBagConstraints();
153 c.gridx = 1;
154 c.gridy = 0;
155 c.fill = GridBagConstraints.BOTH;
156 countdownPane.add(countDownSpinner, c);
157
158 JPanel buttonPane = new JPanel(new GridBagLayout());
159 buttonPane.setPreferredSize(new Dimension(AnimatedSampleGraph.GRAPH_WIDTH, BUTTON_HEIGHT));
160
161 c = new GridBagConstraints();
162 c.gridx = 0;
163 c.gridy = 0;
164 c.fill = GridBagConstraints.BOTH;
165 buttonPane.add(recordButton, c);
166
167 c = new GridBagConstraints();
168 c.gridx = 1;
169 c.gridy = 0;
170 c.fill = GridBagConstraints.BOTH;
171 buttonPane.add(recordSynchedButton, c);
172
173 c = new GridBagConstraints();
174 c.gridx = 0;
175 c.gridy = 0;
176 c.gridwidth = 2;
177 c.fill = GridBagConstraints.BOTH;
178 buttonPane.add(stopButton, c);
179
180
181 // Assemble
182
183 c = new GridBagConstraints();
184 c.gridx = 0;
185 c.gridy = 0;
186 c.fill = GridBagConstraints.BOTH;
187 _swingComponent.add(buttonPane, c);
188
189 c = new GridBagConstraints();
190 c.gridx = 0;
191 c.gridy = 1;
192 c.fill = GridBagConstraints.BOTH;
193 _swingComponent.add(countdownPane, c);
194
195 c = new GridBagConstraints();
196 c.gridx = 0;
197 c.gridy = 2;
198 c.fill = GridBagConstraints.BOTH;
199 _swingComponent.add(sampleGraph, c);
200
201 c = new GridBagConstraints();
202 c.gridx = 0;
203 c.gridy = 3;
204 c.fill = GridBagConstraints.BOTH;
205 _swingComponent.add(statusLabel, c);
206
207 _swingComponent.doLayout();
208
209 setState(WidgetState.Ready, "Ready");
210 }
211
212 /**
213 * All of the widget logic is centarlized here - according to the new state transition
214 * Must be in AWT thread
215 *
216 * @param newState
217 */
218 private void setState(WidgetState newState, String status) {
219 if (state == newState) return;
220 WidgetState oldState = state;
221 state = newState;
222
223 sampleGraph.alternateText = null;
224 sampleGraph.invalidateGraph();
225
226 statusLabel.setText(status);
227
228 if (newState == WidgetState.Ready) {
229
230 recordButton.setVisible(true);
231 recordSynchedButton.setVisible(true);
232 stopButton.setVisible(false);
233
234 if (oldState != null) sampleGraph.clear();
235
236 } else if (newState == WidgetState.CountingDown) {
237 assert(oldState == WidgetState.Ready);
238
239 assert (countdownThread == null || !countdownThread.isAlive());
240
241 int countDown = (Integer)countDownSpinner.getValue();
242
243 if (countDown == 0) {
244 commenceRecording(shouldPlayback);
245 } else {
246
247 recordButton.setVisible(false);
248 recordSynchedButton.setVisible(false);
249 stopButton.setVisible(true);
250 stopButton.setEnabled(true);
251
252 sampleGraph.alternateText = Integer.toString(countDown);
253 sampleGraph.alternateTextColor = (countDown > 3) ? Color.WHITE : Color.RED;
254 sampleGraph.invalidateGraph();
255 FrameGraphics.refresh(true);
256
257 countdownThread = new CountDownTimer(countDown);
258 countdownThread.start();
259 }
260
261 } else if (newState == WidgetState.LoadingPlayback) {
262 assert(oldState == WidgetState.Ready || oldState == WidgetState.CountingDown);
263
264 recordButton.setVisible(false);
265 recordSynchedButton.setVisible(false);
266 stopButton.setVisible(true);
267 stopButton.setEnabled(false);
268
269 // TODO: Cancel load on users demand.
270
271
272 } else if (newState == WidgetState.Recording) {
273 assert(oldState == WidgetState.Ready ||
274 oldState == WidgetState.CountingDown ||
275 oldState == WidgetState.LoadingPlayback);
276
277 hasExplicityStopped = false;
278
279 recordButton.setVisible(false);
280 recordSynchedButton.setVisible(false);
281 stopButton.setVisible(true);
282 stopButton.setEnabled(true);
283
284 setStatusLabelToRecordTime();
285
286 } else if (newState == WidgetState.Finalizing) { // wait for pipe to finish reading bytes
287
288 // This state can be skipped if pipe has finished before
289 // stop event captured.
290
291 recordButton.setVisible(false);
292 recordSynchedButton.setVisible(false);
293 stopButton.setVisible(true);
294 stopButton.setEnabled(false);
295
296 // Thread reading from pipe will finish and trigger the finished
297
298 } else if (newState == WidgetState.Finished) {
299
300 // The widget could have been removed while recording or finializing.
301 // In such cases then do not do anything as the user has suggested
302 // that the want away with it all...
303 if (!hasExplicityStopped) {
304
305 // Reset the state
306 setState(WidgetState.Ready, "Ready");
307
308 } else {
309
310 // Remove this temporary widget
311 removeSelf();
312
313 // Spawn an audio track using the actual bytes and audio format buffered from
314 // the pipe. This will load instantly, saving of bytes is its responsibility.
315 if (audioByteReader != null && audioByteReader.bufferedAudioBytes != null
316 && audioByteReader.bufferedAudioBytes.size() > 0) {
317
318 // Get frame to anchor track to
319 Frame targetFrame = getParentFrame();
320 if (targetFrame == null) {
321 targetFrame = DisplayIO.getCurrentFrame();
322 }
323
324 assert(targetFrame != null);
325
326 if (!shouldPlayback) initiationTime = -1;
327
328 SampledTrackEvo4 trackWidget = SampledTrackEvo4.createFromMemory(
329 audioByteReader.bufferedAudioBytes.toByteArray(),
330 audioByteReader.audioFormat,
331 targetFrame,
332 getX(),
333 getY(),
334 initiationTime,
335 TrackNameCreator.getNameCopy(targetFrame.getTitle() + "_"),
336 null);
337
338 targetFrame.addAllItems(trackWidget.getItems());
339
340
341 }
342 }
343
344 }
345
346 // Ensure that if not recording or loading playback then stop/cancel multiplaybackl controller events
347 // that are commenced via this widget.
348 if (
349 newState != WidgetState.LoadingPlayback && newState != WidgetState.Recording
350 && (oldState == WidgetState.LoadingPlayback || oldState == WidgetState.Recording)
351 && playbackFrameName != null && shouldPlayback
352 && MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
353 playbackFrameName,
354 FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID)) {
355
356 MultiTrackPlaybackController.getInstance().cancelLoad(
357 playbackFrameName,
358 FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID);
359
360 MultiTrackPlaybackController.getInstance().stopPlayback();
361 }
362
363 }
364
365 private void setStatusLabelToRecordTime() {
366 long elapsed = (System.currentTimeMillis() - recordStartTime);
367 elapsed /= 1000;
368 //statusLabel.setText("Recorded " + elapsed + " seconds");
369 statusLabel.setText(elapsed + " seconds");
370 }
371
372 /**
373 * {@inheritDoc}
374 * SampleRecorderWidget is really stateless - they are means to capture audio only...
375 * That is, temporary.
376 */
377 @Override
378 protected String[] getArgs() {
379 return null;
380 }
381
382 @Override
383 protected void onParentStateChanged(int eventType) {
384 super.onParentStateChanged(eventType);
385
386 switch (eventType) {
387 case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
388 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
389 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
390
391 if (state == WidgetState.Recording) {
392 // This will change the state of this widget
393 RecordManager.getInstance().stopCapturing();
394
395 } else if (state == WidgetState.CountingDown) {
396 countdownThread.abortCountdown();
397
398 } else if (state == WidgetState.LoadingPlayback && playbackFrameName != null) {
399 MultiTrackPlaybackController.getInstance().cancelLoad(
400 playbackFrameName,
401 FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID);
402 }
403
404 // Ensure that this can be disposed
405 RecordManager.getInstance().removeObserver(this);
406 MultiTrackPlaybackController.getInstance().removeObserver(this);
407 break;
408
409 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
410 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
411 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
412 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
413 RecordManager.getInstance().addObserver(this);
414 MultiTrackPlaybackController.getInstance().addObserver(this);
415 break;
416
417 }
418
419 }
420
421 public Subject getObservedSubject() {
422 return observeredSubject;
423 }
424
425 public void setObservedSubject(Subject parent) {
426 observeredSubject = parent;
427 }
428
429 public void modelChanged(Subject source, SubjectChangedEvent event) { // subject messages
430
431 switch (event.getID()) {
432
433 case ApolloSubjectChangedEvent.CAPTURE_STARTED:
434 if (event.getState() == this) {
435 // Change to recording state
436 recordStartTime = System.currentTimeMillis();
437 setState(WidgetState.Recording, "Record started");
438 }
439 break;
440
441
442 case ApolloSubjectChangedEvent.CAPTURE_STOPPED:
443
444 if (state == WidgetState.Recording) {
445
446 // assert (event.getState() == this);
447 setState(WidgetState.Finalizing, "finalizing");
448 }
449 break;
450
451 case ApolloSubjectChangedEvent.PLAYBACK_STARTED: // from multi playback controller
452
453 if (state == WidgetState.LoadingPlayback && playbackFrameName != null &&
454 MultiTrackPlaybackController.getInstance().isCurrentPlaybackSubject(
455 playbackFrameName, FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID)) {
456 commenceRecording(false);
457 }
458 break;
459
460 }
461
462 }
463
464
465 public void multiplaybackLoadStatusUpdate(int id, Object state) {
466
467 if (state != WidgetState.LoadingPlayback) return;
468 String abortMessage = null;
469
470 switch(id) {
471 case MultitrackLoadListener.LOAD_CANCELLED:
472 abortMessage = "Playback/Record cancelled";
473 break;
474 case MultitrackLoadListener.LOAD_COMPLETE:
475 break;
476 case MultitrackLoadListener.LOAD_FAILED_BAD_GRAPH:
477 abortMessage = "Graph contains loops";
478 ((Exception)state).printStackTrace();
479 break;
480 case MultitrackLoadListener.LOAD_FAILED_GENERIC:
481 abortMessage = "Unexpected error";
482 ((Exception)state).printStackTrace();
483 break;
484 case MultitrackLoadListener.LOAD_FAILED_PLAYBACK:
485 abortMessage = "Graph contains loops";
486 break;
487 case MultitrackLoadListener.NOTHING_TO_PLAY:
488 abortMessage = "Nothing to play"; // could be due to user slecting empty space
489 break;
490 case MultitrackLoadListener.TRACK_LOAD_FAILED_IO:
491 // This is special... the loader does not abort... and it tries to load more.
492 ((Exception)state).printStackTrace();
493 break;
494 case MultitrackLoadListener.TRACK_LOADED:
495 break;
496
497 }
498
499 if (abortMessage != null) {
500 AudioSystemLog.println("Aborted playback - " + abortMessage);
501 setState(WidgetState.Ready, abortMessage);
502 }
503
504 }
505
506 public void actionPerformed(ActionEvent e) { // For action button
507
508
509 if (e.getSource() == stopButton) {
510 assert (state == WidgetState.Recording || state == WidgetState.CountingDown);
511
512 if (state == WidgetState.CountingDown) {
513 assert (countdownThread != null);
514 countdownThread.abortCountdown();
515 } else {
516 hasExplicityStopped = true;
517 RecordManager.getInstance().stopCapturing();
518 }
519
520 } else if (e.getSource() == recordButton || e.getSource() == recordSynchedButton) {
521 assert (state == WidgetState.Ready);
522 shouldPlayback = (e.getSource() == recordSynchedButton);
523 setState(WidgetState.CountingDown, "Counting down...");
524 }
525
526
527 }
528
529 /**
530 * Reads bytes asynchronously from a given pipe until it is finished or an exception occurs.
531 * Once finished excecution, the widget state will be set to finished.
532 *
533 * The bytes read are buffered, rendered and sent to the AnimatedSampleGraph for drawing.
534 * The status label is also updated according to the record time.
535 *
536 * @author Brook Novak
537 *
538 */
539 private class RecordedByteReader extends Thread {
540
541 private PipedInputStream pin;
542 private AudioFormat audioFormat;
543 private int bufferSize;
544 private int aggregationSize;
545 public ByteArrayOutputStream bufferedAudioBytes = null; // not null if started
546 private DoUpdateLabel updateLabelTask = new DoUpdateLabel();
547 private WaveFormRenderer waveFormRenderer = null;
548
549 RecordedByteReader(AudioCapturePipe pipe) {
550 assert(pipe != null);
551 this.pin = pipe.getPin();
552 this.audioFormat = pipe.getAudioFormat();
553 this.bufferSize = pipe.getBufferSize();
554 waveFormRenderer = new PeakTroughWaveFormRenderer(audioFormat);
555
556 // Aggregate size depends on samplerate
557 aggregationSize = (int)(audioFormat.getFrameRate() / RENDER_POINTS_PER_SECOND);
558 }
559
560 @Override
561 public void run() {
562
563 bufferedAudioBytes = new ByteArrayOutputStream();
564 byte[] buffer = new byte[bufferSize];
565 int len = 0;
566 int bytesBuffered;
567
568 int lastUpdateStateTime = 0;
569
570 try {
571 while(true) {
572
573 bytesBuffered = 0;
574
575 while (bytesBuffered < buffer.length) { // Full the buffer (unless reaches uneven end)
576 len = pin.read(buffer, bytesBuffered, buffer.length - bytesBuffered);
577 if (len == -1) break; // done
578 bytesBuffered += len;
579 }
580
581 if (bytesBuffered > 0) {
582
583 // Buffer bytes
584 bufferedAudioBytes.write(buffer, 0, bytesBuffered);
585
586 // Render bytes
587 float[] waveforms = waveFormRenderer.getSampleAmplitudes(
588 buffer,
589 0,
590 bytesBuffered / audioFormat.getFrameSize(),
591 aggregationSize);
592
593 // Send renderings to graph for drawing
594 SwingUtilities.invokeLater(new DoRenderGraph(waveforms));
595
596 }
597
598 if (len == -1) break; // done
599
600 // For every second elapsed, update status
601 if ((System.currentTimeMillis() - lastUpdateStateTime) >= 1000) {
602 SwingUtilities.invokeLater(updateLabelTask);
603 }
604 }
605 } catch (IOException e) {
606 e.printStackTrace();
607
608 } finally {
609
610 try {
611 pin.close();
612 } catch (IOException e) {
613 e.printStackTrace();
614 }
615
616 try {
617 bufferedAudioBytes.close();
618 } catch (IOException e) {
619 e.printStackTrace();
620 }
621
622 // Ensure that will enter finalized state
623 SwingUtilities.invokeLater(new DoFinished());
624
625 }
626
627 }
628 }
629
630 class DoFinished implements Runnable {
631 public void run() {
632 setState(WidgetState.Finished, "Finished");
633 }
634 }
635
636 class DoUpdateLabel implements Runnable {
637 public void run() {
638 setStatusLabelToRecordTime();
639 }
640 }
641
642 class DoRenderGraph implements Runnable {
643 private float[] waveForms;
644 public DoRenderGraph(float[] waveForms) {
645 this.waveForms = waveForms;
646 }
647 public void run() {
648 assert (audioByteReader != null);
649 sampleGraph.updateVisualization(waveForms);
650 }
651 }
652
653 /**
654 * The state will change eventually (or instantly is failed)
655 *
656 * @param withPlayback
657 * True to commence frame and enter in a loading state.
658 */
659 private void commenceRecording(boolean withPlayback) {
660
661 if (withPlayback) {
662
663 assert (state != WidgetState.LoadingPlayback);
664 assert (state != WidgetState.Recording);
665
666 setState(WidgetState.LoadingPlayback, "Loading tracks...");
667
668 Frame currentFrame = DisplayIO.getCurrentFrame();
669 if (currentFrame == null || currentFrame.getName() == null) {
670
671 setState(WidgetState.Ready, "No frame to play");
672 return;
673 }
674
675 playbackFrameName = currentFrame.getName();
676
677
678 Timeline tl = FrameLayoutDaemon.getInstance().getLastComputedTimeline();
679
680 if (tl == null || FrameLayoutDaemon.getInstance().getTimelineOwner() == null ||
681 FrameLayoutDaemon.getInstance().getTimelineOwner() != currentFrame) {
682 tl = FrameLayoutDaemon.inferTimeline(currentFrame);
683 }
684
685 if (tl != null) {
686
687 initiationTime = tl.getMSTimeAtX(getX());
688
689 // Clamp
690 if (initiationTime < tl.getFirstInitiationTime())
691 initiationTime = tl.getFirstInitiationTime();
692
693 else if (initiationTime > (tl.getFirstInitiationTime() + tl.getRunningTime()))
694 initiationTime = tl.getFirstInitiationTime() + tl.getRunningTime();
695
696 int startFrame = AudioMath.millisecondsToFrames(
697 initiationTime - tl.getFirstInitiationTime(),
698 SampledAudioManager.getInstance().getDefaultPlaybackFormat());
699
700 // To ensure that the frame channels are not in use
701 MultiTrackPlaybackController.getInstance().stopPlayback();
702 SoundDesk.getInstance().freeChannels(FramePlayer.FRAME_PLAYERMASTER_CHANNEL_ID);
703
704 FramePlayer.playFrame(
705 this,
706 currentFrame.getName(),
707 false,
708 0,
709 startFrame,
710 Integer.MAX_VALUE);
711
712 } else {
713 commenceRecording(false); // without playback
714 return;
715 }
716
717 } else {
718
719 try {
720
721 // Start capturing
722 AudioCapturePipe pipe = RecordManager.getInstance().captureAudio(this);
723
724 // Read bytes asynchronously ... buffer and render visual feedback
725 audioByteReader = new RecordedByteReader(pipe);
726 audioByteReader.start();
727
728 } catch (LineUnavailableException ex) {
729 AudioSystemLog.printException("Failed to commence audio capture via record widget", ex);
730 setState(WidgetState.Ready, "Bad Device");
731 } catch (IOException ex) {
732 AudioSystemLog.printException("Failed to commence audio capture via record widget", ex);
733 setState(WidgetState.Ready, "Failed");
734 }
735 }
736 }
737
738 /**
739 * Renders a waveform to a panel at realtime
740 */
741 private class AnimatedSampleGraph extends JPanel {
742
743 protected static final long serialVersionUID = 0L;
744
745 static final int GRAPH_WIDTH = 140;
746 static final int GRAPH_HEIGHT = 80;
747 static final int HALF_GRAPH_HEIGHT = GRAPH_HEIGHT / 2;
748 static final int SAMPLE_PIXEL_SPACING = 1;
749
750 private BufferedImage imageBuffer;
751 private Graphics2D imageGraphics;
752 private int lastSampleHeight = HALF_GRAPH_HEIGHT;
753
754 private String alternateText = null;
755 private Color alternateTextColor = Color.WHITE;
756
757 AnimatedSampleGraph() {
758 imageBuffer = new BufferedImage(GRAPH_WIDTH, GRAPH_HEIGHT, BufferedImage.TYPE_INT_RGB);
759 imageGraphics = (Graphics2D)imageBuffer.getGraphics();
760 imageGraphics.setColor(GRAPH_BACKCOLOR);
761 imageGraphics.fillRect(0, 0, GRAPH_WIDTH, GRAPH_HEIGHT);
762 setPreferredSize(new Dimension(GRAPH_WIDTH, GRAPH_HEIGHT));
763 }
764
765 /**
766 * Clears the graph and invlaidates itself
767 */
768 public void clear() {
769 imageGraphics.setColor(GRAPH_BACKCOLOR);
770 imageGraphics.fillRect(0, 0, GRAPH_WIDTH, GRAPH_HEIGHT);
771 invalidateGraph();
772 }
773
774 @Override
775 public void paint(Graphics g) {
776
777 if (alternateText != null) {
778
779 g.setColor(Color.BLACK);
780 g.fillRect(0, 0, getWidth(), getHeight());
781
782 g.setFont(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
783 g.setColor(alternateTextColor);
784
785 // Center track name
786 FontMetrics fm = g.getFontMetrics(TrackWidgetCommons.FREESPACE_TRACKNAME_FONT);
787 Rectangle2D rect = fm.getStringBounds(alternateText, g);
788
789 g.drawString(
790 alternateText,
791 (int)((getWidth() - rect.getWidth()) / 2),
792 (int)((getHeight() - rect.getHeight()) / 2) + (int)rect.getHeight()
793 );
794
795 } else {
796 g.drawImage(imageBuffer, 0, 0, null);
797 }
798 }
799
800 /**
801 * Renders the audio bytes to the graph
802 * @param audioBytes
803 */
804 public void updateVisualization(float[] waveForms) {
805
806 if (waveForms == null || waveForms.length == 0) return;
807
808 int pixelWidth = waveForms.length * SAMPLE_PIXEL_SPACING;
809
810 // Translate buffer back pixelWidth pixels
811 AffineTransform transform = new AffineTransform();
812 transform.translate(-pixelWidth, 0.0);
813 //transform.translate(pixelWidth, 0.0);
814 //AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR);
815 //imageBuffer = op.filter(imageBuffer, null);
816 imageGraphics.drawImage(imageBuffer, transform, this);
817
818 // Render backcolor
819 imageGraphics.setColor(GRAPH_BACKCOLOR);
820 imageGraphics.fillRect(GRAPH_WIDTH - pixelWidth, 0, pixelWidth, GRAPH_HEIGHT);
821
822 // Render wave forms from lastSampleHeight
823 imageGraphics.setColor(GRAPH_WAVECOLOR);
824 int currentPixelX = GRAPH_WIDTH - pixelWidth;
825
826 for (int i = 0; i < waveForms.length; i++) {
827
828 int currentHeight = HALF_GRAPH_HEIGHT + (int)(waveForms[i] * HALF_GRAPH_HEIGHT);
829
830 // Draw a line
831 imageGraphics.drawLine(currentPixelX - 1, lastSampleHeight, currentPixelX, currentHeight);
832
833 currentPixelX += SAMPLE_PIXEL_SPACING;
834 lastSampleHeight = currentHeight;
835 }
836
837 invalidateGraph();
838 }
839
840 private void invalidateGraph() {
841
842 // For fastest refreshing - invalidate directy via expedtiee
843 Rectangle dirty = this.getBounds();
844 dirty.translate(SampleRecorder.this.getX(), SampleRecorder.this.getY());
845 FrameGraphics.invalidateArea(dirty);
846 FrameGraphics.refresh(true);
847
848 }
849
850 }
851
852 /**
853 * Updates the timer / widget state.
854 *
855 * @author Brook Novak
856 *
857 */
858 private class CountDownTimer extends Thread {
859
860 private int currentCountdown;
861 private boolean returnToReadyState = false;
862
863 public CountDownTimer(int countdownSecs) {
864 assert(countdownSecs > 0);
865 currentCountdown = countdownSecs;
866 }
867
868 public void abortCountdown() {
869 interrupt();
870 }
871
872 public void run() {
873
874 try {
875
876 while (currentCountdown > 0) {
877
878 if (interrupted()) {
879 returnToReadyState = true;
880 break;
881 }
882
883 sleep(1000);
884 currentCountdown--;
885
886 // Update graph
887 SwingUtilities.invokeLater(new Runnable() {
888 public void run() {
889 sampleGraph.alternateText = Integer.toString(currentCountdown);
890 sampleGraph.alternateTextColor = (currentCountdown > 3) ?
891 Color.WHITE : Color.RED;
892 sampleGraph.invalidateGraph();
893 FrameGraphics.refresh(true);
894 }
895 });
896
897 }
898
899 } catch (InterruptedException e) {
900 returnToReadyState = true;
901 }
902
903 SwingUtilities.invokeLater(new Runnable() {
904 public void run() {
905 if (returnToReadyState) {
906 setState(WidgetState.Ready, "Ready");
907 } else {
908 commenceRecording(shouldPlayback);
909 }
910 }
911 });
912
913 }
914 }
915
916}
Note: See TracBrowser for help on using the repository browser.