Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 23 additions & 10 deletions encoder/src/main/java/com/pedro/encoder/audio/G711Codec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,48 @@
*/
package com.pedro.encoder.audio

import com.pedro.encoder.utils.PCMUtil
import kotlin.experimental.inv

/**
* G711A (pcm-alaw) encoder/decoder
*/
class G711Codec {

private var sampleRate = 8000
private var channels = 1

fun configure(sampleRate: Int, channels: Int) {
require(!(sampleRate != 8000 || channels != 1)) {
"G711 codec only support 8000 sampleRate and mono channel"
}
this.sampleRate = sampleRate
this.channels = channels
}

fun encode(buffer: ByteArray, offset: Int, size: Int): ByteArray {
var j = offset
val count = size / 2
val validBuffer = PCMUtil.resample(
if (offset == 0 && size == buffer.size) buffer else buffer.copyOfRange(offset, offset + size),
channels, 1,
sampleRate, 8000
)
var j = 0
val count = buffer.size / 2
val out = ByteArray(count)
for (i in 0 until count) {
val sample = (buffer[j++].toInt() and 0xff or (buffer[j++].toInt() shl 8)).toShort()
val sample = (validBuffer[j++].toInt() and 0xff or (validBuffer[j++].toInt() shl 8)).toShort()
out[i] = linearToALawSample(sample)
}
return out
}

fun decode(src: ByteArray, offset: Int, len: Int): ByteArray {
fun decode(buffer: ByteArray, offset: Int, size: Int): ByteArray {
val validBuffer = PCMUtil.resample(
if (offset == 0 && size == buffer.size) buffer else buffer.copyOfRange(offset, offset + size),
channels, 1,
sampleRate, 8000
)
var j = 0
val out = ByteArray(len * 2)
for (i in 0 until len) {
val s = aLawDecompressTable[src[i + offset].toInt() and 0xff]
val out = ByteArray(validBuffer.size * 2)
for (i in validBuffer.indices) {
val s = aLawDecompressTable[validBuffer[i].toInt() and 0xff]
out[j++] = s.toByte()
out[j++] = (s.toInt() shr 8).toByte()
}
Expand Down
80 changes: 80 additions & 0 deletions encoder/src/main/java/com/pedro/encoder/utils/PCMUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,86 @@ public static byte[] mixPCM(byte[] pcm1, byte[] pcm2) {
return pcmL;
}

/**
* Resample pcm 16 bits little endian (mono or stereo) to the given sample rate.
* Upsampling interpolates between the 2 nearest frames. Downsampling averages
* all the frames of each interval to reduce aliasing.
*
* @param pcm pcm buffer, 16 bits per sample little endian
* @param channels 1 (mono) or 2 (stereo)
* @param inputSampleRate sample rate of the pcm buffer in Hz
* @param outputSampleRate desired sample rate in Hz
* @return pcm buffer resampled to outputSampleRate
*/
public static byte[] resample(byte[] pcm, int channels, int inputSampleRate, int outputSampleRate) {
return resample(pcm, channels, channels, inputSampleRate, outputSampleRate);
}

/**
* Resample pcm 16 bits little endian to the given sample rate and convert channels
* in the same pass. Mono to stereo duplicates the channel, stereo to mono averages
* both channels. Upsampling interpolates between the 2 nearest frames. Downsampling
* averages all the frames of each interval to reduce aliasing.
*
* @param pcm pcm buffer, 16 bits per sample little endian
* @param inputChannels 1 (mono) or 2 (stereo)
* @param outputChannels 1 (mono) or 2 (stereo)
* @param inputSampleRate sample rate of the pcm buffer in Hz
* @param outputSampleRate desired sample rate in Hz
* @return pcm buffer resampled to outputSampleRate with outputChannels channels
*/
public static byte[] resample(byte[] pcm, int inputChannels, int outputChannels, int inputSampleRate, int outputSampleRate) {
if (inputSampleRate == outputSampleRate && inputChannels == outputChannels) return pcm;
int inputFrameSize = inputChannels * 2;
int outputFrameSize = outputChannels * 2;
int inputFrames = pcm.length / inputFrameSize;
if (inputFrames == 0) return new byte[0];
int outputFrames = (int) ((long) inputFrames * outputSampleRate / inputSampleRate);
byte[] resampled = new byte[outputFrames * outputFrameSize];
double step = (double) inputSampleRate / outputSampleRate;
boolean downsampling = step > 1;
for (int i = 0; i < outputFrames; i++) {
double position = i * step;
int frame = (int) position;
double fraction = position - frame;
int nextFrame = Math.min(frame + 1, inputFrames - 1);
int endFrame = Math.min((int) (position + step), inputFrames);
for (int channel = 0; channel < outputChannels; channel++) {
short value;
if (downsampling) { //average all frames of the interval to reduce aliasing
long sum = 0;
for (int f = frame; f < endFrame; f++) {
sum += readSample(pcm, f, channel, inputChannels, outputChannels, inputFrameSize);
}
value = (short) Math.round((double) sum / (endFrame - frame));
} else { //interpolate between the 2 nearest frames
int sample = readSample(pcm, frame, channel, inputChannels, outputChannels, inputFrameSize);
int nextSample = readSample(pcm, nextFrame, channel, inputChannels, outputChannels, inputFrameSize);
value = (short) Math.round(sample + (nextSample - sample) * fraction);
}
int outIndex = i * outputFrameSize + channel * 2;
resampled[outIndex] = (byte) (value & 0xFF);
resampled[outIndex + 1] = (byte) ((value >> 8) & 0xFF);
}
}
return resampled;
}

private static int readSample(byte[] pcm, int frame, int outputChannel, int inputChannels, int outputChannels, int inputFrameSize) {
int base = frame * inputFrameSize;
if (outputChannels < inputChannels) { //downmix, average all input channels
int sum = 0;
for (int channel = 0; channel < inputChannels; channel++) {
int index = base + channel * 2;
sum += (short) ((pcm[index] & 0xFF) | (pcm[index + 1] << 8));
}
return sum / inputChannels;
} else { //same channels or mono to stereo duplicating the channel
int index = base + Math.min(outputChannel, inputChannels - 1) * 2;
return (short) ((pcm[index] & 0xFF) | (pcm[index + 1] << 8));
}
}

/**
* Experimental method to downgrade pcm with 3 channels or more to stereo.
* Keeps the first two channels (16 bits little endian per sample) and drops the rest.
Expand Down
88 changes: 88 additions & 0 deletions encoder/src/test/java/com/pedro/encoder/PCMUtilTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,94 @@ class PCMUtilTest {
assertArrayEquals(byteArrayOf(1, 2, 3, 4, 7, 8, 9, 10), stereo)
}

@Test
fun `GIVEN a mono pcm WHEN resample to double rate THEN output has double frames and interpolates values`() {
// 4 mono samples 16 bits: 0, 100, 200, 300
val pcm = shortsToBytes(shortArrayOf(0, 100, 200, 300))
val resampled = PCMUtil.resample(pcm, 1, 8000, 16000)
// 8 samples, odd indexes interpolated between neighbors (last one clamps to last sample)
assertArrayEquals(shortArrayOf(0, 50, 100, 150, 200, 250, 300, 300), bytesToShorts(resampled))
}

@Test
fun `GIVEN a stereo pcm WHEN resample to half rate THEN average each interval keeping channels independent`() {
// 4 stereo frames: L = 0, 100, 200, 300 | R = 1000, 1100, 1200, 1300
val pcm = shortsToBytes(shortArrayOf(0, 1000, 100, 1100, 200, 1200, 300, 1300))
val resampled = PCMUtil.resample(pcm, 2, 16000, 8000)
// each output frame averages 2 input frames per channel
assertArrayEquals(shortArrayOf(50, 1050, 250, 1250), bytesToShorts(resampled))
}

@Test
fun `GIVEN same input and output sample rate WHEN resample THEN return the same buffer`() {
val pcm = shortsToBytes(shortArrayOf(0, 100, 200, 300))
assertArrayEquals(pcm, PCMUtil.resample(pcm, 1, 44100, 44100))
}

@Test
fun `GIVEN a mono pcm WHEN resample to stereo keeping rate THEN duplicate the channel`() {
val pcm = shortsToBytes(shortArrayOf(0, 100, 200, 300))
val resampled = PCMUtil.resample(pcm, 1, 2, 8000, 8000)
assertArrayEquals(shortArrayOf(0, 0, 100, 100, 200, 200, 300, 300), bytesToShorts(resampled))
}

@Test
fun `GIVEN a stereo pcm WHEN resample to mono keeping rate THEN average both channels`() {
// 3 stereo frames: L = 0, 100, 200 | R = 1000, 1100, 1300
val pcm = shortsToBytes(shortArrayOf(0, 1000, 100, 1100, 200, 1300))
val resampled = PCMUtil.resample(pcm, 2, 1, 8000, 8000)
assertArrayEquals(shortArrayOf(500, 600, 750), bytesToShorts(resampled))
}

@Test
fun `GIVEN a mono pcm WHEN resample to stereo and double rate THEN duplicate and interpolate in one pass`() {
val pcm = shortsToBytes(shortArrayOf(0, 100, 200, 300))
val resampled = PCMUtil.resample(pcm, 1, 2, 8000, 16000)
assertArrayEquals(
shortArrayOf(0, 0, 50, 50, 100, 100, 150, 150, 200, 200, 250, 250, 300, 300, 300, 300),
bytesToShorts(resampled)
)
}

@Test
fun `GIVEN a stereo pcm WHEN resample to mono and half rate THEN downmix and average intervals in one pass`() {
// 4 stereo frames: L = 0, 100, 200, 300 | R = 1000, 1100, 1200, 1300
// downmixed frames: 500, 600, 700, 800 -> averaged intervals of 2: 550, 750
val pcm = shortsToBytes(shortArrayOf(0, 1000, 100, 1100, 200, 1200, 300, 1300))
val resampled = PCMUtil.resample(pcm, 2, 1, 16000, 8000)
assertArrayEquals(shortArrayOf(550, 750), bytesToShorts(resampled))
}

@Test
fun `GIVEN a mono pcm WHEN resample from 32000 to 8000 THEN average intervals of 4 frames`() {
val pcm = shortsToBytes(shortArrayOf(0, 100, 200, 300, 1000, 1100, 1200, 1300))
val resampled = PCMUtil.resample(pcm, 1, 32000, 8000)
assertArrayEquals(shortArrayOf(150, 1150), bytesToShorts(resampled))
}

@Test
fun `GIVEN a stereo pcm WHEN resample THEN output size matches rate ratio`() {
// 1 second of stereo at 44100Hz resampled to 32000Hz
val pcm = ByteArray(44100 * 4)
val resampled = PCMUtil.resample(pcm, 2, 44100, 32000)
assertEquals(32000 * 4, resampled.size)
}

private fun shortsToBytes(samples: ShortArray): ByteArray {
val bytes = ByteArray(samples.size * 2)
samples.forEachIndexed { i, sample ->
bytes[i * 2] = (sample.toInt() and 0xFF).toByte()
bytes[i * 2 + 1] = ((sample.toInt() shr 8) and 0xFF).toByte()
}
return bytes
}

private fun bytesToShorts(bytes: ByteArray): ShortArray {
return ShortArray(bytes.size / 2) { i ->
((bytes[i * 2].toInt() and 0xFF) or (bytes[i * 2 + 1].toInt() shl 8)).toShort()
}
}

@Test
fun `GIVEN a large multichannel frame WHEN pcmToStereo THEN output size is exact and does not overflow`() {
// 4 channels, 2000 frames -> input 16000 bytes, stereo output 8000 bytes (old static buffer was 4096)
Expand Down
Loading