init
This commit is contained in:
158
src/lib/components/VUMeter.svelte
Normal file
158
src/lib/components/VUMeter.svelte
Normal file
@ -0,0 +1,158 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
buffer: AudioBuffer | null;
|
||||
playbackPosition?: number;
|
||||
}
|
||||
|
||||
let { buffer, playbackPosition = 0 }: Props = $props();
|
||||
let canvas: HTMLCanvasElement;
|
||||
|
||||
onMount(() => {
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
if (canvas) {
|
||||
updateCanvasSize();
|
||||
draw();
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(canvas.parentElement!);
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
buffer;
|
||||
playbackPosition;
|
||||
draw();
|
||||
});
|
||||
|
||||
function updateCanvasSize() {
|
||||
const parent = canvas.parentElement!;
|
||||
canvas.width = parent.clientWidth;
|
||||
canvas.height = parent.clientHeight;
|
||||
}
|
||||
|
||||
function calculateLevels(): [number, number] {
|
||||
if (!buffer) return [-Infinity, -Infinity];
|
||||
|
||||
const numChannels = buffer.numberOfChannels;
|
||||
const duration = buffer.length / buffer.sampleRate;
|
||||
|
||||
if (playbackPosition <= 0 || playbackPosition >= duration) {
|
||||
return [-Infinity, -Infinity];
|
||||
}
|
||||
|
||||
const windowSize = Math.floor(buffer.sampleRate * 0.05);
|
||||
const currentSample = Math.floor(playbackPosition * buffer.sampleRate);
|
||||
const startSample = Math.max(0, currentSample - windowSize);
|
||||
const endSample = Math.min(buffer.length, currentSample);
|
||||
|
||||
const leftData = new Float32Array(endSample - startSample);
|
||||
buffer.copyFromChannel(leftData, 0, startSample);
|
||||
|
||||
let leftSum = 0;
|
||||
for (let i = 0; i < leftData.length; i++) {
|
||||
leftSum += leftData[i] * leftData[i];
|
||||
}
|
||||
const leftRMS = Math.sqrt(leftSum / leftData.length);
|
||||
const leftDB = leftRMS > 0 ? 20 * Math.log10(leftRMS) : -Infinity;
|
||||
|
||||
let rightDB = leftDB;
|
||||
if (numChannels > 1) {
|
||||
const rightData = new Float32Array(endSample - startSample);
|
||||
buffer.copyFromChannel(rightData, 1, startSample);
|
||||
|
||||
let rightSum = 0;
|
||||
for (let i = 0; i < rightData.length; i++) {
|
||||
rightSum += rightData[i] * rightData[i];
|
||||
}
|
||||
const rightRMS = Math.sqrt(rightSum / rightData.length);
|
||||
rightDB = rightRMS > 0 ? 20 * Math.log10(rightRMS) : -Infinity;
|
||||
}
|
||||
|
||||
return [leftDB, rightDB];
|
||||
}
|
||||
|
||||
function draw() {
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const width = canvas.width;
|
||||
const height = canvas.height;
|
||||
|
||||
ctx.fillStyle = '#0a0a0a';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
if (!buffer) return;
|
||||
|
||||
const [leftDB, rightDB] = calculateLevels();
|
||||
const channelWidth = width / 2;
|
||||
|
||||
drawChannel(ctx, 0, leftDB, channelWidth, height);
|
||||
drawChannel(ctx, channelWidth, rightDB, channelWidth, height);
|
||||
|
||||
ctx.strokeStyle = '#333';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(channelWidth, 0);
|
||||
ctx.lineTo(channelWidth, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function dbToY(db: number, height: number): number {
|
||||
const minDB = -60;
|
||||
const maxDB = 0;
|
||||
const clampedDB = Math.max(minDB, Math.min(maxDB, db));
|
||||
const normalized = (clampedDB - minDB) / (maxDB - minDB);
|
||||
return height - (normalized * height);
|
||||
}
|
||||
|
||||
function drawChannel(ctx: CanvasRenderingContext2D, x: number, levelDB: number, width: number, height: number) {
|
||||
const gridMarks = [0, -3, -6, -10, -20, -40, -60];
|
||||
ctx.strokeStyle = '#222';
|
||||
ctx.lineWidth = 1;
|
||||
for (const db of gridMarks) {
|
||||
const y = dbToY(db, height);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x + width, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
if (levelDB === -Infinity) return;
|
||||
|
||||
const segments = [
|
||||
{ startDB: -60, endDB: -18, color: '#00ff00' },
|
||||
{ startDB: -18, endDB: -6, color: '#ffff00' },
|
||||
{ startDB: -6, endDB: 0, color: '#ff0000' }
|
||||
];
|
||||
|
||||
for (const segment of segments) {
|
||||
if (levelDB >= segment.startDB) {
|
||||
const startY = dbToY(segment.startDB, height);
|
||||
const endY = dbToY(segment.endDB, height);
|
||||
|
||||
const clampedLevelDB = Math.min(levelDB, segment.endDB);
|
||||
const levelY = dbToY(clampedLevelDB, height);
|
||||
|
||||
const segmentHeight = startY - levelY;
|
||||
|
||||
if (segmentHeight > 0) {
|
||||
ctx.fillStyle = segment.color;
|
||||
ctx.fillRect(x, levelY, width, segmentHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
|
||||
<style>
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user