001package bradleyross.sound;
002
003import java.awt.*;
004import java.awt.event.*;
005import javax.swing.*;
006import javax.swing.event.*;
007import javax.swing.border.*;
008import java.text.DecimalFormat;
009import java.io.IOException;
010//import javax.sound.sampled.*;
011import javax.sound.sampled.AudioSystem;
012import javax.sound.sampled.AudioFormat;
013import javax.sound.sampled.AudioInputStream;
014import javax.sound.sampled.Clip;
015import javax.sound.sampled.FloatControl;
016import javax.sound.sampled.LineUnavailableException;
017import javax.sound.sampled.UnsupportedAudioFileException;
018
019import java.io.ByteArrayInputStream;
020/** Beeper presents a small, loopable tone that can be heard
021by clicking on the Code Key.  It uses a Clip to loop the sound,
022as well as for access to the Clip's gain control.
023 * <p> See
024 *    <a href="http://stackoverflow.com/questions/7782721/java-raw-audio-output/7782749#7782749"
025 *    target="_blank">
026 *    http://stackoverflow.com/questions/7782721/java-raw-audio-output/7782749#7782749
027 *    </a>
028 *    </p>
029 * <p>This runs when triggered  from the command line.  It may or may not run when triggered
030 *    from within Eclipse.</p>
031 * <p>It appears that this was based on an older library that did not contain clip.getControl.</p>
032 * @author Andrew Thompson
033 * @version 2009-12-19
034 * <p>license LGPL</p> 
035 * @see javax.sound.sampled.AudioSystem
036 * @see AudioFormat
037 * @see AudioInputStream
038 * 
039 * @see LineUnavailableException
040 * @see UnsupportedAudioFileException
041 */
042@SuppressWarnings("serial")
043public class Beeper extends JApplet {
044
045        BeeperPanel bp;
046
047        public void init() {
048                bp = new BeeperPanel();
049                getContentPane().add(bp);
050                validate();
051
052                String sampleRate = getParameter("samplerate");
053                if (sampleRate!=null) {
054                        try {
055                                int sR = Integer.parseInt(sampleRate);
056                                bp.setSampleRate(sR);
057                        } catch(NumberFormatException useDefault) {
058                        }
059                }
060
061                String fpw = getParameter("fpw");
062                if (fpw!=null) {
063                        try {
064                                int fPW = Integer.parseInt(fpw);
065                                JSlider slider = bp.getFramesPerWavelengthSlider();
066                                slider.setValue( fPW );
067                        } catch(NumberFormatException useDefault) {
068                        }
069                }
070
071                boolean harmonic = (getParameter("addharmonic")!=null);
072                bp.setAddHarmonic(harmonic);
073
074                bp.setUpSound();
075
076                if ( getParameter("autoloop")!=null ) {
077                        String loopcount = getParameter("loopcount");
078                        if (loopcount!=null) {
079                                try {
080                                        Integer lC = Integer.parseInt(loopcount);
081                                        bp.loop( lC.intValue() );
082                                } catch(NumberFormatException doNotLoop) {
083                                }
084                        }
085                }
086        }
087
088        public void stop() {
089                bp.loopSound(false);
090        }
091
092        public static void main(String[] args) {
093                SwingUtilities.invokeLater(new Runnable() {
094                        public void run() {
095                                JFrame f = new JFrame("Beeper");
096                                f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
097                                BeeperPanel BeeperPanel = new BeeperPanel();
098                                f.setContentPane(BeeperPanel);
099                                f.pack();
100                                f.setMinimumSize( f.getSize() );
101                                f.setLocationByPlatform(true);
102                                f.setVisible(true);
103                        }
104                });
105        }
106}
107
108/** The main UI of Beeper. */
109@SuppressWarnings("serial")
110class BeeperPanel extends JPanel {
111
112        JComboBox<Integer> sampleRate;
113        JSlider framesPerWavelength;
114        JLabel frequency;
115        JCheckBox harmonic;
116        Clip clip;
117
118        DecimalFormat decimalFormat = new DecimalFormat("###00.00");
119        /**
120         * Build panel for controlling tone generation.
121         * 
122         * It is not safe to set the volume on the audio device here 
123         * because many systems only only the controls to be used when
124         * the line is open.
125         * 
126         * @see FloatControl
127         */
128        BeeperPanel() {
129                super(new BorderLayout());
130                // Use current OS look and feel.
131                try {
132                        UIManager.setLookAndFeel(
133                                        UIManager.getSystemLookAndFeelClassName());
134                        SwingUtilities.updateComponentTreeUI(this);
135                } catch (Exception e) {
136                        e.printStackTrace();
137                }
138                setPreferredSize( new Dimension(300,300) );
139
140                JPanel options = new JPanel();
141                BoxLayout bl = new BoxLayout(options,BoxLayout.Y_AXIS);
142                options.setLayout(bl);
143
144                Integer[] rates = {
145                                new Integer(8000),
146                                new Integer(11025),
147                                new Integer(16000),
148                                new Integer(22050)
149                };
150                sampleRate = new JComboBox<Integer>(rates);
151                sampleRate.setToolTipText("Samples per second");
152                sampleRate.setSelectedIndex(1);
153                JPanel pSampleRate = new JPanel(new BorderLayout());
154                pSampleRate.setBorder(new TitledBorder("Sample Rate"));
155                pSampleRate.add( sampleRate );
156                sampleRate.addActionListener(new ActionListener() {
157                        public void actionPerformed(ActionEvent ae) {
158                                setUpSound();
159                        }
160                });
161                options.add( pSampleRate );
162
163                framesPerWavelength = new JSlider(JSlider.HORIZONTAL,10,200,25);
164                framesPerWavelength.setPaintTicks(true);
165                framesPerWavelength.setMajorTickSpacing(10);
166                framesPerWavelength.setMinorTickSpacing(5);
167                framesPerWavelength.setToolTipText("Frames per Wavelength");
168                framesPerWavelength.addChangeListener( new ChangeListener(){
169                        public void stateChanged(ChangeEvent ce) {
170                                setUpSound();
171                        }
172                } );
173
174                JPanel pFPW = new JPanel( new BorderLayout() );
175                pFPW.setBorder(new TitledBorder("Frames per Wavelength"));
176
177                pFPW.add( framesPerWavelength );
178                options.add( pFPW );
179
180                JPanel bottomOption = new JPanel( new BorderLayout(4,4) );
181                harmonic = new JCheckBox("Add Harmonic", false);
182                harmonic.setToolTipText(
183                                "Add harmonic to second channel, one octave up");
184                harmonic.addActionListener( new ActionListener(){
185                        public void actionPerformed(ActionEvent ae) {
186                                setUpSound();
187                        }
188                } );
189                bottomOption.add( harmonic, BorderLayout.WEST );
190
191                frequency = new JLabel();
192                bottomOption.add( frequency, BorderLayout.CENTER );
193
194                options.add(bottomOption);
195
196                add( options, BorderLayout.NORTH );
197
198                JPanel play = new JPanel(new BorderLayout(3,3));
199                play.setBorder( new EmptyBorder(4,4,4,4) );
200                JButton bPlay  = new JButton("Code Key");
201                bPlay.setToolTipText("Click to make tone!");
202                Dimension preferredSize = bPlay.getPreferredSize();
203                bPlay.setPreferredSize( new Dimension(
204                                (int)preferredSize.getWidth(),
205                                (int)preferredSize.getHeight()*3) );
206
207                // TODO comment out to try KeyListener!
208                //bPlay.setFocusable(false);
209                bPlay.addKeyListener( new KeyAdapter(){
210                        @Override
211                        public void keyPressed(KeyEvent ke) {
212                                loopSound(true);
213                        }
214                } );
215                bPlay.addMouseListener( new MouseAdapter() {
216                        @Override
217                        public void mousePressed(MouseEvent me) {
218                                loopSound(true);
219                        }
220
221                        @Override
222                        public void mouseReleased(MouseEvent me) {
223                                loopSound(false);
224                        }
225                } );
226                play.add( bPlay );
227                /*
228                 * There are multiple types of controls that can be supported 
229                 * here.  Two of them are VOLUME and MASTER_GAIN.  None, one,
230                 * or both of these may be supported by a given audio line.
231                 * 
232                 * The code previously did not check for support and assumed 
233                 * support of MASTER_GAIN.  I changed the code to check
234                 * for support of  both MASTER_GAIN and VOLUME.
235                 */
236                try {
237                                clip = AudioSystem.getClip();
238
239                } catch (LineUnavailableException e) {
240                        e.printStackTrace();
241                        clip = null;
242                }
243                if ( clip != null && clip.isControlSupported(FloatControl.Type.MASTER_GAIN))
244                {
245                        try {
246                
247                                final FloatControl control = (FloatControl)
248                                                clip.getControl( FloatControl.Type.MASTER_GAIN );
249
250
251                                final JSlider volume = new JSlider(
252                                                JSlider.VERTICAL,
253                                                (int)control.getMinimum(),
254                                                (int)control.getMaximum(),
255                                                (int)control.getValue()
256                                                );
257                                volume.setToolTipText("Volume of beep");
258                                volume.addChangeListener( new ChangeListener(){
259                                        public void stateChanged(ChangeEvent ce) {
260                                                control.setValue( volume.getValue() );
261                                        }
262                                } );
263                                play.add( volume, BorderLayout.EAST );
264                        } catch(Exception e) {
265                                e.printStackTrace();
266                        }
267                } else {
268                        System.out.println("MASTER_GAIN not available");
269                }
270                /*
271                 * Controls not supported at this point because clip is not open.
272                 * This apparently varies between systems.
273                 */
274
275                if (clip != null && clip.isControlSupported(FloatControl.Type.VOLUME))
276                {
277                        try {
278                                clip = AudioSystem.getClip();
279                                final FloatControl control = (FloatControl)
280                                                clip.getControl( FloatControl.Type.VOLUME);
281
282
283                                final JSlider volume = new JSlider(
284                                                JSlider.VERTICAL,
285                                                (int)control.getMinimum(),
286                                                (int)control.getMaximum(),
287                                                (int)control.getValue()
288                                                );
289                                volume.setToolTipText("Volume of beep");
290                                volume.addChangeListener( new ChangeListener(){
291                                        public void stateChanged(ChangeEvent ce) {
292                                                control.setValue( volume.getValue() );
293                                        }
294                                } );
295                                play.add( volume, BorderLayout.EAST );
296                        } catch(Exception e) {
297                                e.printStackTrace();
298                        }
299                }       else {
300                        System.out.println("VOLUME not available");
301                }
302                if (clip != null && clip.isControlSupported(FloatControl.Type.AUX_RETURN)) {
303                        System.out.println("AUX_RETURN control is supported");
304                } else {
305                        System.out.println("AUX_RETURN control is not supported");
306                }
307                if (clip != null && clip.isControlSupported(FloatControl.Type.AUX_SEND)) {
308                        System.out.println("AUX_SEND control is supported");
309                } else {
310                        System.out.println("AUX_SEND control is not supported");
311                }
312                if (clip != null && clip.isControlSupported(FloatControl.Type.BALANCE)) {
313                        System.out.println("BALANCE control is supported");
314                } else {
315                        System.out.println("BALANCE control is not supported");
316                }
317                add(play, BorderLayout.CENTER);
318
319                setUpSound();
320        }
321
322        public void loop(int loopcount) {
323                if (clip!=null) {
324                        clip.loop( loopcount );
325                }
326        }
327
328        public void setAddHarmonic(boolean addHarmonic) {
329                harmonic.setSelected(addHarmonic);
330        }
331
332        /** Provides the slider for determining the # of frames per wavelength,
333    primarily to allow easy adjustment by host classes. */
334        public JSlider getFramesPerWavelengthSlider() {
335                return framesPerWavelength;
336        }
337
338        /** Sets the sample rate to one of the four
339    allowable rates. Is ignored otherwise. */
340        public void setSampleRate(int sR) {
341                switch (sR) {
342                case 8000:
343                        sampleRate.setSelectedIndex(0);
344                        break;
345                case 11025:
346                        sampleRate.setSelectedIndex(1);
347                        break;
348                case 16000:
349                        sampleRate.setSelectedIndex(2);
350                        break;
351                case 22050:
352                        sampleRate.setSelectedIndex(3);
353                        break;
354                default:
355                }
356        }
357
358        /** Sets label to current frequency settings. */
359        public void setFrequencyLabel() {
360                float freq = getFrequency();
361                if (harmonic.isSelected()) {
362                        frequency.setText(
363                                        decimalFormat.format(freq) +
364                                        "(/" +
365                                        decimalFormat.format(freq*2f) +
366                                        ") Hz" );
367                } else {
368                        frequency.setText( decimalFormat.format(freq) + " Hz" );
369                }
370        }
371
372        /** Generate the tone and inform the user of settings. */
373        public void setUpSound() {
374                try {
375                        generateTone();
376                        setFrequencyLabel();
377                } catch(Exception e) {
378                        e.printStackTrace();
379                }
380        }
381
382        /** Provides the frequency at current settings for
383    sample rate & frames per wavelength. */
384        public float getFrequency() {
385                Integer sR = (Integer)sampleRate.getSelectedItem();
386                int intST = sR.intValue();
387                int intFPW = framesPerWavelength.getValue();
388
389                return (float)intST/(float)intFPW;
390        }
391
392        /** Loops the current Clip until a commence false is passed. */
393        public void loopSound(boolean commence) {
394                if ( commence ) {
395                        clip.setFramePosition(0);
396                        clip.loop( Clip.LOOP_CONTINUOUSLY );
397                } else {
398                        clip.stop();
399                }
400        }
401
402        /** Generates a tone, and assigns it to the Clip. 
403         * @see AudioSystem
404         */
405        public void generateTone()
406                        throws LineUnavailableException {
407                if ( clip!=null ) {
408                        clip.stop();
409                        clip.close();
410                } else {
411                        clip = AudioSystem.getClip();
412                }
413                boolean addHarmonic = harmonic.isSelected();
414
415                int intSR = ((Integer)sampleRate.getSelectedItem()).intValue();
416                int intFPW = framesPerWavelength.getValue();
417
418                float sampleRate = (float)intSR;
419
420                // oddly, the sound does not loop well for less than
421                // around 5 or so, wavelengths
422                int wavelengths = 20;
423                byte[] buf = new byte[2*intFPW*wavelengths];
424                AudioFormat af = new AudioFormat(
425                                sampleRate,
426                                8,  // sample size in bits
427                                2,  // channels
428                                true,  // signed
429                                false  // bigendian
430                                );
431
432                // int maxVol = 127;
433                for(int i=0; i<intFPW*wavelengths; i++){
434                        double angle = ((float)(i*2)/((float)intFPW))*(Math.PI);
435                        buf[i*2]=getByteValue(angle);
436                        if(addHarmonic) {
437                                buf[(i*2)+1]=getByteValue(2*angle);
438                        } else {
439                                buf[(i*2)+1] = buf[i*2];
440                        }
441                }
442
443                try {
444                        byte[] b = buf;
445                        AudioInputStream ais = new AudioInputStream(
446                                        new ByteArrayInputStream(b),
447                                        af,
448                                        buf.length/2 );
449
450                        clip.open( ais );
451                        System.out.println("Clip opened in generateTone");
452                        System.out.println("VOLUME: " + clip.isControlSupported(FloatControl.Type.VOLUME));
453                        System.out.println("MASTER_GAIN: " + clip.isControlSupported(FloatControl.Type.MASTER_GAIN));
454                        if (clip.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
455                                FloatControl gain =  (FloatControl) clip.getControl(FloatControl.Type.MASTER_GAIN);
456                                System.out.println("Min. Gain: " + Float.toString(gain.getMinimum()));
457                                System.out.println("Max Gain: " + Float.toString(gain.getMaximum()));
458                        }
459                        System.out.println("AUX_RETURN: " + clip.isControlSupported(FloatControl.Type.AUX_RETURN));
460                        System.out.println("AUX_SEND: " + clip.isControlSupported(FloatControl.Type.AUX_SEND));
461                } catch(IOException e) {
462                        e.printStackTrace();
463                }
464        }
465
466        /** 
467         * Provides the byte value for this point in the sinusoidal wave. 
468         */
469        private static byte getByteValue(double angle) {
470                int maxVol = 127;
471                return (new Integer(
472                                (int)Math.round(
473                                                Math.sin(angle)*maxVol))).
474                                                byteValue();
475        }
476}
477