source: trunk/src/org/apollo/widgets/SampleRecorder.java@ 1057

Last change on this file since 1057 was 1057, checked in by davidb, 8 years ago

Tweaks to make widget smaller in height

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