"Fossies" - the Fresh Open Source Software Archive

Member "jitsi-meet-7561/react/features/stream-effects/noise-suppression/NoiseSuppressorWorklet.ts" (29 Sep 2023, 8016 Bytes) of package /linux/misc/jitsi-meet-7561.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) TypeScript source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file.

    1 // @ts-expect-error
    2 import { createRNNWasmModuleSync } from '@jitsi/rnnoise-wasm';
    3 
    4 import { leastCommonMultiple } from '../../base/util/math';
    5 import RnnoiseProcessor from '../rnnoise/RnnoiseProcessor';
    6 
    7 
    8 /**
    9  * Audio worklet which will denoise targeted audio stream using rnnoise.
   10  */
   11 class NoiseSuppressorWorklet extends AudioWorkletProcessor {
   12     /**
   13      * RnnoiseProcessor instance.
   14      */
   15     private _denoiseProcessor: RnnoiseProcessor;
   16 
   17     /**
   18      * Audio worklets work with a predefined sample rate of 128.
   19      */
   20     private _procNodeSampleRate = 128;
   21 
   22     /**
   23      * PCM Sample size expected by the denoise processor.
   24      */
   25     private _denoiseSampleSize: number;
   26 
   27     /**
   28      * Circular buffer data used for efficient memory operations.
   29      */
   30     private _circularBufferLength: number;
   31 
   32     private _circularBuffer: Float32Array;
   33 
   34     /**
   35      * The circular buffer uses a couple of indexes to track data segments. Input data from the stream is
   36      * copied to the circular buffer as it comes in, one `procNodeSampleRate` sized sample at a time.
   37      * _inputBufferLength denotes the current length of all gathered raw audio segments.
   38      */
   39     private _inputBufferLength = 0;
   40 
   41     /**
   42      * Denoising is done directly on the circular buffer using subArray views, but because
   43      * `procNodeSampleRate` and `_denoiseSampleSize` have different sizes, denoised samples lag behind
   44      * the current gathered raw audio samples so we need a different index, `_denoisedBufferLength`.
   45      */
   46     private _denoisedBufferLength = 0;
   47 
   48     /**
   49      * Once enough data has been denoised (size of procNodeSampleRate) it's sent to the
   50      * output buffer, `_denoisedBufferIndx` indicates the start index on the circular buffer
   51      * of denoised data not yet sent.
   52      */
   53     private _denoisedBufferIndx = 0;
   54 
   55     /**
   56      * C'tor.
   57      */
   58     constructor() {
   59         super();
   60 
   61         /**
   62          * The wasm module needs to be compiled to load synchronously as the audio worklet `addModule()`
   63          * initialization process does not wait for the resolution of promises in the AudioWorkletGlobalScope.
   64          */
   65         this._denoiseProcessor = new RnnoiseProcessor(createRNNWasmModuleSync());
   66 
   67         /**
   68          * PCM Sample size expected by the denoise processor.
   69          */
   70         this._denoiseSampleSize = this._denoiseProcessor.getSampleLength();
   71 
   72         /**
   73          * In order to avoid unnecessary memory related operations a circular buffer was used.
   74          * Because the audio worklet input array does not match the sample size required by rnnoise two cases can occur
   75          * 1. There is not enough data in which case we buffer it.
   76          * 2. There is enough data but some residue remains after the call to `processAudioFrame`, so its buffered
   77          * for the next call.
   78          * A problem arises when the circular buffer reaches the end and a rollover is required, namely
   79          * the residue could potentially be split between the end of buffer and the beginning and would
   80          * require some complicated logic to handle. Using the lcm as the size of the buffer will
   81          * guarantee that by the time the buffer reaches the end the residue will be a multiple of the
   82          * `procNodeSampleRate` and the residue won't be split.
   83          */
   84         this._circularBufferLength = leastCommonMultiple(this._procNodeSampleRate, this._denoiseSampleSize);
   85         this._circularBuffer = new Float32Array(this._circularBufferLength);
   86     }
   87 
   88     /**
   89      * Worklet interface process method. The inputs parameter contains PCM audio that is then sent to rnnoise.
   90      * Rnnoise only accepts PCM samples of 480 bytes whereas `process` handles 128 sized samples, we take this into
   91      * account using a circular buffer.
   92      *
   93      * @param {Float32Array[]} inputs - Array of inputs connected to the node, each of them with their associated
   94      * array of channels. Each channel is an array of 128 pcm samples.
   95      * @param {Float32Array[]} outputs - Array of outputs similar to the inputs parameter structure, expected to be
   96      * filled during the execution of `process`. By default each channel is zero filled.
   97      * @returns {boolean} - Boolean value that returns whether or not the processor should remain active. Returning
   98      * false will terminate it.
   99      */
  100     process(inputs: Float32Array[][], outputs: Float32Array[][]) {
  101 
  102         // We expect the incoming track to be mono, if a stereo track is passed only on of its channels will get
  103         // denoised and sent pack.
  104         // TODO Technically we can denoise both channel however this might require a new rnnoise context, some more
  105         // investigation is required.
  106         const inData = inputs[0][0];
  107         const outData = outputs[0][0];
  108 
  109         // Exit out early if there is no input data (input node not connected/disconnected)
  110         // as rest of worklet will crash otherwise
  111         if (!inData) {
  112             return true;
  113         }
  114 
  115         // Append new raw PCM sample.
  116         this._circularBuffer.set(inData, this._inputBufferLength);
  117         this._inputBufferLength += inData.length;
  118 
  119         // New raw samples were just added, start denoising frames, _denoisedBufferLength gives us
  120         // the position at which the previous denoise iteration ended, basically it takes into account
  121         // residue data.
  122         for (; this._denoisedBufferLength + this._denoiseSampleSize <= this._inputBufferLength;
  123             this._denoisedBufferLength += this._denoiseSampleSize) {
  124             // Create view of circular buffer so it can be modified in place, removing the need for
  125             // extra copies.
  126 
  127             const denoiseFrame = this._circularBuffer.subarray(
  128                 this._denoisedBufferLength,
  129                 this._denoisedBufferLength + this._denoiseSampleSize
  130             );
  131 
  132             this._denoiseProcessor.processAudioFrame(denoiseFrame, true);
  133         }
  134 
  135         // Determine how much denoised audio is available, if the start index of denoised samples is smaller
  136         // then _denoisedBufferLength that means a rollover occurred.
  137         let unsentDenoisedDataLength;
  138 
  139         if (this._denoisedBufferIndx > this._denoisedBufferLength) {
  140             unsentDenoisedDataLength = this._circularBufferLength - this._denoisedBufferIndx;
  141         } else {
  142             unsentDenoisedDataLength = this._denoisedBufferLength - this._denoisedBufferIndx;
  143         }
  144 
  145         // Only copy denoised data to output when there's enough of it to fit the exact buffer length.
  146         // e.g. if the buffer size is 1024 samples but we only denoised 960 (this happens on the first iteration)
  147         // nothing happens, then on the next iteration 1920 samples will be denoised so we send 1024 which leaves
  148         // 896 for the next iteration and so on.
  149         if (unsentDenoisedDataLength >= outData.length) {
  150             const denoisedFrame = this._circularBuffer.subarray(
  151                 this._denoisedBufferIndx,
  152                 this._denoisedBufferIndx + outData.length
  153             );
  154 
  155             outData.set(denoisedFrame, 0);
  156             this._denoisedBufferIndx += outData.length;
  157         }
  158 
  159         // When the end of the circular buffer has been reached, start from the beginning. By the time the index
  160         // starts over, the data from the begging is stale (has already been processed) and can be safely
  161         // overwritten.
  162         if (this._denoisedBufferIndx === this._circularBufferLength) {
  163             this._denoisedBufferIndx = 0;
  164         }
  165 
  166         // Because the circular buffer's length is the lcm of both input size and the processor's sample size,
  167         // by the time we reach the end with the input index the denoise length index will be there as well.
  168         if (this._inputBufferLength === this._circularBufferLength) {
  169             this._inputBufferLength = 0;
  170             this._denoisedBufferLength = 0;
  171         }
  172 
  173         return true;
  174     }
  175 }
  176 
  177 registerProcessor('NoiseSuppressorWorklet', NoiseSuppressorWorklet);