3

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?

user1661303
  • 539
  • 1
  • 7
  • 20
  • 3
    If my simple search for this API was any good, then I don’t think choosing audio player is a well maintained option that is future proof. I might be wrong though. – Michiel Leegwater Sep 01 '19 at 10:00
  • Are you looking for a desktop or an Android solution? – Hendrik Sep 01 '19 at 10:08
  • 1
    Do not use `sun.audio` classes - they are not part of the official API and don't work at all in modern versions of Java. Look at the `javax.sound.sampled` classes. The [tag:javasound] tag info has lots of links to examples. – greg-449 Sep 01 '19 at 10:14
  • 3
    Generally, don't use the `sun` classes. – Zoe Sep 01 '19 at 10:22
  • @hendrik a desktop solution. It would be fine if it could be done with any of the standard java/kotlin classes (and I don't really see why it couldn't be since it's one of the most basic things you could ask a computer to do), but I guess an embedded c/c++ file or .dll library would work too. – user1661303 Sep 01 '19 at 11:01
  • Further to the comment of @greg-449: See also the [info page for Java Sound](https://stackoverflow.com/tags/javasound/info). I added an example using a Java Sound based `Clip`. – Andrew Thompson Sep 01 '19 at 11:13

1 Answers1

1

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)
    }
}
Hendrik
  • 5,085
  • 24
  • 56