Created
August 26, 2018 18:55
-
-
Save robotnealan/ee6ad753738d430631d0412a88d4202b to your computer and use it in GitHub Desktop.
An example React component on
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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