2

I am trying to write a simple application which plays sound and can alter the volume of that sound at any time during playing. I am doing this by converting each pair of bytes in the byte array of the sound into an int, then multiplying that int by increase or decrease in volume and then writing them back as two bytes (i.e. 1 sample). However, this results in extreme distortion in the sound. Is it possible that I have got the bit shifting wrong? My sound format is:

.wav 44100.0hz, 16bit, little-endian

At the moment the byte array that I pass the adjustVolume method represents a 10th of a second of audio data. i.e. sampleRate/10

Is there something I am missing here that is causing it to distort and not scale volume properly? Have I got the writing of bytes back and fort wrong?

private byte[] adjustVolume(byte[] audioSamples, double volume) {
        byte[] array = new byte[audioSamples.length];
        for (int i = 0; i < array.length; i += 2) {
            // convert byte pair to int
            int audioSample = (int) (((audioSamples[i + 1] & 0xff) << 8) | (audioSamples[i] & 0xff));


            audioSample = (int) (audioSample * volume);


            // convert back
            array[i] = (byte) audioSample;
            array[i + 1] = (byte) (audioSample >> 16);

        }
        return array;
    }

This code is based off of: Audio: Change Volume of samples in byte array in which the asker is trying to do the same thing. However, having used the code from his question (which I think was not updated after he got his answer) I can't get it to work and I am not exactly sure what it is doing.

Community
  • 1
  • 1
user2457072
  • 173
  • 2
  • 8

1 Answers1

2

I suggest you wrap your byte array in a ByteBuffer (not forgetting to set its .order() to little endian), read a short, manipulate it, write it again.

Sample code:

// Necessary in order to convert negative shorts!
private static final int USHORT_MASK = (1 << 16) - 1;

final ByteBuffer buf = ByteBuffer.wrap(audioSamples)
    .order(ByteOrder.LITTLE_ENDIAN);
final ByteBuffer newBuf = ByteBuffer.allocate(audioSamples.length)
    .order(ByteOrder.LITTLE_ENDIAN);

int sample;

while (buf.hasRemaining()) {
    sample = (int) buf.getShort() & USHORT_MASK;
    sample *= volume;
    newBuf.putShort((short) (sample & USHORT_MASK));
}

return newBuf.array();
fge
  • 119,121
  • 33
  • 254
  • 329
  • What do you mean by reposition? – user2457072 Jun 06 '13 at 11:32
  • Sorry, see edit, I only noticed afterwards that you returned a _new_ array. – fge Jun 06 '13 at 11:37
  • This only seems to produce silence. And I don't think byteBuffer actually has a method called .readShort() - I replaced this with getShort() but what are you actually trying to do on this line. Is there a method that could replace this? – user2457072 Jun 06 '13 at 12:30
  • Uh yes, that is `.getShort()`, sorry. What I do is what you did in your original code: read a 16bit sample, multiply it, put the multiplied result in the new array. But I only adapted your code, I may be doing something wrong. In fact I don't know the wav format at all ;) – fge Jun 06 '13 at 12:32
  • I'm assuming this is java.nio.bytebuffer as well. – user2457072 Jun 06 '13 at 12:34
  • Ok! I'll have a go at getting it to work and report back. It's such a shame that there isn't an easier way to do this in java. – user2457072 Jun 06 '13 at 12:35
  • There was a glaring error... Please see edit. And yes, that is java.nio.ByteBuffer. – fge Jun 06 '13 at 12:36
  • The glaring error being that it should be bit shifted by 8 and not 16 right? It seems to work now. Well. I can fade out effectively. Sub question: when I wrap up the .jar with my audio files do you think they will still play effectively when I move it to a mac as the wave files on macs use big endians? – user2457072 Jun 06 '13 at 17:53
  • That was the error, yes. As to Mac, it is doubtful that it matters. There are several wav formats, I can't believe Mac OS X cannot play little endian ones... In any case, glad that it seems to work! It was mostly guess work from the code you provided ;) – fge Jun 06 '13 at 18:01
  • Although now I see you've edited it! What is the "s" in the line: sample = (int) s & USHORT_MASK; referring to? As it isn't defined in the method body. Thanks again! :) – user2457072 Jun 06 '13 at 18:43
  • Argh, it's missing a line :/ The first instruction in the `while` loop should be `sample = (int) buf.readShort() & USHORT_MASK;` – fge Jun 06 '13 at 18:44
  • @fge Why do you need the mask? This data is representing a sine wave, therefore, we want to multiply the negative and positive portions of it. Why do you need a mask? – Code Doggo Nov 13 '17 at 03:36
  • @DanHoynoski this is because of how right shifting for integer primitive types work in Java. If you read a `short` whose value is, for instance, exactly -2^15 (that is, 1000 0000), right shifting it by 1, for instance, would give 1100 0000, but we want 0100 0000. It is therefore necessary to use an int instead and do a logical and mask with 1111 1111 to avoid that. – fge Nov 13 '17 at 08:29