I created an Audio visualizer using p5.js and React and i need some help.
I am trying to change the state of my button input text from Play
to Stop
when the song starts playing & from Stop
back to Play
when the user decides to stop the song at that moment.
I am using react-p5
library and everything works great until i setState
when a user clicks the play button. Since that causes a re render it breaks my canvas.
Is there a way to only re-render the button element? I am not too sure how i would go about changing the inner text of my button without a state change?
import React, { useState } from 'react';
import Sketch from 'react-p5';
import 'p5/lib/addons/p5.sound';
import Beat from '../../instrumental.mp3';
const App = () => {
let width = 900;
let height = 600;
let song;
let fft;
const [flag, setFlag] = useState(true);
const preload = (p) => {
p.soundFormats('mp3');
song = p.loadSound(Beat);
}
const setup = (p, canvasParentRef) => {
// react-p5 conveniently initializes window.p5 thats why all the other p5's
// have been changed to p in order to create an FFT object
fft = new p5.FFT();
// console.log(p.point)
// use parent to render the canvas in this ref
// without it p5 will render the canvas outside of this component
const canvas = p.createCanvas(width, height).parent(canvasParentRef);
}
const playStop = () => {
if (song.isPlaying()) {
song.pause();
setFlag(true);
//p.noLoop();
} else {
song.play();
setFlag(false);
//p.loop();
}
}
const draw = p => {
p.background(0);
// by default the stroke color is black
// we need to change this in order to see the wave
p.stroke(255, 204, 0);
// no fill in between waves
p.noFill();
// returns an array with 1024 elements
let wave = fft.waveform();
p.beginShape();
// By looping through the waveform data, we are able
// to draw the waveform across the canvas
for (let i = 0; i < width; i++) {
// create an index that maps the for loop variable
// to the index of the wave we want
// value must be integer thats we we use floor
let index = p.floor(p.map(i, 0, width, 0, wave.length));
let x = i;
let y = wave[index] * 100 + height / 2;
p.vertex(x, y);
}
p.endShape();
}
return (
<div className='outerbox'>
<h1>Audio Visualizer</h1>
<Sketch preload={preload} setup={setup} draw={draw}/>
{flag ? <button onClick={playStop}>Play</button> : <button onClick={playStop}>Stop</button>}
</div>
);
}
export default App;
The sad thing is there aren't many resources available that includes react + p5.js
If anyone would like to take the time and clone this repository down in order to see what the problem might be, i would very much appreciate that.
Repo link: https://github.com/imperium11/audio-visualizer
npm i
npm run dev-build
npm start
The issue here is that each time you update state in a functional component the function gets called again. As a result each time state changes you re-declare your preload
/setup
/draw
, because of the way react-p5
work, the running sketch will start using your updated draw
function. However, the updated draw function expects fft
to be defined, but the version of the fft
variable referenced by the new draw
function is undefined.
In order to fix this you can make any local variables that you sketch uses into state variables. In this example I've packed all the locals into one object:
const { useState } = React;
const Sketch = reactP5;
let v = 0;
const App = () => {
const width = 500;
const height = 300;
const [flag, setFlag] = useState(true);
const [locals, setLocals] = useState({});
const preload = (p) => {
p.soundFormats('mp3');
setLocals({
song: p.loadSound('https://www.paulwheeler.us/files/Ipu.wav')
});
}
const setup = (p, canvasParentRef) => {
// react-p5 conveniently initializes window.p5 thats why all the other p5's
// have been changed to p in order to create an FFT object
setLocals({
...locals,
fft: new p5.FFT()
});
// console.log(p.point)
// use parent to render the canvas in this ref
// without it p5 will render the canvas outside of this component
p.createCanvas(width, height).parent(canvasParentRef);
};
setup.version = v++;
const playStop = () => {
if (locals.song.isPlaying()) {
locals.song.pause();
setFlag(true);
//p.noLoop();
} else {
locals.song.play();
setFlag(false);
//p.loop();
}
}
const draw = p => {
p.background(0);
p.text(setup.version.toString(), 20, 20);
// by default the stroke color is black
// we need to change this in order to see the wave
p.stroke(255, 204, 0);
// no fill in between waves
p.noFill();
// returns an array with 1024 elements
let wave = locals.fft.waveform();
p.beginShape();
// By looping through the waveform data, we are able
// to draw the waveform across the canvas
for (let i = 0; i < width; i++) {
// create an index that maps the for loop variable
// to the index of the wave we want
// value must be integer thats we we use floor
let index = p.floor(p.map(i, 0, width, 0, wave.length));
let x = i;
let y = wave[index] * 100 + height / 2;
p.vertex(x, y);
}
p.endShape();
}
return (
<div className='outerbox'>
<span>Audio Visualizer</span>
{flag ? <button onClick={playStop}>Play</button> : <button onClick={playStop}>Stop</button>}
<Sketch preload={preload} setup={setup} draw={draw}/>
</div>
);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/index.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/addons/p5.sound.min.js"></script>
<div id="root"></div>
Another way to go would be to actually make the functions that use those local variables into state variables. That way they would only be declared the first time your component function is called for each instance of it. This may be a bit of a hack, but it has the advantage of working for rapidly updating local variables (if you were changing local variables in your draw()
function, my understanding is that you would not want to make those into state variables, since high frequency state updates may impact performance.
const { useState } = React;
const Sketch = reactP5;
const App = () => {
const width = 500;
const height = 300;
let song;
let fft;
const [flag, setFlag] = useState(true);
const [sketchFns] = useState({
preload: (p) => {
p.soundFormats('mp3');
song = p.loadSound('https://www.paulwheeler.us/files/Ipu.wav');
},
setup: (p, canvasParentRef) => {
// react-p5 conveniently initializes window.p5 thats why all the other p5's
// have been changed to p in order to create an FFT object
fft = new p5.FFT();
// console.log(p.point)
// use parent to render the canvas in this ref
// without it p5 will render the canvas outside of this component
p.createCanvas(width, height).parent(canvasParentRef);
},
playStop: () => {
if (song.isPlaying()) {
song.pause();
setFlag(true);
//p.noLoop();
} else {
song.play();
setFlag(false);
//p.loop();
}
},
draw: p => {
p.background(0);
// by default the stroke color is black
// we need to change this in order to see the wave
p.stroke(255, 204, 0);
// no fill in between waves
p.noFill();
// returns an array with 1024 elements
let wave = fft.waveform();
p.beginShape();
// By looping through the waveform data, we are able
// to draw the waveform across the canvas
for (let i = 0; i < width; i++) {
// create an index that maps the for loop variable
// to the index of the wave we want
// value must be integer thats we we use floor
let index = p.floor(p.map(i, 0, width, 0, wave.length));
let x = i;
let y = wave[index] * 100 + height / 2;
p.vertex(x, y);
}
p.endShape();
}
});
return (
<div className='outerbox'>
<span>Audio Visualizer</span>
{flag ? <button onClick={sketchFns.playStop}>Play</button> : <button onClick={sketchFns.playStop}>Stop</button>}
<Sketch preload={sketchFns.preload} setup={sketchFns.setup} draw={sketchFns.draw}/>
</div>
);
}
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/index.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.1/addons/p5.sound.min.js"></script>
<div id="root"></div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With