refactor AI WaveForm.jsx

Signed-off-by: ale <ale@manalejandro.com>
This commit is contained in:
ale 2025-05-29 21:57:22 +02:00
parent 8bbd0c3b27
commit 1cb4b8a023
Signed by: ale
GPG Key ID: 244A9C4DAB1C0C81
5 changed files with 166 additions and 83 deletions

View File

@ -29,7 +29,6 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<script src="js/materialize.min.js" defer></script>
<title>Stream Radio</title>
</head>
<body>

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,9 @@
import "./App.css";
import { useCallback, useEffect, useRef, useState } from "react";
import WaveForm from "./WaveForm";
import M from "materialize-css";
import { Button, Icon, Range } from "react-materialize";
import text from "./list.txt";
const App = () => {
const [audioUrl, setAudioUrl] = useState(null),
@ -17,6 +19,7 @@ const App = () => {
[maxListeners, setMaxListeners] = useState(0),
[paused, setPaused] = useState(true),
audioElmRef = useRef(null),
loadedAnalyzer = useRef(false),
once = useRef(false),
audioAnalyzer = () => {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
@ -32,23 +35,20 @@ const App = () => {
}
setAnalyzerData({ analyzer, bufferLength, dataArray })
},
loadData = () => {
fetch('/stream.json').then(response => response.json()).then(json => {
loadData = async () => {
try {
const response = await fetch('/stream.json'),
json = await response.json()
setJson(json)
}).catch(err => {
} catch (err) {
console.error('Error fetching data: ' + err.message)
})
}
},
loadImages = () => {
fetch('/wallpapers/list.txt').then(response => response.text()).then(text => {
setImages(text.split('\n').filter(line => line.length > 0))
}).catch(err => {
console.error('Error fetching data: ' + err.message)
})
},
loadListeners = () => {
fetch('/status.xsl').then(response => response.text()).then(data => {
const parser = new DOMParser(),
loadListeners = async () => {
try {
const response = await fetch('/status.xsl'),
data = await response.text(),
parser = new DOMParser(),
xmlDoc = parser.parseFromString(data, 'text/html'),
listeners = xmlDoc.getElementsByTagName('td')
for (let i = 0; i < listeners.length; i++) {
@ -58,14 +58,23 @@ const App = () => {
setMaxListeners(listeners[i].textContent)
}
}
}).catch(err => {
} catch (err) {
console.error('Error fetching data: ' + err.message)
})
}
},
loadImages = useCallback(async () => {
const response = await fetch(text),
data = await response.text()
setImages(data.split('\n').filter(line => line.length > 0))
}),
load = useCallback(async () => {
await loadData()
await loadListeners()
}),
play = useCallback(async () => {
if (!once.current) {
if (!loadedAnalyzer.current) {
audioAnalyzer()
once.current = true
loadedAnalyzer.current = true
}
setPaused(false)
setMuted(false)
@ -91,26 +100,27 @@ const App = () => {
setBounce(data => data + json.media.track[0][key])
}
})
} else if (!json?.media && bounce.search('Now Playing: null') === -1) {
} else if (!json?.media && bounce.search('Now Playing: Title not available') === -1) {
setLink('')
setTitle('Stream Radio')
setBounce('Now Playing: null')
setBounce('Now Playing: Title not available')
}
}, [json])
useEffect(() => {
document.body.style.backgroundImage = `url('/wallpapers/${images[Math.floor(Math.random() * images.length)]}')`
}, [images])
useEffect(() => {
loadData()
if (once.current) return
once.current = true
M.AutoInit()
load()
loadImages()
loadListeners()
setAudioUrl('/stream.mp3')
document.querySelector('input[type="range"]').addEventListener('change', (e) => {
setCurrentVolume(e.target.value / 100)
})
const inter = setInterval(() => {
loadData()
loadListeners()
load()
}, (Math.floor(Math.random() * 20) + 10) * 1000),
interback = setInterval(() => {
loadImages()
@ -159,7 +169,7 @@ const App = () => {
<Icon tiny>people</Icon>&nbsp;{currentListeners} / {maxListeners}
</span>
</div >
<audio src={audioUrl} ref={audioElmRef} volume={Math.log(currentVolume)} preload={"none"} muted={muted} controls={false} />
<audio src={audioUrl} ref={audioElmRef} volume={Math.log10(currentVolume * 10)} preload={"none"} muted={muted} controls={false} />
</>
)
}

View File

@ -1,55 +1,68 @@
import { useRef, useEffect } from "react";
import useSize from "./useSize";
const animateBars = (analyser, canvas, canvasCtx, dataArray, bufferLength) => {
analyser.getByteFrequencyData(dataArray)
canvasCtx.fillStyle = "#000"
const HEIGHT = canvas.height / 2
var barWidth = Math.ceil(canvas.width / bufferLength) * 2.5
let barHeight
let x = 0
for (var i = 0; i < bufferLength; i++) {
barHeight = (dataArray[i] / 255) * HEIGHT
const blueShade = Math.floor((dataArray[i] / 255) * 4) // generate a shade of blue based on the audio input
const blueHex = ['rgba(255,255,255,0.5)', 'rgba(255,255,255,0.4)', 'rgba(255,255,255,0.3)', 'rgba(255,255,255,0.2)'][blueShade]; // use react logo blue shades
canvasCtx.fillStyle = blueHex
canvasCtx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight)
x += barWidth + 1
}
},
WaveForm = ({ analyzerData }) => {
const canvasRef = useRef(null),
{ dataArray, analyzer, bufferLength } = analyzerData,
[width, height] = useSize(),
draw = (dataArray, analyzer, bufferLength) => {
const canvas = canvasRef.current
if (!canvas || !analyzer) return
const canvasCtx = canvas.getContext("2d"),
animate = () => {
requestAnimationFrame(animate)
// eslint-disable-next-line no-self-assign
canvas.width = canvas.width
canvasCtx.translate(0, canvas.offsetHeight / 2)
animateBars(analyzer, canvas, canvasCtx, dataArray, bufferLength)
}
animate()
}
useEffect(() => {
draw(dataArray, analyzer, bufferLength)
}, [dataArray, analyzer, bufferLength])
return (
<canvas
style={{
position: "absolute",
top: "0",
left: "0",
zIndex: "-10"
}}
ref={canvasRef}
width={width}
height={height}
/>
)
}
const BLUE_SHADES = [
"rgba(255,255,255,0.5)",
"rgba(255,255,255,0.4)",
"rgba(255,255,255,0.3)",
"rgba(255,255,255,0.2)",
];
export default WaveForm
const animateBars = (analyser, canvas, ctx, dataArray, bufferLength) => {
analyser.getByteFrequencyData(dataArray);
const HEIGHT = canvas.height / 2;
const barWidth = Math.ceil(canvas.width / bufferLength) * 2.5;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
const barHeight = (dataArray[i] / 255) * HEIGHT;
const blueShade = Math.floor((dataArray[i] / 255) * 4);
ctx.fillStyle = BLUE_SHADES[blueShade];
ctx.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
};
const WaveForm = ({ analyzerData }) => {
const canvasRef = useRef(null);
const { dataArray, analyzer, bufferLength } = analyzerData;
const [width, height] = useSize();
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas || !analyzer) return;
const ctx = canvas.getContext("2d");
let animationId;
const render = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(0, canvas.height / 2);
animateBars(analyzer, canvas, ctx, dataArray, bufferLength);
ctx.restore();
animationId = requestAnimationFrame(render);
};
render();
return () => {
cancelAnimationFrame(animationId);
};
}, [dataArray, analyzer, bufferLength, width, height]);
return (
<canvas
style={{
position: "absolute",
top: 0,
left: 0,
zIndex: 0,
}}
ref={canvasRef}
width={width}
height={height}
/>
);
};
export default WaveForm;

