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:
- the code was running a bit too slow for real-time operation on a phone (float is slow)
- 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:
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.
audioInputStream.getFormat()
. – MozellaudioInputStream.getFormat()
says: PCM_SIGNED 44100.0 Hz, 16 bit, mono, 2 bytes/frame, little-endian – Downandout