134 lines
3.1 KiB
Svelte
134 lines
3.1 KiB
Svelte
<script lang="ts">
|
|
import { onMount, onDestroy } from 'svelte';
|
|
|
|
interface Props {
|
|
analyserNode?: AnalyserNode | null;
|
|
}
|
|
|
|
let { analyserNode }: Props = $props();
|
|
|
|
let container: HTMLDivElement;
|
|
let canvas: HTMLCanvasElement;
|
|
let canvasContext: CanvasRenderingContext2D | null = null;
|
|
let animationFrameId: number | null = null;
|
|
let dataArray: Uint8Array | null = null;
|
|
let width = $state(800);
|
|
let height = $state(600);
|
|
|
|
$effect(() => {
|
|
if (animationFrameId !== null) {
|
|
cancelAnimationFrame(animationFrameId);
|
|
animationFrameId = null;
|
|
}
|
|
|
|
if (analyserNode && canvasContext) {
|
|
setupSpectrogram(analyserNode);
|
|
}
|
|
});
|
|
|
|
function setupSpectrogram(node: AnalyserNode) {
|
|
try {
|
|
const bufferLength = node.frequencyBinCount;
|
|
dataArray = new Uint8Array(bufferLength);
|
|
startDrawing(node);
|
|
} catch (error) {
|
|
console.error('Failed to setup spectrogram:', error);
|
|
}
|
|
}
|
|
|
|
function startDrawing(node: AnalyserNode) {
|
|
if (!canvasContext || !dataArray) return;
|
|
|
|
const draw = () => {
|
|
if (!canvasContext || !dataArray || !node || !canvas) return;
|
|
|
|
animationFrameId = requestAnimationFrame(draw);
|
|
|
|
node.getByteFrequencyData(dataArray);
|
|
|
|
const imageData = canvasContext.getImageData(1, 0, canvas.width - 1, canvas.height);
|
|
canvasContext.putImageData(imageData, 0, 0);
|
|
|
|
const barWidth = 1;
|
|
const x = canvas.width - barWidth;
|
|
|
|
for (let i = 0; i < dataArray.length; i++) {
|
|
const value = dataArray[i];
|
|
const percent = value / 255;
|
|
const y = canvas.height - (i / dataArray.length) * canvas.height;
|
|
const height = (canvas.height / dataArray.length);
|
|
|
|
const hue = (1 - percent) * 240;
|
|
const saturation = 100;
|
|
const lightness = percent * 50;
|
|
|
|
canvasContext.fillStyle = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
|
canvasContext.fillRect(x, y - height, barWidth, height);
|
|
}
|
|
};
|
|
|
|
draw();
|
|
}
|
|
|
|
function updateSize() {
|
|
if (container && canvas) {
|
|
const rect = container.getBoundingClientRect();
|
|
width = Math.floor(rect.width);
|
|
height = Math.floor(rect.height);
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
if (canvas) {
|
|
canvasContext = canvas.getContext('2d');
|
|
if (canvasContext) {
|
|
canvasContext.fillStyle = '#000';
|
|
canvasContext.fillRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
}
|
|
|
|
updateSize();
|
|
|
|
const resizeObserver = new ResizeObserver(() => {
|
|
updateSize();
|
|
});
|
|
|
|
if (container) {
|
|
resizeObserver.observe(container);
|
|
}
|
|
|
|
return () => {
|
|
resizeObserver.disconnect();
|
|
};
|
|
});
|
|
|
|
onDestroy(() => {
|
|
if (animationFrameId !== null) {
|
|
cancelAnimationFrame(animationFrameId);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div class="spectrogram" bind:this={container}>
|
|
<canvas bind:this={canvas} {width} {height}></canvas>
|
|
</div>
|
|
|
|
<style>
|
|
.spectrogram {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background-color: #0a0a0a;
|
|
overflow: hidden;
|
|
}
|
|
|
|
canvas {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-color: #0a0a0a;
|
|
}
|
|
</style>
|