67
src/list.txt Normal file
View File

@ -0,0 +1,67 @@
pexels-baskincreativeco-1480807.jpg
pexels-belle-co-99483-847393.jpg
pexels-bess-hamiti-83687-36487.jpg
pexels-carlos-oliva-1966452-3586966.jpg
pexels-christian-heitz-285904-842711.jpg
pexels-dreamypixel-547115.jpg
pexels-eberhardgross-1062249.jpg
pexels-eberhardgross-1287075.jpg
pexels-eberhardgross-1287089.jpg
pexels-eberhardgross-1301976.jpg
pexels-eberhardgross-1367192(1).jpg
pexels-eberhardgross-1367192.jpg
pexels-eberhardgross-1612351.jpg
pexels-eberhardgross-1612360.jpg
pexels-eberhardgross-1612362.jpg
pexels-eberhardgross-1612371.jpg
pexels-eberhardgross-1624255.jpg
pexels-eberhardgross-534164.jpg
pexels-eberhardgross-629167.jpg
pexels-eberhardgross-691668.jpg
pexels-eberhardgross-707344.jpg
pexels-eberhardgross-730981.jpg
pexels-esan-2085998.jpg
pexels-fotios-photos-109260.jpg
pexels-francesco-ungaro-1525041.jpg
pexels-gochrisgoxyz-1477166.jpg
pexels-johnnoibn-1448136.jpg
pexels-joshkjack-135018.jpg
pexels-jplenio-1110656.jpg
pexels-jplenio-1146708.jpg
pexels-jplenio-1435075.jpg
pexels-kasperphotography-1042423.jpg
pexels-katja-79053-592077.jpg
pexels-lastly-937782.jpg
pexels-lilartsy-1213447.jpg
pexels-maxfrancis-2246476.jpg
pexels-mdsnmdsnmdsn-1831234.jpg
pexels-mdx014-814499.jpg
pexels-michal-pech-213601-1632044.jpg
pexels-no-name-14543-66997.jpg
pexels-pixabay-158063.jpg
pexels-pixabay-33109(1).jpg
pexels-pixabay-33109.jpg
pexels-pixabay-33545.jpg
pexels-pixabay-358532.jpg
pexels-pixabay-41004.jpg
pexels-pixabay-414144.jpg
pexels-pixabay-416160.jpg
pexels-pixabay-459203.jpg
pexels-pixabay-462162.jpg
pexels-pixabay-50594.jpg
pexels-pixabay-50686.jpg
pexels-pixabay-52500.jpg
pexels-rpnickson-2559941.jpg
pexels-rpnickson-2647990.jpg
pexels-samandgos-709552.jpg
pexels-samkolder-2387873.jpg
pexels-sebastian-312105.jpg
pexels-simon73-1183099.jpg
pexels-souvenirpixels-1519088.jpg
pexels-souvenirpixels-417074.jpg
pexels-stefanstefancik-919606.jpg
pexels-stywo-1054289.jpg
pexels-stywo-1668246.jpg
pexels-therato-1933239.jpg
pexels-todd-trapani-488382-1198817.jpg
pexels-umaraffan499-21787.jpg