maybeDownsampleBuffer: Loop vs. reduce vs. views

Benchmark created on


Setup

const inputRate = 48_000;
const inputBuffer = new Float32Array(inputRate);
for (let i = 0; i < inputBuffer.length; ++i) {
    inputBuffer[i] = Math.random();
}

const targetRate = 16_000;

function maybeDownsampleBufferLoop(
  buffer,
  inputRate,
  targetRate,
) {
  if (targetRate === inputRate) {
    return buffer;
  }
  const ratio = inputRate / targetRate;
  const newLength = Math.round(buffer.length / ratio);
  const result = new Float32Array(newLength);
  let offsetResult = 0;
  let offsetBuffer = 0;
  while (offsetResult < result.length) {
  	const nextOffsetBuffer = Math.round((offsetResult + 1) * ratio);
    // Average to avoid aliasing
    let accum = 0;
    let count = 0;
    for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) {
      accum += buffer[i];
      count++;
    }
    if (count > 0) {
      result[offsetResult] = accum / count;
    }
    offsetResult++;
    offsetBuffer = nextOffsetBuffer;
  }
  return result;
}

function maybeDownsampleBufferReduce(
  buffer,
  inputRate,
  targetRate,
) {
  if (targetRate === inputRate) {
    return buffer;
  }
  const ratio = inputRate / targetRate;
  const newLength = Math.round(buffer.length / ratio);
  const result = new Float32Array(newLength);
  let offsetResult = 0;
  let offsetBuffer = 0;
  buffer.reduce(([offsetResult, sumSoFar, numValuesSoFar], inputBufferVal, inputBufferIndex) => {
    const nextOffsetBuffer = Math.round((offsetResult + 1) * ratio);
    const isLast = inputBufferIndex === buffer.length - 1;
    if (inputBufferIndex < nextOffsetBuffer && !isLast) {
      return [offsetResult, sumSoFar + inputBufferVal, numValuesSoFar + 1];
    }
    const sum = isLast ? sumSoFar + inputBufferVal : sumSoFar;
    const numValues = isLast ? numValuesSoFar + 1 : numValuesSoFar;
    result[offsetResult] = sum / numValues;
    return [offsetResult + 1, inputBufferVal, 1];
  }, [0, 0, 0]);
  return result;
}

function average(arr) {
  const sum = arr.reduce((a,c) => a + c, 0);
  return sum / arr.length;
}

function splitArrayIntoChunks(buffer, numChunks) {
  const sizePerChunk = buffer.length / numChunks;
  const views = [];
  for (let i = 0; i < numChunks; ++i) {
    views.push(new Float32Array(buffer.buffer, i * sizePerChunk * Float32Array.BYTES_PER_ELEMENT, sizePerChunk));
  }
  return views;
}

function maybeDownsampleBufferViews(
  buffer,
  inputRate,
  targetRate,
) {
  if (targetRate === inputRate) {
    return buffer;
  }
  const ratio = inputRate / targetRate;
  const newLength = Math.round(buffer.length / ratio);
  const sizePerView = buffer.length / newLength;
  const result = new Float32Array(newLength);
  const views = splitArrayIntoChunks(buffer, newLength);
  views.forEach((view, i) => {
    result[i] = average(view);
  })
  return result;
}

Test runner

Ready to run.

Testing in
TestOps/sec
With loop
maybeDownsampleBufferLoop(inputBuffer, inputRate, targetRate);
ready
With TypedArray.prototype.reduce()
maybeDownsampleBufferReduce(inputBuffer, inputRate, targetRate);
ready
With views
maybeDownsampleBufferViews(inputBuffer, inputRate, targetRate);
ready

Revisions

You can edit these tests or add more tests to this page by appending /edit to the URL.