feat: real-time dB level meters per node
Backend:
- One pw_stream (INPUT, F32) per audio node; for sinks use
stream.capture.sink=true to read the monitor port
- RT process callback computes instantaneous linear peak with no
allocations; stored in std::atomic<float>
- Meter streams are filtered from the registry so they never appear
in the graph or trigger recursive meter creation
- Meter streams are created on first node-ready event, destroyed on
node removal, and cleaned up on engine close
- GET /api/peaks → {node_id: linear_peak} (polled by frontend)
Frontend:
- peaks store polled at 100 ms via setInterval; starts/stops with
initGraph/destroyGraph
- Each node card grows 8 px and shows a 3 px meter bar at the bottom
(green below -12 dB, yellow -12 to -3 dB, red above -3 dB)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import {
|
||||
nodes, ports, links, connected, patchbay,
|
||||
nodes, ports, links, connected, patchbay, peaks,
|
||||
portById,
|
||||
initGraph, destroyGraph,
|
||||
connectPorts, disconnectPorts,
|
||||
@@ -139,6 +139,19 @@
|
||||
return $patchbay.aliases?.[nd.name] || nd.nick || nd.name;
|
||||
}
|
||||
|
||||
// Level meter helpers
|
||||
function meterDb(nodeId: number): number {
|
||||
const p = $peaks[nodeId] ?? 0;
|
||||
return p > 0 ? Math.max(-60, 20 * Math.log10(p)) : -60;
|
||||
}
|
||||
function meterWidth(nodeId: number, barW: number): number {
|
||||
return barW * Math.max(0, (meterDb(nodeId) + 60) / 60);
|
||||
}
|
||||
function meterColor(nodeId: number): string {
|
||||
const db = meterDb(nodeId);
|
||||
return db >= -3 ? '#c44' : db >= -12 ? '#ca4' : '#4a9';
|
||||
}
|
||||
|
||||
// Build computed layout
|
||||
let graphNodes = $derived.by(() => {
|
||||
const n = $nodes;
|
||||
@@ -194,7 +207,7 @@
|
||||
const headerH = 22;
|
||||
const portH = 16;
|
||||
const maxPorts = Math.max(allInPorts.length, allOutPorts.length, 1);
|
||||
const height = headerH + maxPorts * portH + 20; // extra 20 for volume slider
|
||||
const height = headerH + maxPorts * portH + 28; // 20 for volume slider + 8 for meter bar
|
||||
const width = 220;
|
||||
|
||||
const portPositions = new Map<number, { x: number; y: number }>();
|
||||
@@ -677,6 +690,10 @@
|
||||
{Math.round(Math.max(0, Math.min(1, nd.volume)) * 100)}%
|
||||
</text>
|
||||
|
||||
<!-- Level meter -->
|
||||
<rect x={nd.x + 8} y={nd.y + nd.height - 6} width={nd.width - 16} height="3" rx="1" fill="#1a1a1a" />
|
||||
<rect x={nd.x + 8} y={nd.y + nd.height - 6} width={meterWidth(nd.id, nd.width - 16)} height="3" rx="1" fill={meterColor(nd.id)} />
|
||||
|
||||
</g>
|
||||
{/each}
|
||||
</svg>
|
||||
|
||||
@@ -8,6 +8,21 @@ export const ports = writable<Port[]>([]);
|
||||
export const links = writable<Link[]>([]);
|
||||
export const connected = writable(false);
|
||||
|
||||
// Level meter peaks: node_id → linear peak (0–1)
|
||||
export const peaks = writable<Record<number, number>>({});
|
||||
let peakInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
async function pollPeaks() {
|
||||
try {
|
||||
const res = await fetch('/api/peaks');
|
||||
if (!res.ok) return;
|
||||
const raw: Record<string, number> = await res.json();
|
||||
const out: Record<number, number> = {};
|
||||
for (const [k, v] of Object.entries(raw)) out[Number(k)] = v;
|
||||
peaks.set(out);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Patchbay state
|
||||
export const patchbay = writable<PatchbayState>({
|
||||
profiles: {},
|
||||
@@ -102,6 +117,8 @@ export async function initGraph() {
|
||||
unsubscribe = subscribe((graph: GraphMessage) => {
|
||||
applyGraph(graph);
|
||||
});
|
||||
|
||||
peakInterval = setInterval(pollPeaks, 100);
|
||||
}
|
||||
|
||||
export function destroyGraph() {
|
||||
@@ -110,6 +127,11 @@ export function destroyGraph() {
|
||||
unsubscribe = null;
|
||||
connected.set(false);
|
||||
}
|
||||
if (peakInterval) {
|
||||
clearInterval(peakInterval);
|
||||
peakInterval = null;
|
||||
}
|
||||
peaks.set({});
|
||||
}
|
||||
|
||||
// Patchbay operations
|
||||
|
||||
Reference in New Issue
Block a user