| | <script lang="ts"> |
| | import { onMount, onDestroy } from "svelte"; |
| | |
| | interface Props { |
| | frequencyData: Uint8Array; |
| | minHeight?: number; |
| | maxHeight?: number; |
| | } |
| | |
| | let { frequencyData, minHeight = 4, maxHeight = 40 }: Props = $props(); |
| | |
| | const PILL_WIDTH = 2; |
| | const PILL_GAP = 2; |
| | const SAMPLE_INTERVAL_MS = 50; |
| | |
| | let containerRef: HTMLDivElement | undefined = $state(); |
| | let timeline: number[] = $state([]); |
| | let pillCount = $state(60); |
| | let intervalId: ReturnType<typeof setInterval> | undefined; |
| | let smoothedAmplitude = 0; |
| | |
| | |
| | function getAmplitude(): number { |
| | if (!frequencyData.length) return 0; |
| | let sum = 0; |
| | for (let i = 0; i < frequencyData.length; i++) { |
| | sum += frequencyData[i]; |
| | } |
| | return sum / frequencyData.length / 255; |
| | } |
| | |
| | function addSample() { |
| | const rawAmplitude = getAmplitude(); |
| | |
| | smoothedAmplitude = smoothedAmplitude * 0.3 + rawAmplitude * 0.7; |
| | |
| | |
| | const boostedAmplitude = Math.min(1, Math.pow(smoothedAmplitude * 1.5, 0.85)); |
| | |
| | const height = minHeight + boostedAmplitude * (maxHeight - minHeight); |
| | |
| | |
| | timeline = [...timeline, height].slice(-pillCount); |
| | } |
| | |
| | function calculatePillCount() { |
| | if (containerRef) { |
| | const width = containerRef.clientWidth; |
| | pillCount = Math.max(20, Math.floor(width / (PILL_WIDTH + PILL_GAP))); |
| | } |
| | } |
| | |
| | onMount(() => { |
| | calculatePillCount(); |
| | |
| | |
| | timeline = Array(pillCount).fill(minHeight); |
| | |
| | |
| | intervalId = setInterval(addSample, SAMPLE_INTERVAL_MS); |
| | |
| | |
| | const resizeObserver = new ResizeObserver(() => { |
| | const oldCount = pillCount; |
| | calculatePillCount(); |
| | |
| | if (pillCount > oldCount) { |
| | |
| | timeline = [...Array(pillCount - oldCount).fill(minHeight), ...timeline]; |
| | } else if (pillCount < oldCount) { |
| | timeline = timeline.slice(-pillCount); |
| | } |
| | }); |
| | |
| | if (containerRef) { |
| | resizeObserver.observe(containerRef); |
| | } |
| | |
| | return () => { |
| | resizeObserver.disconnect(); |
| | }; |
| | }); |
| | |
| | onDestroy(() => { |
| | if (intervalId) clearInterval(intervalId); |
| | }); |
| | </script> |
| |
|
| | <div bind:this={containerRef} class="flex h-12 w-full items-center justify-start gap-[2px]"> |
| | {#each timeline as height, i (i)} |
| | <div |
| | class="w-0.5 shrink-0 rounded-full bg-gray-400 dark:bg-white/60" |
| | style="height: {Math.max(minHeight, Math.round(height))}px;" |
| | ></div> |
| | {/each} |
| | </div> |
| |
|