I am looking for a way to generate and play back sound in Kotlin/Java. I have been searching a lot and tried different solutions, but it's not really satisfactory. I'm not looking for the Java Control class, that lets me add reverb to existing sounds, or the javax.sound.midi package that lets me do MIDI sequencing. Instead, I want to build up sound from scratch as a sound vector/List, through something like this:
fun createSinWaveBuffer(freq: Double, ms: Int, sampleRate: Int = 44100): ByteArray {
val samples = (ms * sampleRate / 1000)
val output = ByteArray(samples)
val period = sampleRate.toDouble() / freq
for (i in output.indices) {
val angle = 2.0 * Math.PI * i.toDouble() / period
output[i] = (Math.sin(angle) * 127f).toByte()
}
//output.forEach { println(it) }
return output
}
I then want to play back the sound, and have the actual output of the speakers match the input parameters sent to the function with regards to frequency, length etc. Of course, creating two sound vectors like this with different frequencies should summing them together or at least taking the average should result in two tones playing simultanously.
This is simple enough in matlab, if you have a vector y, like this
t=0:1/samplerate:duration;
y=sin(2*pi*freq*t);
Just do
sound(y,sampleRate)
Although there might not be as simple or clean solution in Java, I still feel like it should be possible to play custom sound.
After searching around a bit here and on other places, this is one of the cleanest solutions I'm trying now (even though it uses sun.audio, the other suggestions were way messier):
import sun.audio.AudioPlayer
import sun.audio.AudioDataStream
import sun.audio.AudioData
private fun playsound(sound: ByteArray) {
val audiodata = AudioData(sound)
val audioStream = AudioDataStream(audiodata)
AudioPlayer.player.start(audioStream)
}
but playsound(createSinWaveBuffer(440.0, 10000, 44100)) does not sound right in my speakers. It sounds choppy, it is not at 440 hz, it is not a pure sine wave and it is not ten seconds. What am I missing?
First of all, do not use sun packages. Ever.
For the desktop, the way to go is to generate the data, acquire a SourceDataLine, open and start the line and then write your data to it. It's important that the line is suitable for the AudioFormat you have chosen to generate. In this case, 8 bits/sample and a sample rate of 44,100 Hz.
Here's a working example in Java, which I am sure you can easily translate to Kotlin.
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
public class ClipDemo {
private static byte[] createSinWaveBuffer(final float freq, final int ms, final float sampleRate) {
final int samples = (int)(ms * sampleRate / 1000);
final byte[] output = new byte[samples];
final float period = sampleRate / freq;
for (int i=0; i<samples; i++) {
final float angle = (float)(2f * Math.PI * i / period);
output[i] = (byte) (Math.sin(angle) * 127f);
}
return output;
}
public static void main(String[] args) throws LineUnavailableException {
final int rate = 44100;
final byte[] sineBuffer = createSinWaveBuffer(440, 5000, rate);
// describe the audio format you're using.
// because its byte-based, it's 8 bit/sample and signed
// if you use 2 bytes for one sample (CD quality), you need to pay more attention
// to endianess and data encoding in your byte buffer
final AudioFormat format = new AudioFormat(rate, 8, 1, true, true);
final SourceDataLine line = AudioSystem.getSourceDataLine(format);
// open the physical line, acquire system resources
line.open(format);
// start the line (... to your speaker)
line.start();
// write to the line (... to your speaker)
// this call blocks.
line.write(sineBuffer, 0, sineBuffer.length);
// cleanup, i.e. close the line again (left out in this example)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With