There are various ways to produce a sound from samples generated on the fly - this example shows a simple approach using java. sound.sampled.SourceDataLine. It should work with any waveform which is a series of samples you generate programmatically in code with an algorithm, formula, etc...
The first example is straightforward with no user interface. It plays a sine wave of the frequency in fFreq for about 5 seconds, then exits.
The second example builds on the first, adding a simple Swing JSlider that controls the sine wave's frequency though user input.
These samples are written in a simple style intended to be easily understandable. Free feel to reuse this code, although you will probably want to make changes to handle exceptions properly, provide error checking, etc...
Download the Eclipse project for Example 1 - FixFreqSine.zip.
package example.fixedfreqsine; import java.nio.ByteBuffer; import javax.sound.sampled.*; public class FixedFreqSine { //This is just an example - you would want to handle LineUnavailable properly... public static void main(String[] args) throws InterruptedException, LineUnavailableException { final int SAMPLING_RATE = 44100; // Audio sampling rate final int SAMPLE_SIZE = 2; // Audio sample size in bytes SourceDataLine line; double fFreq = 440; // Frequency of sine wave in hz //Position through the sine wave as a percentage (i.e. 0 to 1 is 0 to 2*PI) double fCyclePosition = 0; //Open up audio output, using 44100hz sampling rate, 16 bit samples, mono, and big // endian byte ordering AudioFormat format = new AudioFormat(SAMPLING_RATE, 16, 1, true, true); DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); if (!AudioSystem.isLineSupported(info)){ System.out.println("Line matching " + info + " is not supported."); throw new LineUnavailableException(); } line = (SourceDataLine)AudioSystem.getLine(info); line.open(format); line.start(); // Make our buffer size match audio system's buffer ByteBuffer cBuf = ByteBuffer.allocate(line.getBufferSize()); int ctSamplesTotal = SAMPLING_RATE*5; // Output for roughly 5 seconds //On each pass main loop fills the available free space in the audio buffer //Main loop creates audio samples for sine wave, runs until we tell the thread to exit //Each sample is spaced 1/SAMPLING_RATE apart in time while (ctSamplesTotal>0) { double fCycleInc = fFreq/SAMPLING_RATE; // Fraction of cycle between samples cBuf.clear(); // Discard samples from previous pass // Figure out how many samples we can add int ctSamplesThisPass = line.available()/SAMPLE_SIZE; for (int i=0; i < ctSamplesThisPass; i++) { cBuf.putShort((short)(Short.MAX_VALUE * Math.sin(2*Math.PI * fCyclePosition))); fCyclePosition += fCycleInc; if (fCyclePosition > 1) fCyclePosition -= 1; } //Write sine samples to the line buffer. If the audio buffer is full, this will // block until there is room (we never write more samples than buffer will hold) line.write(cBuf.array(), 0, cBuf.position()); ctSamplesTotal -= ctSamplesThisPass; // Update total number of samples written //Wait until the buffer is at least half empty before we add more while (line.getBufferSize()/2 < line.available()) Thread.sleep(1); } //Done playing the whole waveform, now wait until the queued samples finish //playing, then clean up and exit line.drain(); line.close(); } }
The second example uses the same basic loop structure to generate the audio samples for the sine wave on the fly. This version creates a second thread to generate the samples and keeps refilling the audio output buffer, 100ms worth of sine samples at a time. To keep things all in one easily readable sample file, the thread is implemented as a nested class of the JFrame(SampleThread)
After outputting 100ms of sine wave into the audio buffer, the loop rereads the frequency setting from the JSlider control. You'll hear changes in the slider frequency setting when the next sine buffer is written to the audio output line.
Download the Eclipse project - SliderFreqSine.zip
package example.simplesine; import java.nio.ByteBuffer; import java.awt.*; import javax.swing.*; import javax.sound.sampled.*; public class JFrame_sliderSine extends JFrame { private SampleThread m_thread; private JSlider m_sliderPitch; //Launch the app public static void main(String[] args) { EventQueue.invokeLater(new Runnable() { public void run() { try { JFrame_sliderSine frame = new JFrame_sliderSine(); frame.setVisible(true); } catch (Exception e) { e.printStackTrace(); } } }); } public JFrame_sliderSine() { //UI stuff, created with WindowsBuilder addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { m_thread.exit(); System.exit(0); } }); setTitle("Slider Frequency Sine Wave Demo"); setResizable(false); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setBounds(100, 100, 793, 166); setLocationRelativeTo(null); getContentPane().setLayout(new BorderLayout(0, 0)); m_sliderPitch = new JSlider(); m_sliderPitch.setName(""); m_sliderPitch.setMinimum(100); m_sliderPitch.setPaintLabels(true); m_sliderPitch.setPaintTicks(true); m_sliderPitch.setMajorTickSpacing(500); m_sliderPitch.setMaximum(4100); m_sliderPitch.setValue(880); getContentPane().add(m_sliderPitch); JLabel lblAdjustPitch = new JLabel("Adjust Pitch"); lblAdjustPitch.setHorizontalAlignment(SwingConstants.CENTER); lblAdjustPitch.setFont(new Font("Tahoma", Font.PLAIN, 18)); getContentPane().add(lblAdjustPitch, BorderLayout.NORTH); //Non-UI stuff m_thread = new SampleThread(); m_thread.start(); } class SampleThread extends Thread { final static public int SAMPLING_RATE = 44100; final static public int SAMPLE_SIZE = 2; //Sample size in bytes final static public double BUFFER_DURATION = 0.100; //About a 100ms buffer //You can play with the size of this buffer if you want. Making it smaller speeds up
//the response to the slider movement, but if you make it too small you will get
//noise in your output from buffer underflows, etc...
final static public double BUFFER_DURATION = 0.100; //About a 100ms buffer // Size in bytes of sine wave samples we'll create on each loop pass
final static public int SINE_PACKET_SIZE = (int)(BUFFER_DURATION*SAMPLING_RATE*SAMPLE_SIZE);
SourceDataLine line; public double fFreq; //Set from the pitch slider public boolean bExitThread = false; //Get the number of queued samples in the SourceDataLine buffer private int getLineSampleCount() { return line.getBufferSize() - line.available(); } //Continually fill the audio output buffer whenever it starts to get empty, SINE_PACKET_SIZE/2 //samples at a time, until we tell the thread to exit public void run() { //Position through the sine wave as a percentage (i.e. 0-1 is 0-2*PI) double fCyclePosition = 0; //Open up the audio output, using a sampling rate of 44100hz, 16 bit samples, mono, and big // endian byte ordering. Ask for a buffer size of at least 2*SINE_PACKET_SIZE try { AudioFormat format = new AudioFormat(44100, 16, 1, true, true); DataLine.Info info = new DataLine.Info(SourceDataLine.class, format, SINE_PACKET_SIZE*2); if (!AudioSystem.isLineSupported(info)) throw new LineUnavailableException(); line = (SourceDataLine)AudioSystem.getLine(info); line.open(format); line.start(); } catch (LineUnavailableException e) { System.out.println("Line of that type is not available"); e.printStackTrace(); System.exit(-1); } System.out.println("Requested line buffer size = " + SINE_PACKET_SIZE*2); System.out.println("Actual line buffer size = " + line.getBufferSize()); ByteBuffer cBuf = ByteBuffer.allocate(SINE_PACKET_SIZE); //On each pass main loop fills the available free space in the audio buffer //Main loop creates audio samples for sine wave, runs until we tell the thread to exit //Each sample is spaced 1/SAMPLING_RATE apart in time while (bExitThread==false) { fFreq = m_sliderPitch.getValue(); double fCycleInc = fFreq/SAMPLING_RATE; //Fraction of cycle between samples cBuf.clear(); //Toss out samples from previous pass //Generate SINE_PACKET_SIZE samples based on the current fCycleInc from fFreq for (int i=0; i < SINE_PACKET_SIZE/SAMPLE_SIZE; i++) { cBuf.putShort((short)(Short.MAX_VALUE * Math.sin(2*Math.PI * fCyclePosition))); fCyclePosition += fCycleInc; if (fCyclePosition > 1) fCyclePosition -= 1; } //Write sine samples to the line buffer // If the audio buffer is full, this would block until there is enough room, // but we are not writing unless we know there is enough space. line.write(cBuf.array(), 0, cBuf.position()); //Wait here until there are less than SINE_PACKET_SIZE samples in the buffer //(Buffer size is 2*SINE_PACKET_SIZE at least, so there will be room for // at least SINE_PACKET_SIZE samples when this is true) try { while (getLineSampleCount() > SINE_PACKET_SIZE) Thread.sleep(1); // Give UI a chance to run } catch (InterruptedException e) { // We don't care about this } } line.drain(); line.close(); } public void exit() { bExitThread=true; } } }
Send comments, questions, money in large denominations, etc to android at wolinlabs.com
If you enjoyed this article, please consider buying my products ...
ATX PS Adapter
Use an ATX PC power supply as a 5V, 3.3V, and +12V/-12V bench supply the easy way, without cutting the case or mounting external connectors, resistors, LEDs, switches, and fuses. Provides visual indication when supply is plugged in and turned on, also fuses the power voltage outputs for safety. Run USB powered development boards via the USB connectors on the 5V line. |
Ultimate Serial Port (Debug Buddy)
USB serial port with standard, 5V and 3V RS232, plus integrated null modem and gender changer. Implements TX/RX and RTS#/CTS# for optional hardware handshake. Also includes 3.3V<->5V level shifters, debug LEDs, and 13 clock sources. Valuable tool for hands on problem solving and hacking |