1

I have a class I am attempting to write that looks like the below.

When I run the class, I get an error:

Audio.js:53 Uncaught ReferenceError: bufferLength is not defined

I believe this is because bufferLength is not available in the drawCanvas function.

I have tried adding the this keyword, however this did not work.

How can I make these variables available to functions within a method of this class?

export const LINE_COLORS = ['rgba(255, 23, 204, 0.5)', 'rgba(130, 23, 255, 0.5)'];

// The dimensions of the current viewport
// - Used to set canvas width & height
export const PAGE_DIMENSIONS = {
  width: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth,
  height: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight,
};

class AudioEngine {
  constructor(params) {
    this.params = params;
    this.audio = new Audio();
    this.ctx = new (window.AudioContext || window.webkitAudioContext)();
    this.analyser = this.ctx.createAnalyser();
    this.source = this.ctx.createMediaElementSource(this.audio);
    this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
    this.bufferLength = this.analyser.frequencyBinCount;

    this.canvas = document.getElementById(params.waveform);

    this.onInit();
  }

  onInit() {
    this.ConfigAudio();
    this.DrawAudioWave();
  }

  ConfigAudio = () => {
    this.audio.src = this.params.stream;
    this.audio.controls = false;
    this.audio.autoplay = false;
    this.audio.crossOrigin = 'anonymous';
    this.analyser.smoothingTimeConstant = 0.6;
    this.source.connect(this.ctx.destination);
    this.source.connect(this.analyser);
    this.analyser.fftSize = 2048;
    this.analyser.getByteFrequencyData(this.dataArray);

    document.body.appendChild(this.audio);
  };

  DrawAudioWave = () => {
    // Bind to the context
    const canvasCtx = this.canvas.getContext('2d');

    function drawCanvas() {
      // We always start the drawing function by clearing the canvas. Otherwise
      // we will be drawing over the previous frames, which gets messy real quick
      canvasCtx.clearRect(0, 0, PAGE_DIMENSIONS.width, PAGE_DIMENSIONS.height);
      requestAnimationFrame(drawCanvas);
      const sliceWidth = (PAGE_DIMENSIONS.width * 1.0) / bufferLength;
      // Create a new bounding rectangle to act as our backdrop, and set the
      // fill color to black.
      canvasCtx.fillStyle = '#000';
      canvasCtx.fillRect(0, 0, PAGE_DIMENSIONS.width, PAGE_DIMENSIONS.height);

      // Loop over our line colors. This allows us to draw two lines with
      // different colors.
      LINE_COLORS.forEach((color, index) => {
        let x = 0;
        // Some basic line width/color config
        canvasCtx.lineWidth = 2;
        canvasCtx.strokeStyle = color;

        // Start drawing the path
        canvasCtx.beginPath();
        for (let i = 0; i < bufferLength; i++) {
          // We offset using the loops index (stops both colored lines
          // from overlapping one another)
          const v = dataArray[i] / 120 + index / 20;
          // Set the Y point to be half of the screen size (the middle)
          const y = (v * PAGE_DIMENSIONS.height) / 2;

          if (i === 0) {
            canvasCtx.moveTo(x, y);
          } else {
            canvasCtx.lineTo(x, y);
          }

          x += sliceWidth;
        }
        canvasCtx.lineTo(canvas.width, canvas.height / 2);
        canvasCtx.stroke();
      });
    }
    drawCanvas();
  };

  audioToggle = () => (this.audio.paused ? this.audio.play() : this.audio.pause());
}

export default AudioEngine;
PetePete
  • 27
  • 8
  • Something else I noticed with your code. You are calling `requestAnimationFrame` before the main body of your function, which means you can have race conditions. You should move the request to after your function's body executes, or look into clearing/cancelling previous requests. – noahnu Jul 15 '18 at 16:55

1 Answers1

3

requestAnimationFrame will call drawCanvas with the global context bound, so this.bufferLength will not be defined. Easiest solution is to take advantage of lexical scoping, which arrow functions make easy.

If you look at how you're defining your class's methods, they're using arrow functions. This is precisely to avoid the issue with rebinding this that you're currently having. You should read up on this and scoping in JavaScript to better understand why your code isn't working as you'd expect.

Couple solutions, both require drawCanvas -> this.drawCanvas:

(A) Bind the context of drawCanvas before passing it to requestAnimationFrame:

requestAnimationFrame(drawCanvas.bind(this))

Or (B) take advantange of lexical scoping:

requestAnimationFrame(() => drawCanvas())

"Lexical" scope is scope derived from the "text", i.e. where your arrow function is defined relative to other scopes. Non-lexical scoping uses the caller function to determine bound context.

You can also change the drawCanvas function itself to be bound to the appropriate scope using .bind or changing it to an arrow function.

Further Reading:

noahnu
  • 3,013
  • 2
  • 15
  • 37