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! :)
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;
}
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.
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.
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.
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:
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.
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