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?
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.)