Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to stream sound in java without delay using SourceDataLine

Tags:

java

delay

audio

I want to generate sounds based on user's action in Java. Even if I set the buffer size in SourceDataLine to the smallest possible value (1 frame) I still have delay of about 1 second.

Because a code snippet is worth a thousand words (or was it a picture?), here is the code:

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;
import javax.swing.JFrame;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class SoundTest {

    private static int sliderValue = 500;

    public static void main(String[] args) throws Exception {
        final JFrame frame = new JFrame();
        final JSlider slider = new JSlider(500, 1000);
        frame.add(slider);
        slider.addChangeListener(new ChangeListener() {
            @Override
            public void stateChanged(ChangeEvent e) {
                sliderValue = slider.getValue();
            }
        });
        frame.pack();
        frame.setVisible(true);

        final AudioFormat audioFormat = new AudioFormat(44100, 8, 1, true, true);
        final DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, 1);
        final SourceDataLine soundLine = (SourceDataLine) AudioSystem.getLine(info);
        soundLine.open(audioFormat);
        soundLine.start();
        byte counter = 0;
        final byte[] buffer = new byte[1];
        byte sign = 1;
        while (frame.isVisible()) {
            if (counter > audioFormat.getFrameRate() / sliderValue) {
                sign = (byte) -sign;
                counter = 0;
            }
            buffer[0] = (byte) (sign * 30);
            soundLine.write(buffer, 0, 1);
            counter++;
        }
    }
}

Try moving the slider while listening to the sound. Is it possible, or do I have to create in-memory buffers and wrap them in Clip instances?

like image 472
andi Avatar asked Aug 08 '11 18:08

andi


1 Answers

The fix is to specify the buffer size in the open(AudioFormat,int) method. A delay of 10ms-100ms will be acceptable for realtime audio. Very low latencies like will not work on all computer systems, and 100ms or more will probably be annoying for your users. A good tradeoff is, e.g. 50ms. For your audio format, 8-bit, mono at 44100Hz, a good buffer size is 2200 bytes, which is almost 50ms.

Also note that different OS's have different audio capabilities in Java. On Windows and Linux you can work with quite small buffer sizes, but OS X uses an old implementation with significantly larger delay.

Also, writing data byte by byte to the SourceDataLine is very inefficient (the buffer size is set in the open() method, not in write()), as a rule of thumb I'd always write one full buffer size to the SourceDataLine.

After setting up the SourceDataLine, use this code:

final int bufferSize = 2200; // in Bytes
soundLine.open(audioFormat, bufferSize);
soundLine.start();
byte counter = 0;
final byte[] buffer = new byte[bufferSize];
byte sign = 1;
while (frame.isVisible()) {
    int threshold = audioFormat.getFrameRate() / sliderValue;
    for (int i = 0; i < bufferSize; i++) {
        if (counter > threshold) {
            sign = (byte) -sign;
            counter = 0;
        }
        buffer[i] = (byte) (sign * 30);
        counter++;
    }
    // the next call is blocking until the entire buffer is 
    // sent to the SourceDataLine
    soundLine.write(buffer, 0, bufferSize);
}
like image 79
Florian Avatar answered Nov 05 '22 22:11

Florian