/Users/lyon/j4p/src/sound/player/AudioSynth01.java
|
1 package sound.player;
2
3 /**
4 * Created by IntelliJ IDEA.
5 * User: Douglas Lyon
6 * Date: Dec 13, 2004
7 * Time: 8:09:16 PM
8 * Copyright DocJava, Inc.
9 */
10
11 import javax.swing.*;
12 import java.awt.*;
13 import java.awt.event.*;
14 import javax.sound.sampled.*;
15 import java.io.*;
16 import java.nio.channels.*;
17 import java.nio.*;
18 import java.util.*;
19
20 public class AudioSynth01 extends JFrame {
21
22 //The following are general instance variables
23 // used to create a SourceDataLine object.
24 AudioFormat audioFormat;
25 AudioInputStream audioInputStream;
26 SourceDataLine sourceDataLine;
27
28 //The following are audio format parameters.
29 // They may be modified by the signal generator
30 // at runtime. Values allowed by Java
31 // SDK 1.4.1 are shown in comments.
32 float sampleRate = 16000.0F;
33 //Allowable 8000,11025,16000,22050,44100
34 int sampleSizeInBits = 16;
35 //Allowable 8,16
36 int channels = 1;
37 //Allowable 1,2
38 boolean signed = true;
39 //Allowable true,false
40 boolean bigEndian = true;
41 //Allowable true,false
42
43 //A buffer to hold two seconds monaural and one
44 // second stereo data at 16000 samp/sec for
45 // 16-bit samples
46 byte audioData[] = new byte[16000 * 4];
47
48 //Following components appear in the North
49 // position of the GUI.
50 final JButton generateBtn =
51 new JButton("Generate");
52 final JButton playOrFileBtn =
53 new JButton("Play/File");
54 final JLabel elapsedTimeMeter =
55 new JLabel("0000");
56
57 //Following radio buttons select a synthetic
58 // data type. Add more buttons if you add
59 // more synthetic data types. They appear in
60 // the center position of the GUI.
61 final JRadioButton tones =
62 new JRadioButton("Tones", true);
63 final JRadioButton stereoPanning =
64 new JRadioButton("Stereo Panning");
65 final JRadioButton stereoPingpong =
66 new JRadioButton("Stereo Pingpong");
67 final JRadioButton fmSweep =
68 new JRadioButton("FM Sweep");
69 final JRadioButton decayPulse =
70 new JRadioButton("Decay Pulse");
71 final JRadioButton echoPulse =
72 new JRadioButton("Echo Pulse");
73 final JRadioButton waWaPulse =
74 new JRadioButton("WaWa Pulse");
75
76 //Following components appear in the South
77 // position of the GUI.
78 final JRadioButton listen =
79 new JRadioButton("Listen", true);
80 final JRadioButton file =
81 new JRadioButton("File");
82 final JTextField fileName =
83 new JTextField("junk", 10);
84
85 //-------------------------------------------//
86 public static void main(String args[]) {
87 new AudioSynth01();
88 }//end main
89 //-------------------------------------------//
90
91 public AudioSynth01() {//constructor
92 //A panel for the North position. Note the
93 // etched border.
94 final JPanel controlButtonPanel =
95 new JPanel();
96 controlButtonPanel.setBorder(BorderFactory.createEtchedBorder());
97
98 //A panel and button group for the radio
99 // buttons in the Center position.
100 final JPanel synButtonPanel = new JPanel();
101 final ButtonGroup synButtonGroup =
102 new ButtonGroup();
103 //This panel is used for cosmetic purposes
104 // only, to cause the radio buttons to be
105 // centered horizontally in the Center
106 // position.
107 final JPanel centerPanel = new JPanel();
108
109 //A panel for the South position. Note the
110 // etched border.
111 final JPanel outputButtonPanel =
112 new JPanel();
113 outputButtonPanel.setBorder(BorderFactory.createEtchedBorder());
114 final ButtonGroup outputButtonGroup =
115 new ButtonGroup();
116
117 //Disable the Play button initially to force
118 // the user to generate some data before
119 // trying to listen to it or write it to a
120 // file.
121 playOrFileBtn.setEnabled(false);
122
123 //Register anonymous listeners on the
124 // Generate button and the Play/File button.
125 generateBtn.addActionListener(new ActionListener() {
126 public void actionPerformed(ActionEvent e) {
127 //Don't allow Play during generation
128 playOrFileBtn.setEnabled(false);
129 //Generate synthetic data
130 new SynGen().getSyntheticData(audioData);
131 //Now it is OK for the user to listen
132 // to or file the synthetic audio data.
133 playOrFileBtn.setEnabled(true);
134 }//end actionPerformed
135 }//end ActionListener
136 );//end addActionListener()
137
138 playOrFileBtn.addActionListener(new ActionListener() {
139 public void actionPerformed(ActionEvent e) {
140 //Play or file the data synthetic data
141 playOrFileData();
142 }//end actionPerformed
143 }//end ActionListener
144 );//end addActionListener()
145
146 //Add two buttons and a text field to a
147 // physical group in the North of the GUI.
148 controlButtonPanel.add(generateBtn);
149 controlButtonPanel.add(playOrFileBtn);
150 controlButtonPanel.add(elapsedTimeMeter);
151
152 //Add radio buttons to a mutually exclusive
153 // group in the Center of the GUI. Make
154 // additions here if you add new synthetic
155 // generator methods.
156 synButtonGroup.add(tones);
157 synButtonGroup.add(stereoPanning);
158 synButtonGroup.add(stereoPingpong);
159 synButtonGroup.add(fmSweep);
160 synButtonGroup.add(decayPulse);
161 synButtonGroup.add(echoPulse);
162 synButtonGroup.add(waWaPulse);
163
164 //Add radio buttons to a physical group and
165 // center it in the Center of the GUI. Make
166 // additions here if you add new synthetic
167 // generator methods.
168 synButtonPanel.setLayout(new GridLayout(0, 1));
169 synButtonPanel.add(tones);
170 synButtonPanel.add(stereoPanning);
171 synButtonPanel.add(stereoPingpong);
172 synButtonPanel.add(fmSweep);
173 synButtonPanel.add(decayPulse);
174 synButtonPanel.add(echoPulse);
175 synButtonPanel.add(waWaPulse);
176
177 //Note that the centerPanel has center
178 // alignment by default.
179 centerPanel.add(synButtonPanel);
180
181 //Add radio buttons to a mutually exclusive
182 // group in the South of the GUI.
183 outputButtonGroup.add(listen);
184 outputButtonGroup.add(file);
185
186 //Add radio buttons to a physical group in
187 // the South of the GUI.
188 outputButtonPanel.add(listen);
189 outputButtonPanel.add(file);
190 outputButtonPanel.add(fileName);
191
192 //Add the panels containing components to the
193 // content pane of the GUI in the appropriate
194 // positions.
195 getContentPane().add(controlButtonPanel, BorderLayout.NORTH);
196 getContentPane().add(centerPanel,
197 BorderLayout.CENTER);
198 getContentPane().add(outputButtonPanel,
199 BorderLayout.SOUTH);
200
201 //Finish the GUI. If you add more radio
202 // buttons in the center, you may need to
203 // modify the call to setSize to increase
204 // the vertical component of the GUI size.
205 setTitle("Copyright 2003, R.G.Baldwin");
206 setDefaultCloseOperation(EXIT_ON_CLOSE);
207 setSize(250, 275);
208 setVisible(true);
209 }//end constructor
210 //-------------------------------------------//
211
212 //This method plays or files the synthetic
213 // audio data that has been generated and saved
214 // in an array in memory.
215 private void playOrFileData() {
216 try {
217 //Get an input stream on the byte array
218 // containing the data
219 InputStream byteArrayInputStream =
220 new ByteArrayInputStream(audioData);
221
222 //Get the required audio format
223 audioFormat = new AudioFormat(sampleRate,
224 sampleSizeInBits,
225 channels,
226 signed,
227 bigEndian);
228
229 //Get an audio input stream from the
230 // ByteArrayInputStream
231 audioInputStream = new AudioInputStream(byteArrayInputStream,
232 audioFormat,
233 audioData.length / audioFormat.
234 getFrameSize());
235
236 //Get info on the required data line
237 DataLine.Info dataLineInfo =
238 new DataLine.Info(SourceDataLine.class,
239 audioFormat);
240
241 //Get a SourceDataLine object
242 sourceDataLine = (SourceDataLine)
243 AudioSystem.getLine(dataLineInfo);
244 //Decide whether to play the synthetic
245 // data immediately, or to write it into
246 // an audio file, based on the user
247 // selection of the radio buttons in the
248 // South of the GUI..
249 if (listen.isSelected()) {
250 //Create a thread to play back the data and
251 // start it running. It will run until all
252 // the data has been played back
253 new ListenThread().start();
254 } else {
255 //Disable buttons until existing data
256 // is written to the file.
257 generateBtn.setEnabled(false);
258 playOrFileBtn.setEnabled(false);
259
260 //Write the data to an output file with
261 // the name provided by the text field
262 // in the South of the GUI.
263 try {
264 AudioSystem.write(audioInputStream,
265 AudioFileFormat.Type.AU,
266 new File(fileName.getText() +
267 ".au"));
268 } catch (Exception e) {
269 e.printStackTrace();
270 System.exit(0);
271 }//end catch
272 //Enable buttons for another operation
273 generateBtn.setEnabled(true);
274 playOrFileBtn.setEnabled(true);
275 }//end else
276 } catch (Exception e) {
277 e.printStackTrace();
278 System.exit(0);
279 }//end catch
280 }//end playOrFileData
281 //=============================================//
282
283 //Inner class to play back the data that was
284 // saved.
285 class ListenThread extends Thread {
286 //This is a working buffer used to transfer
287 // the data between the AudioInputStream and
288 // the SourceDataLine. The size is rather
289 // arbitrary.
290 byte playBuffer[] = new byte[16384];
291
292 public void run() {
293 try {
294 //Disable buttons while data is being
295 // played.
296 generateBtn.setEnabled(false);
297 playOrFileBtn.setEnabled(false);
298
299 //Open and start the SourceDataLine
300 sourceDataLine.open(audioFormat);
301 sourceDataLine.start();
302
303 int cnt;
304 //Get beginning of elapsed time for
305 // playback
306 long startTime = new Date().getTime();
307
308 //Transfer the audio data to the speakers
309 while ((cnt = audioInputStream.read(playBuffer, 0,
310 playBuffer.length))
311 != -1) {
312 //Keep looping until the input read
313 // method returns -1 for empty stream.
314 if (cnt > 0) {
315 //Write data to the internal buffer of
316 // the data line where it will be
317 // delivered to the speakers in real
318 // time
319 sourceDataLine.write(playBuffer, 0, cnt);
320 }//end if
321 }//end while
322
323 //Block and wait for internal buffer of the
324 // SourceDataLine to become empty.
325 sourceDataLine.drain();
326
327
328 //Get and display the elapsed time for
329 // the previous playback.
330 int elapsedTime =
331 (int) (new Date().getTime() - startTime);
332 elapsedTimeMeter.setText("" + elapsedTime);
333
334 //Finish with the SourceDataLine
335 sourceDataLine.stop();
336 sourceDataLine.close();
337
338 //Re-enable buttons for another operation
339 generateBtn.setEnabled(true);
340 playOrFileBtn.setEnabled(true);
341 } catch (Exception e) {
342 e.printStackTrace();
343 System.exit(0);
344 }//end catch
345
346 }//end run
347 }//end inner class ListenThread
348 //=============================================//
349
350 //Inner signal generator class.
351
352 //An object of this class can be used to
353 // generate a variety of different synthetic
354 // audio signals. Each time the getSyntheticData
355 // method is called on an object of this class,
356 // the method will fill the incoming array with
357 // the samples for a synthetic signal.
358 class SynGen {
359 //Note: Because this class uses a ByteBuffer
360 // asShortBuffer to handle the data, it can
361 // only be used to generate signed 16-bit
362 // data.
363 ByteBuffer byteBuffer;
364 ShortBuffer shortBuffer;
365 int byteLength;
366
367 void getSyntheticData(byte[] synDataBuffer) {
368 //Prepare the ByteBuffer and the shortBuffer
369 // for use
370 byteBuffer = ByteBuffer.wrap(synDataBuffer);
371 shortBuffer = byteBuffer.asShortBuffer();
372
373 byteLength = synDataBuffer.length;
374
375 //Decide which synthetic data generator
376 // method to invoke based on which radio
377 // button the user selected in the Center of
378 // the GUI. If you add more methods for
379 // other synthetic data types, you need to
380 // add corresponding radio buttons to the
381 // GUI and add statements here to test the
382 // new radio buttons. Make additions here
383 // if you add new synthetic generator
384 // methods.
385
386 if (tones.isSelected()) tones();
387 if (stereoPanning.isSelected())
388 stereoPanning();
389 if (stereoPingpong.isSelected())
390 stereoPingpong();
391 if (fmSweep.isSelected()) fmSweep();
392 if (decayPulse.isSelected()) decayPulse();
393 if (echoPulse.isSelected()) echoPulse();
394 if (waWaPulse.isSelected()) waWaPulse();
395
396 }//end getSyntheticData method
397 //-------------------------------------------//
398
399 //This method generates a monaural tone
400 // consisting of the sum of three sinusoids.
401 void tones() {
402 channels = 1;//Java allows 1 or 2
403 //Each channel requires two 8-bit bytes per
404 // 16-bit sample.
405 int bytesPerSamp = 2;
406 sampleRate = 16000.0F;
407 // Allowable 8000,11025,16000,22050,44100
408 int sampLength = byteLength / bytesPerSamp;
409 for (int cnt = 0; cnt < sampLength; cnt++) {
410 double time = cnt / sampleRate;
411 double freq = 950.0;//arbitrary frequency
412 double sinValue =
413 (Math.sin(2 * Math.PI * freq * time) +
414 Math.sin(2 * Math.PI * (freq / 1.8) * time) +
415 Math.sin(2 * Math.PI * (freq / 1.5) * time)) / 3.0;
416 shortBuffer.put((short) (16000 * sinValue));
417 }//end for loop
418 }//end method tones
419 //-------------------------------------------//
420
421 //This method generates a stereo speaker sweep,
422 // starting with a relatively high frequency
423 // tone on the left speaker and moving across
424 // to a lower frequency tone on the right
425 // speaker.
426 void stereoPanning() {
427 channels = 2;//Java allows 1 or 2
428 int bytesPerSamp = 4;//Based on channels
429 sampleRate = 16000.0F;
430 // Allowable 8000,11025,16000,22050,44100
431 int sampLength = byteLength / bytesPerSamp;
432 for (int cnt = 0; cnt < sampLength; cnt++) {
433 //Calculate time-varying gain for each
434 // speaker
435 double rightGain = 16000.0 * cnt / sampLength;
436 double leftGain = 16000.0 - rightGain;
437
438 double time = cnt / sampleRate;
439 double freq = 600;//An arbitrary frequency
440 //Generate data for left speaker
441 double sinValue =
442 Math.sin(2 * Math.PI * (freq) * time);
443 shortBuffer.put((short) (leftGain * sinValue));
444 //Generate data for right speaker
445 sinValue =
446 Math.sin(2 * Math.PI * (freq * 0.8) * time);
447 shortBuffer.put((short) (rightGain * sinValue));
448 }//end for loop
449 }//end method stereoPanning
450 //-------------------------------------------//
451
452 //This method uses stereo to switch a sound
453 // back and forth between the left and right
454 // speakers at a rate of about eight switches
455 // per second. On my system, this is a much
456 // better demonstration of the sound separation
457 // between the two speakers than is the
458 // demonstration produced by the stereoPanning
459 // method. Note also that because the sounds
460 // are at different frequencies, the sound
461 // produced is similar to that of U.S.
462 // emergency vehicles.
463
464 void stereoPingpong() {
465 channels = 2;//Java allows 1 or 2
466 int bytesPerSamp = 4;//Based on channels
467 sampleRate = 16000.0F;
468 // Allowable 8000,11025,16000,22050,44100
469 int sampLength = byteLength / bytesPerSamp;
470 double leftGain = 0.0;
471 double rightGain = 16000.0;
472 for (int cnt = 0; cnt < sampLength; cnt++) {
473 //Calculate time-varying gain for each
474 // speaker
475 if (cnt % (sampLength / 8) == 0) {
476 //swap gain values
477 double temp = leftGain;
478 leftGain = rightGain;
479 rightGain = temp;
480 }//end if
481
482 double time = cnt / sampleRate;
483 double freq = 600;//An arbitrary frequency
484 //Generate data for left speaker
485 double sinValue =
486 Math.sin(2 * Math.PI * (freq) * time);
487 shortBuffer.put((short) (leftGain * sinValue));
488 //Generate data for right speaker
489 sinValue =
490 Math.sin(2 * Math.PI * (freq * 0.8) * time);
491 shortBuffer.put((short) (rightGain * sinValue));
492 }//end for loop
493 }//end stereoPingpong method
494 //-------------------------------------------//
495
496 //This method generates a monaural linear
497 // frequency sweep from 100 Hz to 1000Hz.
498 void fmSweep() {
499 channels = 1;//Java allows 1 or 2
500 int bytesPerSamp = 2;//Based on channels
501 sampleRate = 16000.0F;
502 // Allowable 8000,11025,16000,22050,44100
503 int sampLength = byteLength / bytesPerSamp;
504 double lowFreq = 100.0;
505 double highFreq = 1000.0;
506
507 for (int cnt = 0; cnt < sampLength; cnt++) {
508 double time = cnt / sampleRate;
509
510 double freq = lowFreq +
511 cnt * (highFreq - lowFreq) / sampLength;
512 double sinValue =
513 Math.sin(2 * Math.PI * freq * time);
514 shortBuffer.put((short) (16000 * sinValue));
515 }//end for loop
516 }//end method fmSweep
517 //-------------------------------------------//
518
519 //This method generates a monaural triple-
520 // frequency pulse that decays in a linear
521 // fashion with time.
522 void decayPulse() {
523 channels = 1;//Java allows 1 or 2
524 int bytesPerSamp = 2;//Based on channels
525 sampleRate = 16000.0F;
526 // Allowable 8000,11025,16000,22050,44100
527 int sampLength = byteLength / bytesPerSamp;
528 for (int cnt = 0; cnt < sampLength; cnt++) {
529 //The value of scale controls the rate of
530 // decay - large scale, fast decay.
531 double scale = 2 * cnt;
532 if (scale > sampLength) scale = sampLength;
533 double gain =
534 16000 * (sampLength - scale) / sampLength;
535 double time = cnt / sampleRate;
536 double freq = 499.0;//an arbitrary freq
537 double sinValue =
538 (Math.sin(2 * Math.PI * freq * time) +
539 Math.sin(2 * Math.PI * (freq / 1.8) * time) +
540 Math.sin(2 * Math.PI * (freq / 1.5) * time)) / 3.0;
541 shortBuffer.put((short) (gain * sinValue));
542 }//end for loop
543 }//end method decayPulse
544 //-------------------------------------------//
545
546 //This method generates a monaural triple-
547 // frequency pulse that decays in a linear
548 // fashion with time. However, three echoes
549 // can be heard over time with the amplitude
550 // of the echoes also decreasing with time.
551 void echoPulse() {
552 channels = 1;//Java allows 1 or 2
553 int bytesPerSamp = 2;//Based on channels
554 sampleRate = 16000.0F;
555 // Allowable 8000,11025,16000,22050,44100
556 int sampLength = byteLength / bytesPerSamp;
557 int cnt2 = -8000;
558 int cnt3 = -16000;
559 int cnt4 = -24000;
560 for (int cnt1 = 0; cnt1 < sampLength;
561 cnt1++, cnt2++, cnt3++, cnt4++) {
562 double val = echoPulseHelper(cnt1, sampLength);
563 if (cnt2 > 0) {
564 val += 0.7 * echoPulseHelper(cnt2, sampLength);
565 }//end if
566 if (cnt3 > 0) {
567 val += 0.49 * echoPulseHelper(cnt3, sampLength);
568 }//end if
569 if (cnt4 > 0) {
570 val += 0.34 * echoPulseHelper(cnt4, sampLength);
571 }//end if
572
573 shortBuffer.put((short) val);
574 }//end for loop
575 }//end method echoPulse
576 //-------------------------------------------//
577
578 double echoPulseHelper(int cnt, int sampLength) {
579 //The value of scale controls the rate of
580 // decay - large scale, fast decay.
581 double scale = 2 * cnt;
582 if (scale > sampLength) scale = sampLength;
583 double gain =
584 16000 * (sampLength - scale) / sampLength;
585 double time = cnt / sampleRate;
586 double freq = 499.0;//an arbitrary freq
587 double sinValue =
588 (Math.sin(2 * Math.PI * freq * time) +
589 Math.sin(2 * Math.PI * (freq / 1.8) * time) +
590 Math.sin(2 * Math.PI * (freq / 1.5) * time)) / 3.0;
591 return (short) (gain * sinValue);
592 }//end echoPulseHelper
593
594 //-------------------------------------------//
595
596 //This method generates a monaural triple-
597 // frequency pulse that decays in a linear
598 // fashion with time. However, three echoes
599 // can be heard over time with the amplitude
600 // of the echoes also decreasing with time.
601 //Note that this method is identical to the
602 // method named echoPulse, except that the
603 // algebraic sign was switched on the amplitude
604 // of two of the echoes before adding them to
605 // the composite synthetic signal. This
606 // resulted in a difference in the
607 // sound.
608 void waWaPulse() {
609 channels = 1;//Java allows 1 or 2
610 int bytesPerSamp = 2;//Based on channels
611 sampleRate = 16000.0F;
612 // Allowable 8000,11025,16000,22050,44100
613 int sampLength = byteLength / bytesPerSamp;
614 int cnt2 = -8000;
615 int cnt3 = -16000;
616 int cnt4 = -24000;
617 for (int cnt1 = 0; cnt1 < sampLength;
618 cnt1++, cnt2++, cnt3++, cnt4++) {
619 double val = waWaPulseHelper(cnt1, sampLength);
620 if (cnt2 > 0) {
621 val += -0.7 * waWaPulseHelper(cnt2, sampLength);
622 }//end if
623 if (cnt3 > 0) {
624 val += 0.49 * waWaPulseHelper(cnt3, sampLength);
625 }//end if
626 if (cnt4 > 0) {
627 val += -0.34 * waWaPulseHelper(cnt4, sampLength);
628 }//end if
629
630 shortBuffer.put((short) val);
631 }//end for loop
632 }//end method waWaPulse
633 //-------------------------------------------//
634
635 double waWaPulseHelper(int cnt, int sampLength) {
636 //The value of scale controls the rate of
637 // decay - large scale, fast decay.
638 double scale = 2 * cnt;
639 if (scale > sampLength) scale = sampLength;
640 double gain =
641 16000 * (sampLength - scale) / sampLength;
642 double time = cnt / sampleRate;
643 double freq = 499.0;//an arbitrary freq
644 double sinValue =
645 (Math.sin(2 * Math.PI * freq * time) +
646 Math.sin(2 * Math.PI * (freq / 1.8) * time) +
647 Math.sin(2 * Math.PI * (freq / 1.5) * time)) / 3.0;
648 return (short) (gain * sinValue);
649 }//end waWaPulseHelper
650
651 //-------------------------------------------//
652 }//end SynGen class
653 //=============================================//
654
655 }//end outer class AudioSynth01.java
656