source: trunk/src_apollo/org/apollo/widgets/SearchRecorder.java@ 355

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

Many fixes and usability improvements.

File size: 11.6 KB
Line 
1package org.apollo.widgets;
2
3import java.awt.BorderLayout;
4import java.awt.Dimension;
5import java.awt.GridBagConstraints;
6import java.awt.GridBagLayout;
7import java.awt.event.ActionEvent;
8import java.awt.event.ActionListener;
9import java.io.ByteArrayOutputStream;
10import java.io.IOException;
11import java.io.PipedInputStream;
12
13import javax.sound.sampled.AudioFormat;
14import javax.sound.sampled.LineUnavailableException;
15import javax.swing.JButton;
16import javax.swing.JLabel;
17import javax.swing.JPanel;
18import javax.swing.SwingUtilities;
19
20import org.apollo.agents.MelodySearch;
21import org.apollo.audio.ApolloSubjectChangedEvent;
22import org.apollo.audio.AudioCapturePipe;
23import org.apollo.audio.RecordManager;
24import org.apollo.audio.SampledAudioManager;
25import org.apollo.audio.util.MultiTrackPlaybackController;
26import org.apollo.io.IconRepository;
27import org.apollo.mvc.Observer;
28import org.apollo.mvc.Subject;
29import org.apollo.mvc.SubjectChangedEvent;
30import org.apollo.util.ApolloSystemLog;
31import org.expeditee.actions.Actions;
32import org.expeditee.gui.DisplayIO;
33import org.expeditee.gui.Frame;
34import org.expeditee.items.ItemParentStateChangedEvent;
35import org.expeditee.items.Text;
36import org.expeditee.items.widgets.InteractiveWidget;
37
38public class SearchRecorder extends InteractiveWidget
39 implements ActionListener, Observer {
40
41 private enum WidgetState {
42 Ready, // Waiting to record a querry
43 Recording, // ...
44 Finalizing, // Capture stopped, reading last bytes from pipe
45 }
46
47 private WidgetState state = null;
48 private RecordedByteReader audioByteReader = null;
49 private long recordStartTime = 0; // system time
50
51 private JButton recordButton;
52 private JButton commitButton; // stop recording and commence search
53 private JButton cancelButton;
54 JLabel statusLabel;
55
56 private boolean hasExplicityStopped = false;
57
58 private final static int BUTTON_SIZE = 50;
59 private final static int LABEL_HEIGHT = 30;
60
61 public SearchRecorder(Text source, String[] args) {
62 super(source, new JPanel(new BorderLayout()),
63 BUTTON_SIZE * 2,
64 BUTTON_SIZE * 2,
65 BUTTON_SIZE + LABEL_HEIGHT,
66 BUTTON_SIZE + LABEL_HEIGHT);
67
68 // Create gui layout
69 recordButton = new JButton();
70 recordButton.setIcon(IconRepository.getIcon("searchmel.png"));
71 recordButton.setToolTipText("Search for audio from live recording");
72 recordButton.addActionListener(this);
73 recordButton.setPreferredSize(new Dimension(BUTTON_SIZE * 2, BUTTON_SIZE));
74
75 commitButton = new JButton();
76 commitButton.setIcon(IconRepository.getIcon("commitquery.png"));
77 commitButton.addActionListener(this);
78 commitButton.setToolTipText("GO!");
79 commitButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
80 commitButton.setVisible(false);
81
82 cancelButton = new JButton();
83 cancelButton.setToolTipText("Cancel");
84 cancelButton.setIcon(IconRepository.getIcon("cancel.png"));
85 cancelButton.addActionListener(this);
86 cancelButton.setPreferredSize(new Dimension(BUTTON_SIZE, BUTTON_SIZE));
87
88
89 statusLabel = new JLabel();
90 statusLabel.setPreferredSize(new Dimension(BUTTON_SIZE * 2, LABEL_HEIGHT));
91 statusLabel.setHorizontalAlignment(JLabel.CENTER);
92 statusLabel.setVerticalAlignment(JLabel.CENTER);
93
94 // Layout GUI
95 GridBagConstraints c;
96
97 JPanel buttonPane = new JPanel(new GridBagLayout());
98 buttonPane.setPreferredSize(new Dimension(BUTTON_SIZE * 2, BUTTON_SIZE));
99
100 c = new GridBagConstraints();
101 c.gridx = 0;
102 c.gridy = 0;
103 c.gridwidth = 2;
104 c.fill = GridBagConstraints.BOTH;
105 buttonPane.add(recordButton, c);
106
107 c = new GridBagConstraints();
108 c.gridx = 0;
109 c.gridy = 0;
110 c.fill = GridBagConstraints.BOTH;
111 buttonPane.add(commitButton, c);
112
113 c = new GridBagConstraints();
114 c.gridx = 1;
115 c.gridy = 0;
116 c.fill = GridBagConstraints.BOTH;
117 buttonPane.add(cancelButton, c);
118
119 // Assemble
120 _swingComponent.add(buttonPane, BorderLayout.CENTER);
121 _swingComponent.add(statusLabel, BorderLayout.SOUTH);
122
123 _swingComponent.doLayout();
124
125 setState(WidgetState.Ready, "Record Query");
126 }
127
128 /**
129 * All of the widget logic is centarlized here - according to the new state transition
130 * Must be in AWT thread
131 *
132 * @param newState
133 */
134 private void setState(WidgetState newState, String status) {
135 if (state == newState) return;
136 WidgetState oldState = state;
137 state = newState;
138
139 statusLabel.setText(status);
140
141 if (newState == WidgetState.Ready) {
142
143 recordButton.setVisible(true);
144 cancelButton.setVisible(false);
145 commitButton.setVisible(false);
146
147 // Ensure that this is observing the record model
148 SampledAudioManager.getInstance().addObserver(this);
149
150 } else if (newState == WidgetState.Recording) {
151 assert(oldState == WidgetState.Ready);
152
153 hasExplicityStopped = false;
154
155 recordButton.setVisible(false);
156 cancelButton.setVisible(true);
157 commitButton.setVisible(true);
158 cancelButton.setEnabled(true);
159 commitButton.setEnabled(true);
160
161 setStatusLabelToRecordTime();
162
163 } else if (newState == WidgetState.Finalizing) { // wait for pipe to finish reading bytes
164
165 // This state can be skipped if pipe has finished before
166 // stop event captured.
167 recordButton.setVisible(false);
168 cancelButton.setVisible(true);
169 commitButton.setVisible(true);
170 cancelButton.setEnabled(false);
171 commitButton.setEnabled(false);
172
173 // Thread reading from pipe will finish and trigger the finished
174
175 } else assert(false);
176
177 }
178
179 private void setStatusLabelToRecordTime() {
180 long elapsed = (System.currentTimeMillis() - recordStartTime);
181 elapsed /= 1000;
182 //statusLabel.setText("Recorded " + elapsed + " seconds");
183 statusLabel.setText("query: " + elapsed + " secs");
184 }
185
186 /**
187 * {@inheritDoc}
188 * SampleRecorderWidget is really stateless - they are means to capture audio only...
189 * That is, temporary.
190 */
191 @Override
192 protected String[] getArgs() {
193 return null;
194 }
195
196 @Override
197 protected void onParentStateChanged(int eventType) {
198 super.onParentStateChanged(eventType);
199
200 switch (eventType) {
201 case ItemParentStateChangedEvent.EVENT_TYPE_HIDDEN:
202 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED:
203 case ItemParentStateChangedEvent.EVENT_TYPE_REMOVED_VIA_OVERLAY:
204
205 if (state == WidgetState.Recording) {
206 // This will change the state of this widget
207 RecordManager.getInstance().stopCapturing();
208 }
209
210 // Ensure that this can be disposed
211 RecordManager.getInstance().removeObserver(this);
212 SampledAudioManager.getInstance().removeObserver(this);
213 break;
214
215 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED:
216 case ItemParentStateChangedEvent.EVENT_TYPE_ADDED_VIA_OVERLAY:
217 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN:
218 case ItemParentStateChangedEvent.EVENT_TYPE_SHOWN_VIA_OVERLAY:
219 RecordManager.getInstance().addObserver(this);
220 MultiTrackPlaybackController.getInstance().addObserver(this);
221 SampledAudioManager.getInstance().addObserver(this);
222 break;
223
224 }
225
226 }
227
228 public Subject getObservedSubject() {
229 return null;
230 }
231
232 public void setObservedSubject(Subject parent) {
233 }
234
235 public void modelChanged(Subject source, SubjectChangedEvent event) { // subject messages
236
237 switch (event.getID()) {
238
239 case ApolloSubjectChangedEvent.CAPTURE_STARTED:
240 if (event.getState() == this) {
241 // Change to recording state
242 recordStartTime = System.currentTimeMillis();
243 setState(WidgetState.Recording, "Recording query");
244 }
245 break;
246
247
248 case ApolloSubjectChangedEvent.CAPTURE_STOPPED:
249
250 if (state == WidgetState.Recording) {
251 setState(WidgetState.Finalizing, "finalizing");
252 }
253 break;
254
255
256 }
257
258 }
259
260 public void actionPerformed(ActionEvent e) { // For action button
261
262
263 if (e.getSource() == commitButton) {
264 assert (state == WidgetState.Recording);
265 hasExplicityStopped = true;
266 RecordManager.getInstance().stopCapturing();
267
268 } else if (e.getSource() == recordButton) {
269 assert (state == WidgetState.Ready);
270
271 try {
272
273 // Start capturing
274 AudioCapturePipe pipe = RecordManager.getInstance().captureAudio(this);
275
276 // Read bytes asynchronously ... buffer and render visual feedback
277 audioByteReader = new RecordedByteReader(pipe);
278 audioByteReader.start();
279
280 } catch (LineUnavailableException ex) {
281 ApolloSystemLog.printException("Failed to commence audio capture via record widget", ex);
282 setState(WidgetState.Ready, "Bad Device");
283 } catch (IOException ex) {
284 ApolloSystemLog.printException("Failed to commence audio capture via record widget", ex);
285 setState(WidgetState.Ready, "Failed");
286 }
287
288 } else if (e.getSource() == cancelButton) {
289 assert (state == WidgetState.Recording);
290 hasExplicityStopped = false; // not needed but to make clear
291 RecordManager.getInstance().stopCapturing();
292 }
293
294
295 }
296
297 /**
298 * Reads bytes asynchronously from a given pipe until it is finished or an exception occurs.
299 * Once finished excecution, the widget state will be set to finished.
300 *
301 * The bytes read are buffered, rendered and sent to the AnimatedSampleGraph for drawing.
302 * The status label is also updated according to the record time.
303 *
304 * @author Brook Novak
305 *
306 */
307 private class RecordedByteReader extends Thread {
308
309 private PipedInputStream pin;
310 private AudioFormat audioFormat;
311 private int bufferSize;
312 public ByteArrayOutputStream bufferedAudioBytes = null; // not null if started
313 private DoUpdateLabel updateLabelTask = new DoUpdateLabel();;
314
315 RecordedByteReader(AudioCapturePipe pipe) {
316 assert(pipe != null);
317 this.pin = pipe.getPin();
318 this.audioFormat = pipe.getAudioFormat();
319 this.bufferSize = pipe.getBufferSize();
320 }
321
322 @Override
323 public void run() {
324
325 bufferedAudioBytes = new ByteArrayOutputStream();
326 byte[] buffer = new byte[bufferSize];
327 int len = 0;
328 int bytesBuffered;
329
330 int lastUpdateStateTime = 0;
331
332 try {
333 while(true) {
334
335 bytesBuffered = 0;
336
337 while (bytesBuffered < buffer.length) { // Full the buffer (unless reaches uneven end)
338 len = pin.read(buffer, bytesBuffered, buffer.length - bytesBuffered);
339 if (len == -1) break; // done
340 bytesBuffered += len;
341 }
342
343 if (bytesBuffered > 0) {
344 // Buffer bytes
345 bufferedAudioBytes.write(buffer, 0, bytesBuffered);
346 }
347
348 if (len == -1) break; // done
349
350 // For every second elapsed, update status
351 if ((System.currentTimeMillis() - lastUpdateStateTime) >= 1000) {
352 SwingUtilities.invokeLater(updateLabelTask);
353 }
354 }
355
356 } catch (IOException e) {
357 e.printStackTrace();
358 } finally {
359
360 try {
361 pin.close();
362 } catch (IOException e) {
363 e.printStackTrace();
364 }
365
366 try {
367 bufferedAudioBytes.close();
368 } catch (IOException e) {
369 e.printStackTrace();
370 }
371
372 // Ensure that will enter finalized state
373 SwingUtilities.invokeLater(new RecoringFinalizer());
374
375 }
376
377 }
378 }
379
380 class RecoringFinalizer implements Runnable {
381
382 public void run() {
383
384 if (hasExplicityStopped) {
385
386 Frame sourceFrame = DisplayIO.getCurrentFrame();
387
388 if (sourceFrame != null &&
389 audioByteReader != null && audioByteReader.bufferedAudioBytes != null
390 && audioByteReader.bufferedAudioBytes.size() > 0) {
391
392 // Create melody search agent - setting raw audio for query data
393 MelodySearch melSearchAgent = new MelodySearch();
394 melSearchAgent.useRawAudio(
395 audioByteReader.bufferedAudioBytes.toByteArray(),
396 audioByteReader.audioFormat);
397
398 Text launcherItem = new Text(sourceFrame.getNextItemID(), "Audio Query");
399
400 // Run the melody search agent using recorded audio
401 Actions.LaunchAgent(
402 melSearchAgent,
403 sourceFrame,
404 launcherItem);
405 }
406
407
408 }
409
410 setState(WidgetState.Ready, "Record Query");
411 }
412 }
413
414 class DoUpdateLabel implements Runnable {
415 public void run() {
416 setStatusLabelToRecordTime();
417 }
418 }
419
420}
Note: See TracBrowser for help on using the repository browser.