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 /** Provides the byte value for this point in the sinusoidal wave. */ 467 private static byte getByteValue(double angle) { 468 int maxVol = 127; 469 return (new Integer( 470 (int)Math.round( 471 Math.sin(angle)*maxVol))). 472 byteValue(); 473 } 474} 475