import React, { useState, useRef, useEffect } from "react";import { IoIosPlay, IoIosPause } from "react-icons/io";// Helpersimport getWidth from "../../helpers/getWidth";// styleimport styled, { css } from "styled-components";import { neuzeitGroLight, fontSize, primary } from "../../styles/fonts";const Container = styled.div`display: flex;flex-direction: column;align-items: flex-start;`;const StyledInput = styled.input`display: none;`;// 100 (default) + 24 (padding) + 2 (border) = 126 widthconst ButtonContainer = styled.div`width: 126px;overflow: hidden;transition: width 0.25s;${(props) =>props.fileName !== null &&css`width: 0px;`}`;const StyledLabel = styled.label`margin: 16px 0px;border: 1px solid black;padding: 6px 12px;cursor: pointer;display: flex;justify-content: center;align-items: center;color: black;background-color: white;width: 100px;min-width: 0px;white-space: nowrap;transition: color 0.25s, background-color 0.25s, padding 0.25s;&:hover {color: white;background-color: black;}${(props) =>props.fileName !== null &&css`padding: 6px 0px;`}`;const VisualizerControls = styled.div`display: flex;align-items: center;width: 100%;justify-content: space-between;`;const UploadAndTitle = styled.div`display: flex;min-width: 0px;`;const FileNameContainer = styled.div`overflow: hidden;display: flex;align-items: center;min-width: 0px;`;const FileName = styled.div`${neuzeitGroLight}font-size: ${fontSize.sm};line-height: 20px;margin-left: 8px;text-overflow: ellipsis;white-space: nowrap;overflow: hidden;`;const ButtonText = styled.div`${neuzeitGroLight}font-size: ${fontSize.sm};line-height: 20px;`;const StyledPlayIcon = styled(IoIosPlay)`color: black;cursor: pointer;font-size: 28px;margin-right: 8px;&:hover {color: ${primary};}`;const StyledPauseIcon = styled(IoIosPause)`color: black;cursor: pointer;font-size: 28px;margin-right: 8px;&:hover {color: ${primary};}`;const RATIO = 2.6666;const calcBarHeight = (barHeight, height) => {const barHeightPercent = barHeight / 255;return Math.floor(barHeightPercent * height);};let AudioContext;let isSafari;if (typeof window !== `undefined`) {AudioContext = window.AudioContext || window.webkitAudioContext;isSafari = !!window.webkitAudioContext;}const Visualizer = () => {const [stateWidth, setWidth] = useState(getWidth());const [fileName, setFileName] = useState(null);const [showIcon, setShowIcon] = useState(null);const width = useRef(getWidth());const audioCtx = useRef(new AudioContext());const analyser = useRef(audioCtx.current.createAnalyser());const canvas = useRef(null);const input = useRef(null);const canvasCtx = useRef();const dataArray = useRef();const source = useRef();useEffect(() => {canvasCtx.current = canvas.current.getContext("2d");const handleResize = () => {// this is only so the component// will rerender when the window is resizedsetWidth(getWidth());// need to store this as a refwidth.current = getWidth();};window.addEventListener("resize", handleResize);return () => {window.removeEventListener("resize", handleResize);audioCtx.current.close();};}, []);const handlePlayClick = () => {audioCtx.current.resume().then(function () {setShowIcon("pause");});};const handlePauseClick = () => {audioCtx.current.suspend().then(function () {setShowIcon("play");});};const handleFiles = () => {const reader = new FileReader();source.current = audioCtx.current.createBufferSource();if (input.current.files.length !== 1) {alert("You may only select a single file!");return;}if (input.current.files[0].type !== "audio/mpeg" &&input.current.files[0].type !== "audio/mp3") {alert("The file selected must be an mp3!");return;}setFileName(input.current.files[0].name);reader.readAsArrayBuffer(input.current.files[0]);reader.onloadend = (e) => {audioCtx.current.decodeAudioData(e.target.result, (decodedData) => {source.current.buffer = decodedData;source.current.connect(analyser.current);analyser.current.connect(audioCtx.current.destination);setUp();});};};const setUp = () => {// audio configanalyser.current.minDecibels = -90;analyser.current.maxDecibels = -10;analyser.current.smoothingTimeConstant = 0.85;analyser.current.fftSize = 256;// 8-bit unsigned integersdataArray.current = new Uint8Array(analyser.current.frequencyBinCount);//beginsetShowIcon(isSafari ? "play" : "pause");// does not auto play on safarisource.current.start(0);setInterval(() => {requestAnimationFrame(draw);}, 1000 / 40);};// use callback?const draw = () => {const WIDTH = width.current;const HEIGHT = width.current / RATIO;const bufferLength = analyser.current.frequencyBinCount;analyser.current.getByteFrequencyData(dataArray.current);canvasCtx.current.clearRect(0, 0, WIDTH, HEIGHT);const barWidth = (WIDTH - bufferLength) / bufferLength;let barHeight;let x = 0;for (let i = 0; i < bufferLength; i++) {barHeight = calcBarHeight(dataArray.current[i], HEIGHT);canvasCtx.current.fillStyle = "rgb(255, 87, 51)";canvasCtx.current.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);x += barWidth + 1;}};return (<Container><canvasref={canvas}height={stateWidth / RATIO}width={stateWidth}style={{ border: "1px solid black" }}/><VisualizerControls><UploadAndTitle><ButtonContainer fileName={fileName}><StyledLabel for="file-upload" fileName={fileName}><ButtonText>Select File</ButtonText></StyledLabel><StyledInputtype="file"id="file-upload"onChange={handleFiles}ref={input}/></ButtonContainer><FileNameContainer><FileName>{fileName}</FileName></FileNameContainer></UploadAndTitle>{audioCtx.current.state === "running" && showIcon !== null && (<StyledPauseIcon onClick={handlePauseClick} />)}{audioCtx.current.state === "suspended" && showIcon !== null && (<StyledPlayIcon onClick={handlePlayClick} />)}</VisualizerControls></Container>);};export default Visualizer;
<StyledInput type="file" id="file-upload" onChange={handleFiles} ref={input} />
const reader = new FileReader();const source = audioCtx.current.createBufferSource();
if (input.current.files.length !== 1) {alert("You may only select a single file!");return;}if (input.current.files[0].type !== "audio/mpeg" &&input.current.files[0].type !== "audio/mp3") {alert("The file selected must be an mp3!");return;}setFileName(input.current.files[0].name);reader.readAsArrayBuffer(input.current.files[0]);
reader.onloadend = (e) => {audioCtx.current.decodeAudioData(e.target.result, (decodedData) => {source.current.buffer = decodedData;source.current.connect(analyser.current);analyser.current.connect(audioCtx.current.destination);setUp();});};
analyser.current.minDecibels = -90;analyser.current.maxDecibels = -10;analyser.current.smoothingTimeConstant = 0.85;analyser.current.fftSize = 256;
// 8-bit unsigned integersdataArray.current = new Uint8Array(analyser.current.frequencyBinCount);//beginsource.start(0);
setInterval(() => {requestAnimationFrame(draw);}, 1000 / 40);
const WIDTH = width.current;const HEIGHT = width.current / RATIO;const bufferLength = analyser.current.frequencyBinCount;
analyser.current.getByteFrequencyData(dataArray.current);
canvasCtx.current.clearRect(0, 0, WIDTH, HEIGHT);const barWidth = (WIDTH - bufferLength) / bufferLength;let barHeight;let x = 0;
const calcBarHeight = (barHeight, height) => {const barHeightPercent = barHeight / 255;return Math.floor(barHeightPercent * height);};
for (let i = 0; i < bufferLength; i++) {barHeight = calcBarHeight(dataArray.current[i], HEIGHT);canvasCtx.current.fillStyle = "rgb(255, 87, 51)";canvasCtx.current.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);x += barWidth + 1;}
[1] The code for this example is mostly taken from Mozilla's audio visualizer example. I've converter their eaxmple into a React component, added a file uploader, and added code to make the width of the canvas dynamic.
[2] The styling at the top of the file will also be ignored as it isn't needed to get the example up and running. If you'd like to learn about the styling I used check out styled components.
[3] If you'd like to learn more about resampling: Audio buffers: frames, samples and channels