Search code examples
javaaudiojavax.sound.sampled

Generated sound played with SouceDataLine is fuzzy


I am trying to generate a set of simultaneous tones in realtime. But all of the sounds the program produces are "fuzzy", or have "static", or even sound like "squealing" in the background. This is especially noticeable in lower pitched sounds. Here is the code:

static final long bufferLength = 44100;
static final AudioFormat af = new AudioFormat(bufferLength, 8, 1, true, false);
static boolean go = true; //to be changed somewhere else

static void startSound(double[] hertz) {
    if (hertz.length == 0) {return;}
    try {
        SourceDataLine sdl = AudioSystem.getSourceDataLine(af);
        sdl.open();
        sdl.start();
        int i = 0;
        //iterate as long as the sound must play
        do {
            //create a new buffer
            double[] buf = new double[128]; //arbitrary number
            final int startI = i;
            //iterate through each of the tones
            for (int k = 0; k < hertz.length; k++) {
                i = startI;
                //iterate through each of the samples for this buffer
                for (int j = 0; j < buf.length; j++) {
                    double x = (double)i/bufferLength*hertz[k]*2*Math.PI;
                    double wave1 = Math.sin(x);
                    //decrease volume with increasing pitch
                    double volume = Math.min(Math.max(300 - hertz[k], 50d), 126d);
                    buf[j] += wave1*volume;
                    i++;
                    if (i == 9999999) { //prevent i from getting too big
                        i = 0;
                    }
                }
            }

            final byte[] finalBuffer = new byte[buf.length];
            //copy the double buffer into byte buffer
            for (int j = 0; j < buf.length; j++) {
                //divide by hertz.length to prevent simultaneous sounds
                //    from becoming too loud
                finalBuffer[j] = (byte)(buf[j]/hertz.length);
            }

            //play the sound
            sdl.write(finalBuffer, 0, finalBuffer.length);
        } while (go);
        sdl.flush();
        sdl.stop();
    } catch (LineUnavailableException e) {
        e.printStackTrace();
    }
}

//play some deep example tones
startSound(new double[]{65.4064, 58.2705, 48.9995});

I've tried recording the sound being output from this program, and the waves do seem slightly jagged. But when I print out the generated waves directly from the program, they seem perfectly smooth. The sound I generate just doesn't seem to match up with the sound coming out of the speakers. Can anyone catch what I'm doing wrong?


Solution

  • Per my comment, I think you are hearing quantization error because of the 8-bit audio and you should switch to 16-bit. Quantization error is sometimes referred to as noise but is a type of square harmonic distortion and is the source of the subtle overtones you are hearing.

    8-bit is sometimes acceptable for things like speech where it will sound more like noise. The distortion is more noticeable with pure tones.

    I turned your code in to a rough MCVE to demonstrate the difference.

    class SoundTest {
        static final int bufferLength = 44100;
        static final AudioFormat af8 = new AudioFormat(bufferLength, 8, 1, true, false);
        static final AudioFormat af16 = new AudioFormat(bufferLength, 16, 1, true, false);
        static volatile boolean go = true; //to be changed somewhere else
    
        static void startSound8(double[] hertz) {
            if (hertz.length == 0) {return;}
            try {
                SourceDataLine sdl = AudioSystem.getSourceDataLine(af8);
                sdl.open();
                sdl.start();
                int i = 0;
                //iterate as long as the sound must play
                do {
                    //create a new buffer
                    double[] buf = new double[128]; //arbitrary number
                    final int startI = i;
                    //iterate through each of the tones
                    for (int k = 0; k < hertz.length; k++) {
                        i = startI;
                        //iterate through each of the samples for this buffer
                        for (int j = 0; j < buf.length; j++) {
                            double x = (double)i/bufferLength*hertz[k]*2*Math.PI;
                            double wave1 = Math.sin(x);
                            //decrease volume with increasing pitch
    //                        double volume = Math.min(Math.max(300 - hertz[k], 50d), 126d);
                            double volume = 64;
                            buf[j] += wave1*volume;
                            i++;
                            if (i == 9999999) { //prevent i from getting too big
                                i = 0;
                            }
                        }
                    }
    
                    final byte[] finalBuffer = new byte[buf.length];
                    //copy the double buffer into byte buffer
                    for (int j = 0; j < buf.length; j++) {
                        //divide by hertz.length to prevent simultaneous sounds
                        //    from becoming too loud
                        finalBuffer[j] = (byte)(buf[j]/hertz.length);
                    }
    
                    //play the sound
                    sdl.write(finalBuffer, 0, finalBuffer.length);
                } while (go);
                sdl.flush();
                sdl.stop();
                synchronized (SoundTest.class) {
                    SoundTest.class.notifyAll();
                }
            } catch (LineUnavailableException e) {
                e.printStackTrace();
            }
        }
    
        static void startSound16(double[] hertz) {
            if (hertz.length == 0) {return;}
            try {
                SourceDataLine sdl = AudioSystem.getSourceDataLine(af16);
                sdl.open();
                sdl.start();
                int i = 0;
                //iterate as long as the sound must play
                do {
                    //create a new buffer
                    double[] buf = new double[128]; //arbitrary number
                    final int startI = i;
                    //iterate through each of the tones
                    for (int k = 0; k < hertz.length; k++) {
                        i = startI;
                        //iterate through each of the samples for this buffer
                        for (int j = 0; j < buf.length; j++) {
                            double x = (double)i/bufferLength*hertz[k]*2*Math.PI;
                            double wave1 = Math.sin(x);
                            //decrease volume with increasing pitch
                            // double volume = Math.min(Math.max(300 - hertz[k], 50d), 126d);
                            double volume = 16384;
                            buf[j] += wave1*volume;
                            i++;
                            if (i == 9999999) { //prevent i from getting too big
                                i = 0;
                            }
                        }
                    }
    
                    final byte[] finalBuffer = new byte[buf.length * 2];
    
                    //copy the double buffer into byte buffer
                    for (int j = 0; j < buf.length; j++) {
                        //divide by hertz.length to prevent simultaneous sounds
                        //    from becoming too loud
    
                        int a = (int) (buf[j] / hertz.length);
                        finalBuffer[j * 2] = (byte) a;
                        finalBuffer[(j * 2) + 1] = (byte) (a >>> 8);
                    }
    
                    //play the sound
                    sdl.write(finalBuffer, 0, finalBuffer.length);
                } while (go);
                sdl.flush();
                sdl.stop();
                synchronized (SoundTest.class) {
                    SoundTest.class.notifyAll();
                }
            } catch (LineUnavailableException e) {
                e.printStackTrace();
            }
        }
    
        static void playTone(final double hz, final boolean fewBits) {
            go = true;
            new Thread() {
                @Override
                public void run() {
                    if (fewBits) {
                        startSound8(new double[] {hz});
                    } else {
                        startSound16(new double[] {hz});
                    }
                }
            }.start();
            try {
                Thread.sleep(5000);
            } catch (InterruptedException x) {
                x.printStackTrace();
            } finally {
                go = false;
                synchronized (SoundTest.class) {
                    try {
                        SoundTest.class.wait();
                    } catch (InterruptedException x) {
                        x.printStackTrace();
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            playTone(220, true);
            playTone(220, false);
        }
    }
    

    I discuss concepts for the bit operations I used to pack the 16-bit byte array here and there's example code.

    Also worth mentioning that if a professional application needed to use 8-bit for some reason, it would probably add dither before quantizing which sounds better than pure quantization error. (Same thing for 16-bit, for that matter, but quantization error at 16-bit is inaudible unless it's been accumulated.)