Skip to content

Instantly share code, notes, and snippets.

@mattmalec
Created June 14, 2022 16:12
Show Gist options
  • Save mattmalec/6ceee1f3961ff3068727ca98ff199fab to your computer and use it in GitHub Desktop.
Save mattmalec/6ceee1f3961ff3068727ca98ff199fab to your computer and use it in GitHub Desktop.
Resample audio in Java (supporting upsampling and down sampling)
package com.mattmalec.resample;
public class Resample {
public static byte[] resample(byte[] data, int length, boolean stereo, int inFrequency, int outFrequency) {
if (inFrequency < outFrequency)
return upsample(data, length, stereo, inFrequency, outFrequency);
if (inFrequency > outFrequency)
return downsample(data, length, stereo, inFrequency, outFrequency);
return trimArray(data, length);
}
/**
* Basic upsampling algorithm. Uses linear approximation to fill in the
* missing data.
*
* @param data Input data
* @param length The current size of the input array (usually, data.length)
* @param inputIsStereo True if input is inputIsStereo
* @param inFrequency Frequency of input
* @param outFrequency Frequency of output
*
* @return Upsampled audio data
*/
private static byte[] upsample(byte[] data, int length, boolean inputIsStereo, int inFrequency, int outFrequency) {
// Special case for no action
if (inFrequency == outFrequency)
return trimArray(data, length);
double scale = (double) inFrequency / (double) outFrequency;
double pos = 0;
byte[] output;
if (!inputIsStereo) {
output = new byte[(int) (length / scale)];
for (int i = 0; i < output.length; i++) {
int inPos = (int) pos;
double proportion = pos - inPos;
if (inPos >= length - 1) {
inPos = length - 2;
proportion = 1;
}
output[i] = (byte) Math.round(data[inPos] * (1 - proportion) + data[inPos + 1] * proportion);
pos += scale;
}
} else {
output = new byte[2 * (int) ((length / 2) / scale)];
for (int i = 0; i < output.length / 2; i++) {
int inPos = (int) pos;
double proportion = pos - inPos;
int inRealPos = inPos * 2;
if (inRealPos >= length - 3) {
inRealPos = length - 4;
proportion = 1;
}
output[i * 2] = (byte) Math.round(data[inRealPos] * (1 - proportion) + data[inRealPos + 2] * proportion);
output[i * 2 + 1] = (byte) Math.round(data[inRealPos + 1] * (1 - proportion) + data[inRealPos + 3] * proportion);
pos += scale;
}
}
return output;
}
/**
* Basic downsampling algorithm. Uses linear approximation to reduce data.
*
* @param data Input data
* @param length The current size of the input array (usually, data.length)
* @param inputIsStereo True if input is inputIsStereo
* @param inFrequency Frequency of input
* @param outFrequency Frequency of output
*
* @return Downsampled audio data
*/
private static byte[] downsample(byte[] data, int length, boolean inputIsStereo, int inFrequency, int outFrequency) {
// Special case for no action
if (inFrequency == outFrequency)
return trimArray(data, length);
double scale = (double) outFrequency / (double) inFrequency;
byte[] output;
double pos = 0;
int outPos = 0;
if (!inputIsStereo) {
double sum = 0;
output = new byte[(int) (length * scale)];
int inPos = 0;
while (outPos < output.length) {
double firstVal = data[inPos++];
double nextPos = pos + scale;
if (nextPos >= 1) {
sum += firstVal * (1 - pos);
output[outPos++] = (byte) Math.round(sum);
nextPos -= 1;
sum = nextPos * firstVal;
} else {
sum += scale * firstVal;
}
pos = nextPos;
if (inPos >= length && outPos < output.length) {
output[outPos++] = (byte) Math.round(sum / pos);
}
}
} else {
double sum1 = 0, sum2 = 0;
output = new byte[2 * (int) ((length / 2) * scale)];
int inPos = 0;
while (outPos < output.length) {
double firstVal = data[inPos++], nextVal = data[inPos++];
double nextPos = pos + scale;
if (nextPos >= 1) {
sum1 += firstVal * (1 - pos);
sum2 += nextVal * (1 - pos);
output[outPos++] = (byte) Math.round(sum1);
output[outPos++] = (byte) Math.round(sum2);
nextPos -= 1;
sum1 = nextPos * firstVal;
sum2 = nextPos * nextVal;
} else {
sum1 += scale * firstVal;
sum2 += scale * nextVal;
}
pos = nextPos;
if (inPos >= length && outPos < output.length) {
output[outPos++] = (byte) Math.round(sum1 / pos);
output[outPos++] = (byte) Math.round(sum2 / pos);
}
}
}
return output;
}
/**
* @param data Data
* @param length Length of valid data
*
* @return Array trimmed to length (or same array if it already is)
*/
public static byte[] trimArray(byte[] data, int length) {
if (data.length == length)
return data;
byte[] output = new byte[length];
System.arraycopy(output, 0, data, 0, length);
return output;
}
}
@subbir
Copy link

subbir commented Jan 10, 2023

Have you tried this code? 16K to 8K does not work. It gives bunch of noise filled up in the pcm audio file. Didn't tried other variations.

@mattmalec
Copy link
Author

Yeah, I used it pretty extensively with 48k -> 8k. If it’s filled with noise, its usually because the audio is big endian vs. little endian.

@subbir
Copy link

subbir commented Jan 11, 2023

I tried this:

byte[] downSamples = Resampler.resample(buf16KIntel, buf16KIntel.length, false, 16000, 8000);

buf16Intel is little Endian. What am I missing then?

@subbir
Copy link

subbir commented Jan 11, 2023

Ahh. I see. You are right. Mine was stereo type. I was feeding it as Mono. It generates the samples correctly. Though it has some noise. But great work. Thanks. It is functional now

@subbir
Copy link

subbir commented Jan 11, 2023

Let me correct it. My audio was actually Mono. But I had to feed it as Stereo. Not sure why. If I set stereo false, it does not work for the mono audio PCM buffer. But if I set it to stereo true even the PCM buffer is Mono, it works.

@mattmalec
Copy link
Author

It generates the samples correctly. Though it has some noise.

What type of noise are you hearing? Just some light hiss or is it just completely rambled and unrecognizable?
If it's just some light hiss, that's probably expected. Linear interpolation isn't the best way to do it, but it's much easier. If you need higher quality, I'd try a library that supports sinc interpolation.

But if I set it to stereo true even the PCM buffer is Mono, it works.

It tries to split the channels in half to independently resample each one, but sending mono into a stereo track will sound the same anyways. So it's probably just computationally more expensive without actually doing anything different.

@Yomanz
Copy link

Yomanz commented Sep 15, 2023

Hi, great implementation. can you add its license header? best regards

if (inPos >= length && outPos < output.length) {
                    output[outPos++] = (byte) Math.round(sum1 / pos);
                    output[outPos++] = (byte) Math.round(sum2 / pos);
                }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment