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