This commit is contained in:
2025-10-10 23:13:57 +02:00
commit 58d1424adb
25 changed files with 1958 additions and 0 deletions

View 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>