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

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

Refactored a class name and extended recorder widgets to have a perminant lifetime option (for optimum idea capturing!)

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