Skip to content

Instantly share code, notes, and snippets.

@robotnealan
Created August 26, 2018 18:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save robotnealan/ee6ad753738d430631d0412a88d4202b to your computer and use it in GitHub Desktop.
Save robotnealan/ee6ad753738d430631d0412a88d4202b to your computer and use it in GitHub Desktop.
An example React component on
import React, { Component } from 'react';
import { connect } from 'react-redux';
import _isEmpty from 'lodash/isEmpty';
import bowser from 'bowser';
const userAgent = bowser.detect(window.navigator.userAgent);
class WaveformAnimated extends Component {
constructor(props) {
super(props);
this.BAR_WIDTH = 2;
this.BAR_PADDING = 2;
// Article Note:
//
// In our production implementation we dynamically resize the waveform based
// on its container size, and automatically when the user resizes their browser window.
// In this example these could easily be hard-coded, but I've left them as-is so it's
// clearer what values you'd need to dynamically alter to do the same.
this.state = {
height: 60,
width: 500,
numberOfBars: 100
};
// Create React Refs so we can reference the <canvas> and its
// container directly.
this.canvas = React.createRef();
this.container = React.createRef();
// Leave these two as old fashioned `.bind(this)` functions as they're
// marginally more performant than ES2015 arrow methods, and performance is
// of the essence when rendering in real time.
this.drawCanvas = this.drawCanvas.bind(this);
this.loop = this.loop.bind(this);
}
componentDidMount() {
this.canvasContext = this.canvas.getContext('2d');
this.createAudioContext();
this.loop();
}
componentWillUnmount() {
// Cancel the animation frame loop,
// and close the AudioContext as browsers typically have a
// hard 6 AudioContext limit without a full page refresh.
cancelAnimationFrame(this.raf);
if (this.audioContext) this.audioContext.close();
}
createAudioContext = () => {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) throw new Error('AudioContext not supported');
this.audioContext = new AudioContext();
this.audioContext.maxChannelCount = 2;
// This is SoundManager2's internal HTML <audio> element that's not actually rendered
// to the page. If you're not using SoundManager2 you can directly reference your own
// <audio> element.
this.audioElement = window.soundManager.sounds[window.soundManager.soundIDs['0']]._a;
this.audioElement.crossOrigin = 'anonymous';
try {
this.audioSource = this.audioContext.createMediaElementSource(this.audioElement);
this.audioSource.crossOrigin = 'anonymous';
this.analyser = this.audioContext.createAnalyser();
this.analyser.smoothingTimeConstant = 0.85;
this.analyser.fftSize = 256;
this.audioSource.connect(this.analyser);
this.audioSource.connect(this.audioContext.destination);
this.frequencyData = new Uint8Array(this.analyser.frequencyBinCount);
this.resampledData = new Uint8Array(this.state.numberOfBars);
} catch (e) {
console.log('[createAudioContext Error]', e);
}
}
loop() {
// Pull the frequency data from the analyser and pass into the Uint8Array.
this.analyser.getByteFrequencyData(this.frequencyData);
// Resample the raw frequency data and save into a separate Uint8Array.
this.resampleData();
// Draw to our canvas.
this.drawCanvas();
// Request an animationFrame to sync our next loop with the user's monitor
// refresh rate.
this.raf = requestAnimationFrame(this.loop);
}
resampleData() {
const elements = this.frequencyData.length;
const deltaT = elements / this.state.numberOfBars;
this.resampledData = [];
for (let i = 0; i < this.state.numberOfBars; i++) {
const t = deltaT * i;
const leftWeight = t % 1;
const leftIndex = Math.floor(t);
// This takes the raw 0-1 value frequency data coming out of AnalyserNode
// and interpolates between the left/right values of each to calculate a
// new value based on the width number of bars based on the container width.
this.resampledData[i] = ((leftWeight * this.frequencyData[leftIndex] + (1 - leftWeight) * this.frequencyData[leftIndex + 1]) * (this.state.height / 255)) / 2;
if (i < 3) {
this.resampledData[i] = this.resampledData[i] * 1 / (4 - i);
}
}
}
drawCanvas() {
// Clear the canvas on each draw so we have a clean slate to work with.
this.canvasContext.clearRect(0, 0, this.state.width, this.state.height);
// Take the resampled data and draw a rectangle for each value based on our
// bar width, the items' actual frequency value, and its position within the
// Uint8Array.
this.resampledData.map((value, key) => {
const posX = key * (this.BAR_WIDTH + this.BAR_PADDING) + ((this.BAR_WIDTH + this.BAR_PADDING) + this.BAR_WIDTH);
this.canvasContext.fillStyle = '#333333';
this.canvasContext.fillRect((this.state.width) - posX, 0, this.BAR_WIDTH, value);
return true;
});
this.canvasContext.fill();
return true;
}
render() {
return (
<div
ref={this.container}
>
<canvas
ref={this.canvas}
height={this.state.height}
width={this.state.width}
/>
</div>
);
}
}
export default WaveformAnimated;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment