How can I implement a jitter buffer in javascript for realtime audio processing

376 Views Asked by At

I have a sender an a receiver running on localhost

both emit and receive on an average interval of 2.89 and 2.92 milliseconds.

the correct constant interval should be 2.90;

So I tried implementing some sort of ringbuffer where buffer size is 128 * N where 128 is the size of the data chunk and N the number of data chunks kept in memory (latency).

I haven't evaluated the time interval between the receival and the processing but according to the data I've managed to calculate a buffer of size N = 2 should be sufficient.

Also my buffer is quite simple it's a FIFO and if a value is received while the buffer is full it replaces the previous chunk.

To get a correct sound I need to make a buffer size of N = 16 so my question is how can I implement a low latency jitter buffer that may be adaptative

I guess currently the buffer adds extra memory for managing an average variation but what I would need is a technique to correct the variation "in place".

my current implementation

_buffer = [];

BUFFER_SIZE = 8;

_isStarted = false;

readIndex = -1

//this callback is triggered any time a packet is received.

_onReceivePacket = ( event ) => {

    let chunks = [];

    //chunk length = 128

    for ( let chunk of event.data ) {

      chunks.push( new Float32Array( chunk ) );

    }

    if ( this._buffer.length < this.BUFFER_SIZE ) {

      this._buffer.unshift( chunks );

      this.readIndex++;

    } else {

      this._buffer.splice( 0, 1 );

      this._buffer.unshift( chunks );

      this._isStarted = true;

    }

}
  //this function copies the buffer into the playout output stream

  _pullOut ( output ) {

    try {

      for ( let i = 0; i < output.length; i++ ) {

        const channel = output[ i ];

        for ( let j = 0; j < channel.length; j++ ) {

          channel[ j ] = this._buffer[ this.readIndex ][ i ][ j ];

        }

      }

      if ( this.readIndex - 1 !== -1 ) {

        this._buffer.splice( this.readIndex, 1 );

        this.readIndex--;

      }

    } catch ( e ) {

      console.log( e, this._buffer, this.readIndex );

    }

  }
1

There are 1 best solutions below

1
Pradnya Kulkarni On

you can write some class in javascript like below

class JitterBuffer {
  constructor(bufferSize, latency) {
    this.bufferSize = bufferSize; // Size of each data chunk
    this.latency = latency; // Number of data chunks kept in memory
    this.buffer = new Array(latency);
    this.head = 0; // Index of the next available slot in the buffer
    this.tail = 0; // Index of the next data chunk to be processed
    this.fillCount = 0; // Number of data chunks currently in the buffer
    this.interval = 2.90; // Target time interval between data chunks
    this.timer = null; // Reference to the setInterval timer
  }

  start() {
    // Start the timer to check for buffer underflow and adjust the fillCount
    this.timer = setInterval(() => {
      if (this.fillCount === 0) {
        return;
      }

      const now = Date.now();
      const expectedTime = this.tail * this.interval;
      const actualTime = now - this.buffer[this.tail].timestamp;

      if (actualTime >= expectedTime) {
        // Remove the oldest data chunk and update the tail index
        this.tail = (this.tail + 1) % this.latency;
        this.fillCount--;
      }
    }, Math.floor(this.interval / 2));
  }

  stop() {
    // Stop the timer
    clearInterval(this.timer);
    this.timer = null;
  }

  push(data) {
    // Add the data chunk to the buffer and update the head index
    this.buffer[this.head] = {
      data,
      timestamp: Date.now(),
    };
    this.head = (this.head + 1) % this.latency;

    if (this.fillCount < this.latency) {
      // Increment the fillCount if the buffer is not full yet
      this.fillCount++;
    } else {
      // Replace the oldest data chunk and update the tail index
      this.tail = (this.tail + 1) % this.latency;
    }
  }

  shift() {
    if (this.fillCount === 0) {
      return null;
    }

    // Get the next data chunk to be processed and update the tail index
    const dataChunk = this.buffer[this.tail].data;
    this.tail = (this.tail + 1) % this.latency;
    this.fillCount--;

    return dataChunk;
  }

  adjustInterval(actualInterval) {
    // Adjust the target time interval based on the actual interval
    const delta = actualInterval - this.interval;
    this.interval += delta / 8; // Adapt to 1/8th of the difference
  }
}

It can be used like below:

// Create a jitter buffer with a buffer size of 128 bytes per chunk and a latency of 16 chunks
const jitterBuffer = new JitterBuffer(128, 16);
// Start the jitter buffer timer
jitterBuffer.start();

// Add some data to the jitter buffer
const data = new Uint8Array(128);
jitterBuffer.push(data);

// Retrieve the next data chunk from the jitter buffer
const nextChunk = jitterBuffer.shift();

// Stop the jitter buffer timer
jitterBuffer.stop();