// Copyright (c) 2005 Pierre Lewis lew@nortel.com // The copyright must remain with any identifiable parts of this code. // You may not use any of this code for commercial purposes without // authorization. // This is adapted from the JavaTuner applet (that demonstrates // tunings and temperaments) and is intended to illustrate how // I've used the JavaSound classes. // User interface: key presses // 2-9: Play the corresponding major interval (eg. 3 for major third) // m: Play a milliwatt tone // (may be necessary to click in the applet's area to get it to respond // As with any S/W generating sound, some care is called for: don't // run with your speakers at high volume until you know it works! // This is especially true if you start playing with the code. The // muLaw() routine in particular will go seriously non-linear if the // input argument exceeds 1.0 volts. // Overview: // - On startup, the applet pre-computes the waveforms for one cycle // (360 degrees) for both a sine wave and a wave with 5 harmonics. // This is done to speed up computations later. // - The applet then waits for user input. Here, it's a key press. // When the users asks for something to be played, freq[] is set to // the one or two frequencies requested. // - The sound thread discovers that freq[] is non-null, converts the // frequencies to degrees (of the cycle) per sample (at 8 kHz), // then computes a 6000-byte audio sample, and finally submits it to // the audio player. Computation is done by interpolating the wave // computed at initialization (faster than doing complex computations // at this stage). // - Beware: only 2 notes in this example (for more notes, see invocation // of the harmonics() method). Also, since we are using up to the // 5th harmonic, no freq[] value should exceed 800 Hz. To go higher, // one would simply set the higher harmonics to 0.0 in the call to // harmonics(). // There are still a few things not fully cleaned up (wrt AWT). // Also, this applet still uses the old JDK1.0 event model. // It can also run stand-alone import java.awt.*; import java.io.*; import java.applet.*; import javax.sound.sampled.*; // The Roule class contains the following methods: // // method thread purpose // // init AWT/browser initialization // harmonics AWT/browser pre-computes one cycle of waveform // paint AWT/browser paint the applet // start AWT/browser start the applet // stop AWT/browser stop the applet // destroy AWT/browser destroy the applet // doPlay soundThread prepare sample if something to play, start it // run soundThread soundThread's main loop // handleEvent AWT/browser user input and other AWT events // main main main in stand-alone mode public class RouleJS extends Applet implements Runnable { // Pre-computed cycle with harmonics and as pure sine wave double [] harmwave; // waveform with harmonics double [] sinewave; static int rate = 8000; // other values possible, eg. 8000, 11025, 22050 int lgt = 6000; // basic sample length (3/4 seconds) byte [] sample = null; // the PCM samples // Thread used to prepare the samples private Thread soundThread; // Communication area between AWT/browser and soundThread threads Object audioLock; private double freq[]; AudioFormat pcm; static boolean bigendian = false; // SourceDataLine klang; // or superclass DataLine ? Clip klang; // or superclass DataLine ? public void init() { setLayout(null); // initialize some objects audioLock = new Object(); sample = new byte[lgt*2]; freq = null; // define one cycle of waveforms harmwave = new double[361]; sinewave = new double[361]; harmonics(2, .91, .83, .75, .67); } // Initialize one cycle (to speed up computations later) void harmonics(int voices, double second, double third, double fourth, double fifth) { int k; double max = 0.0; for (k = 0; k < 361; ++k) { harmwave[k] = ( 1.0 * Math.sin (k * 2. * Math.PI / 360.) + second * Math.sin (k * 4. * Math.PI / 360.) + third * Math.sin (k * 6. * Math.PI / 360.) + fourth * Math.sin (k * 8. * Math.PI / 360.) + fifth * Math.sin (k * 10. * Math.PI / 360.) ); if (harmwave[k] > max) max = harmwave[k]; else if (-harmwave[k] > max) max = -harmwave[k]; // for the milliwatt tone (single note!) sinewave[k] = 0.707 * Math.sin (k * 2. * Math.PI / 360.); } // allow up to number_of_notes simultaneous notes with harmwave[] for (k = 0; k < 361; ++k) harmwave[k] *= ((0.707/voices)/max); // eg. 0.35 for 2 voices } public void paint(Graphics g) { g.drawString("Type 2-9 for interval", 15-6, 10+6); g.drawString("Type m for milliwatt tone", 15-6, 25+6); g.drawString("Version 2.0 ", 15-6, 40+6); super.paint(g); } public void start() { System.out.println("start()"); for (int tries = 0; tries < 2; ++tries) { // CAF: try a few, at different rates pcm = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, rate, 16, 1, 2, rate, bigendian); DataLine.Info info = new DataLine.Info( Clip.class, pcm); // no buffer size -- will that be OK try { klang = (Clip) AudioSystem.getLine(info); System.out.println("Audio DataLine started"); // hack to work around JavaSound bug 4515126. byte [] sampleTest = new byte[2]; sampleTest[0] = 0; sampleTest[1] = 1; klang.open(pcm, sampleTest, 0, sampleTest.length); klang.close(); if (sampleTest[1] == 0) // byte swapping occured in JavaSound { bigendian = ! bigendian; System.out.println("Bigendian=" + bigendian + " (swapped after test)"); continue; // try a second time } break; } catch (Exception ex) { System.out.println("Audio exception: " + ex); klang = null; } } // try // { // FloatControl vol = (FloatControl)klang.getControl(FloatControl.Type.VOLUME); // vol.setValue(vol.getMaximum()); // } // catch (Exception ex) // { // // oh well, I tried... // System.out.println("Couldn't set volume: " + ex); // } if (soundThread == null) { soundThread = new Thread(this); soundThread.start(); System.out.println("Sound thread started"); } } public void stop() { System.out.println("stop()"); if (klang != null && klang.isOpen()) try { klang.stop(); klang.flush(); klang.close(); } catch (Exception ex) { System.out.println("Audio exception: " + ex); klang = null; } klang = null; if (soundThread != null) { soundThread.stop(); System.out.println("Sound thread stopped"); soundThread = null; } } public void destroy() { System.out.println("dispose()"); // "destroy" is a strong word :-) if (soundThread != null) { soundThread.stop(); System.out.println("Sound thread stopped"); soundThread = null; } } // This method, running under the sound thread, recomputes the PCM // sample whenever there is something to play (freq != null). // This following may seem to be a bit round-about, but it was done // thus for performance reasons: limit computations as much as possible // in the main loop doing the preparation of the sample, and esp. avoid // there any complex functions. boolean doPlay() { double degreespersample []; synchronized (audioLock) { if (freq != null) { degreespersample = new double [freq.length]; for (int k = 0; k < freq.length; ++k) { degreespersample[k] = (360. / 8000.) * freq[k]; } freq = null; } else degreespersample = null; } Thread.yield(); if (degreespersample != null) { // prepare u-law and start int cnt = degreespersample.length; for (int k = 0; k < lgt; ++k) { double volt = 0.0; if (cnt > 1) // two or more notes { for (int j = 0; j < cnt; ++j) // for each note { double deg = degreespersample[j] * k; int degi = (int) deg; double degm = deg - (double)degi; // using the wave with 5 harmonics volt += harmwave[degi%360] + // linear interpolate degm * (harmwave[(degi%360)+1] - harmwave[degi%360]); } } else // must be milliwatt tone (single note) { double deg = degreespersample[0] * k; int degi = (int) deg; double degm = deg - (double)degi; volt += sinewave[degi%360] + // linear interpolate degm * (sinewave[(degi%360)+1] - sinewave[degi%360]); } // sample[k] = muLaw(volt); short val = (short)(volt * 32767.); if (bigendian) { sample[2*k] = (byte)(val >> 8); sample[2*k+1] = (byte)(val & 0xff); } else { sample[2*k+1] = (byte)(val >> 8); sample[2*k] = (byte)(val & 0xff); } } // Now start playing! if (klang != null) try { if (klang.isOpen()) { klang.stop(); klang.flush(); klang.close(); } // klang.write(sample, 0, sample.length); klang.open(pcm, sample, 0, sample.length); klang.start(); } catch (Exception ex) { System.out.println("Audio exception: " + ex); } degreespersample = null; return true; } return false; } // soundThread's main: check regularly if there is something to play // by calling doPlay(). public void run() { try { for (;;) { if (doPlay()) { Thread.sleep(800); // duration of a normal play } else { Thread.sleep(50); // be responsive... } } } catch (Exception e) { System.out.println("Sound thread exception: " + e); } } // Handle user input. public boolean handleEvent(Event e) { switch (e.id) { case Event.WINDOW_DESTROY: System.out.println("Event.WINDOW_DESTROY"); if (soundThread != null) { soundThread.stop(); System.out.println("Sound thread stopped"); soundThread = null; } System.exit(0); case Event.KEY_PRESS: if (e.key == 'm') // milliwatt { synchronized (audioLock) { freq = new double[1]; freq[0] = 1004.; } } else if (e.key >= '2' && e.key <= '9') // interval { int cents; switch (e.key) { case '2': cents = 200; break; case '3': cents = 400; break; case '4': cents = 500; break; case '5': cents = 700; break; case '6': cents = 900; break; case '7': cents =1100; break; case '8': cents =1200; break; case '9': cents =1400; break; default: cents = 600; break; } synchronized (audioLock) { freq = new double[2]; freq[0] = 220.; freq[1] = 220. * Math.pow(2.0, cents/1200.); } } return true; default: // System.out.println("Event: " + e.id); return getParent().handleEvent(e); } } // Needed when running in stand-alone mode public static void main(String args[]) { Frame f = new Frame("RouleJS"); RouleJS accord = new RouleJS(); accord.init(); accord.start(); if (args.length != 0) try { //AudioPlayer.player.setPriority(Thread.MAX_PRIORITY); } catch (Exception e) { System.out.println("AudioPlayer.*.setPriority: " + e); } f.add("Center", accord); // same size as in applet tag -- why do I need to make bigger? f.resize(300+10, 90+30); f.show(); } }