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

Last change on this file since 1102 was 1102, checked in by davidb, 6 years ago

Reworking of the code-base to separate logic from graphics. This version of Expeditee now supports a JFX graphics as an alternative to SWING

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