Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Audio: Change Volume of samples in byte array

I'm reading a wav-file to a byte array using this method (shown below). Now that I have it stored inside my byte array, I want to change the sounds volume.

private byte[] getAudioFileData(final String filePath) {
    byte[] data = null;
    try {
    final ByteArrayOutputStream baout = new ByteArrayOutputStream();
    final File file = new File(filePath);
    final AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);

    byte[] buffer = new byte[4096];
    int c;
    while ((c = audioInputStream.read(buffer, 0, buffer.length)) != -1) {
        baout.write(buffer, 0, c);
    }
    audioInputStream.close();
    baout.close();
    data = baout.toByteArray();
    } catch (Exception e) {
    e.printStackTrace();
    }
    return data;
}

Edit: Per request some info on the audio format:

PCM_SIGNED 44100.0 Hz, 16 bit, mono, 2 bytes/frame, little-endian

From physics-class I remembered that you can change the amplitude of a sine-wave by multiplying the sine-value with a number between 0 and 1.

Edit: Updated code for 16-bit samples:

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 >> 8);

    }
    return array;
}

The sound is heavily distorted if I multiply audioSample with volume. If I don't and compare both arrays with Arrays.compare(array, audioSample) I can conclude that the byte-array is being converted correctly to int and the other way around.

Can anybody help me out? What am I getting wrong here? Thank you! :)

like image 514
Macks Avatar asked Jan 23 '13 17:01

Macks


3 Answers

Problem in int type, size of int in java is 4 bytes and the sample size is 2 bytes

This worked code:

private byte[] adjustVolume(byte[] audioSamples, float volume) {
        byte[] array = new byte[audioSamples.length];
        for (int i = 0; i < array.length; i+=2) {
            // convert byte pair to int
            short buf1 = audioSamples[i+1];
            short buf2 = audioSamples[i];

            buf1 = (short) ((buf1 & 0xff) << 8);
            buf2 = (short) (buf2 & 0xff);

            short res= (short) (buf1 | buf2);
            res = (short) (res * volume);

            // convert back
            array[i] = (byte) res;
            array[i+1] = (byte) (res >> 8);

        }
        return array;
}
like image 180
Rodion Avatar answered Nov 07 '22 06:11

Rodion


Are you sure you're reading 8-bit mono audio? Otherwise one byte does not equal one sample, and you cannot just scale each byte. E.g. if it is 16-bit data you have to parse every pair of bytes as a 16-bit integer, scale that, and then write it back as two bytes.

like image 8
johusman Avatar answered Nov 07 '22 06:11

johusman


The answer by Rodion was a good starting point, but it not sufficient to give good results.

It introduced overflows and was not fast enough for real-time audio on Android.

TL;DR: My improved solution involving a LUT and gain compression

private static int N_SHORTS = 0xffff;
private static final short[] VOLUME_NORM_LUT = new short[N_SHORTS];
private static int MAX_NEGATIVE_AMPLITUDE = 0x8000;

static {
    precomputeVolumeNormLUT();
}    

private static void normalizeVolume(byte[] audioSamples, int start, int len) {
    for (int i = start; i < start+len; i+=2) {
        // convert byte pair to int
        short s1 = audioSamples[i+1];
        short s2 = audioSamples[i];

        s1 = (short) ((s1 & 0xff) << 8);
        s2 = (short) (s2 & 0xff);

        short res = (short) (s1 | s2);

        res = VOLUME_NORM_LUT[res+MAX_NEGATIVE_AMPLITUDE];
        audioSamples[i] = (byte) res;
        audioSamples[i+1] = (byte) (res >> 8);
    }
}

private static void precomputeVolumeNormLUT() {
    for(int s=0; s<N_SHORTS; s++) {
        double v = s-MAX_NEGATIVE_AMPLITUDE;
        double sign = Math.signum(v);
        // Non-linear volume boost function
        // fitted exponential through (0,0), (10000, 25000), (32767, 32767)
        VOLUME_NORM_LUT[s]=(short)(sign*(1.240769e-22 - (-4.66022/0.0001408133)*
                           (1 - Math.exp(-0.0001408133*v*sign))));
    }
}

This works very well, boosts audio nicely, does not have a problem with clipping and can run real-time on Android.

How I got there

My task was to wrap a proprietary closed-source TTS engine (supplied by customer) to make it work as a standard Android TextToSpeechService. The customer was complaining about the volume being too low, even though the stream volume was set to highest.

I had to find a way to boost the volume in Java in real-time while avoiding clipping and distortion.

There were two problems with Rodion's solution:

  1. the code was running a bit too slow for real-time operation on a phone (float is slow)
  2. it doesn't prevent overflow, which may cause bad and noticeable artifacts

I came to this solution:

Computation speed can be improved by trading RAM for CPU and using a look-up-table (LUT), i.e. pre-computing the volume-boost function value for every input short value out there.

This way you sacrifice 128K of RAM but get rid of the floating point and multiplication during sound processing completely, which in my case was a win.

As for the overflow, there are two ways around this. The ugly one is to simply replace the values outside of the short range with Short.MIN_VALUE or Short.MAX_VALUE respectively. It does not prevent clipping, but at least it does not overflow and the artifacts are way less disturbing.

But I found a better way, which is to apply a non-linear boost (also called gain compression). You can use an exponential function and instead of just pre-computing a multiplication LUT, you can pre-compute non-linear boost. Actually, the function plays very well with the LUT and any similar function can be pre-computed this way.

The best way to find a good boost function and optimal parameters for the function is to experiment with different functions for a while, a simple but good tool is https://mycurvefit.com/

One of the functions seemed promising, I just had to make a small modification to make negative values work in a symmetrical fashion.

After playing with some parameters, I came to the conclusion that I'll get good results if the function passes through [0,0], [10000, 25000] and [32767, 32767].

I needed quite a big volume boost, you may want to be more subtle.

MyCurveFit gave me this set of parameters: y0 = 1.240769e-22, v0 = -4.66022, k = 0.0001408133

The final boost function to be pre-computed in the LUT looks like this:

Volume boost function plot

Disclaimer: I'm not a DSP expert and I was warned that a boost like this is not suitable for Hi-Fi music and such, because it introduces changes in timbre, harmonics and other subtle artifacts. But it's fast and worked very well for my purpose and I think it will be acceptable for many uses involving speech and Lo-Fi stuff in general.

like image 7
Jan Hadáček Avatar answered Nov 07 '22 05:11

Jan Hadáček