Merge feature/buffer-latency-and-fixes into master
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
saveProfile, loadProfile, deleteProfile,
|
||||
setNodeVolume, setNodeMute,
|
||||
createNullSink, createLoopback, loadModule,
|
||||
getQuantum, setQuantum,
|
||||
} from '../lib/stores';
|
||||
import type { Node, Port, Link } from '../lib/types';
|
||||
|
||||
@@ -41,6 +42,7 @@
|
||||
let showProfileDialog = $state(false);
|
||||
let showRuleDialog = $state(false);
|
||||
let showVirtualMenu = $state(false);
|
||||
let splitNodes = $state(false);
|
||||
let showNetworkDialog = $state<{ type: string } | null>(null);
|
||||
let netHost = $state('127.0.0.1');
|
||||
let netPort = $state('4713');
|
||||
@@ -50,6 +52,7 @@
|
||||
|
||||
// Positions
|
||||
let nodePositions = $state<Record<string, { x: number; y: number }>>({});
|
||||
let currentQuantum = $state(0);
|
||||
const POS_KEY = 'pwweb_positions';
|
||||
|
||||
function loadPositions() {
|
||||
@@ -136,20 +139,26 @@
|
||||
// Filter hidden nodes
|
||||
let visible = n.filter(nd => !isNodeHidden(nd.name));
|
||||
|
||||
// Merge nodes by prefix
|
||||
// Merge nodes by prefix (unless split mode)
|
||||
if (!splitNodes) {
|
||||
const merged = new Map<string, typeof visible[0]>();
|
||||
for (const nd of visible) {
|
||||
const mergedName = getMergedName(nd.name);
|
||||
const existing = merged.get(mergedName);
|
||||
if (existing) {
|
||||
// Merge port IDs
|
||||
existing.port_ids = [...new Set([...existing.port_ids, ...nd.port_ids])];
|
||||
existing.name = mergedName;
|
||||
const mergedInPorts = existing.port_ids.map(pid => portMap.get(pid)).filter((p): p is Port => !!p && p.mode === 'input');
|
||||
const mergedOutPorts = existing.port_ids.map(pid => portMap.get(pid)).filter((p): p is Port => !!p && p.mode === 'output');
|
||||
if (mergedInPorts.length > 0 && mergedOutPorts.length > 0) {
|
||||
existing.mode = 'duplex';
|
||||
}
|
||||
} else {
|
||||
merged.set(mergedName, { ...nd, name: mergedName });
|
||||
}
|
||||
}
|
||||
visible = Array.from(merged.values());
|
||||
}
|
||||
|
||||
const out = visible.filter(nd => nd.mode === 'output');
|
||||
const inp = visible.filter(nd => nd.mode === 'input');
|
||||
@@ -420,7 +429,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => { initGraph(); loadPositions(); });
|
||||
onMount(() => { initGraph(); loadPositions(); getQuantum().then(q => { currentQuantum = q; }); });
|
||||
onDestroy(() => { destroyGraph(); });
|
||||
</script>
|
||||
|
||||
@@ -457,9 +466,19 @@
|
||||
<!-- Dialogs -->
|
||||
<button onclick={() => { showHideDialog = !showHideDialog; showMergeDialog = false; showProfileDialog = false; showRuleDialog = false; }} title="Node hiding rules">Hide Nodes</button>
|
||||
<button onclick={() => { showMergeDialog = !showMergeDialog; showHideDialog = false; showProfileDialog = false; showRuleDialog = false; }} title="Node merging rules">Merge Nodes</button>
|
||||
<button class="toggle" class:active={splitNodes} onclick={() => { splitNodes = !splitNodes; }} title="Show input/output as separate nodes">Split</button>
|
||||
<button onclick={() => { showRuleDialog = !showRuleDialog; showHideDialog = false; showMergeDialog = false; showProfileDialog = false; }} title="Manage patchbay rules">Rules</button>
|
||||
<button onclick={() => { showProfileDialog = !showProfileDialog; showHideDialog = false; showMergeDialog = false; showRuleDialog = false; }} title="Save/load profiles">Profiles</button>
|
||||
<button onclick={() => { showVirtualMenu = !showVirtualMenu; showHideDialog = false; showMergeDialog = false; showProfileDialog = false; showRuleDialog = false; }} title="Add virtual device">+ Add</button>
|
||||
<span class="sep"></span>
|
||||
<label class="quantum-label">Buffer:
|
||||
<select class="quantum-select" onchange={(e) => { const q = Number((e.target as HTMLSelectElement).value); if (q > 0) { currentQuantum = q; setQuantum(q); } }}>
|
||||
<option value="0" selected={currentQuantum === 0}>default</option>
|
||||
{#each [32, 64, 128, 256, 512, 1024, 2048, 4096] as q}
|
||||
<option value={q} selected={currentQuantum === q}>{q}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<span class="stats">{$nodes.length}N {$ports.length}P {$links.length}L {#if $patchbay.pinned_connections.length > 0}{$patchbay.pinned_connections.length}p{/if}</span>
|
||||
</div>
|
||||
@@ -562,9 +581,10 @@
|
||||
{#each graphNodes as nd (nd.id)}
|
||||
{@const isSource = nd.mode === 'output'}
|
||||
{@const isSink = nd.mode === 'input'}
|
||||
{@const bg = isSource ? '#1a2a1a' : isSink ? '#2a1a1a' : '#1e1e2e'}
|
||||
{@const border = isSource ? '#4a9' : isSink ? '#a44' : '#555'}
|
||||
{@const headerBg = isSource ? '#263826' : isSink ? '#382626' : '#262638'}
|
||||
{@const isDuplex = nd.mode === 'duplex'}
|
||||
{@const bg = isSource ? '#1a2a1a' : isSink ? '#2a1a1a' : isDuplex ? '#1a1a2a' : '#1e1e2e'}
|
||||
{@const border = isSource ? '#4a9' : isSink ? '#a44' : isDuplex ? '#49a' : '#555'}
|
||||
{@const headerBg = isSource ? '#263826' : isSink ? '#382626' : isDuplex ? '#262638' : '#262638'}
|
||||
|
||||
<g class="node-group" data-node-id={String(nd.id)} filter="url(#shadow)">
|
||||
<rect x={nd.x} y={nd.y} width={nd.width} height={nd.height} rx="4" fill={bg} stroke={border} stroke-width="1" />
|
||||
@@ -821,8 +841,8 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrap { width: 100%; height: 100vh; background: #14141e; position: relative; overflow: hidden; }
|
||||
.canvas { width: 100%; height: 100%; display: block; cursor: default; position: absolute; top: 0; left: 0; z-index: 1; }
|
||||
.wrap { width: 100%; height: 100vh; background: #14141e; position: relative; overflow: hidden; user-select: none; -webkit-user-select: none; }
|
||||
.canvas { width: 100%; height: 100%; display: block; cursor: default; position: absolute; top: 0; left: 0; z-index: 1; user-select: none; -webkit-user-select: none; }
|
||||
|
||||
.toolbar {
|
||||
position: absolute; top: 0; left: 0; right: 0; z-index: 10;
|
||||
@@ -859,6 +879,12 @@
|
||||
.toolbar button:hover, .toggle:hover { background: #3a3a4e; }
|
||||
.toggle.active { background: #1a3a2a; border-color: #4a9; color: #6c9; }
|
||||
|
||||
.quantum-label { font-size: 10px; color: #888; display: flex; align-items: center; gap: 4px; }
|
||||
.quantum-select {
|
||||
padding: 1px 4px; background: #1a1a2e; border: 1px solid #444;
|
||||
color: #aaa; font-size: 10px; border-radius: 3px; font-family: monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
.stats { margin-left: auto; color: #555; }
|
||||
|
||||
/* Context menu */
|
||||
|
||||
@@ -441,4 +441,25 @@ export async function unloadModule(moduleId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
// Quantum (buffer size) control
|
||||
export async function getQuantum(): Promise<number> {
|
||||
try {
|
||||
const res = await fetch('/api/quantum');
|
||||
const data = await res.json();
|
||||
return data.quantum || 0;
|
||||
} catch { return 0; }
|
||||
}
|
||||
|
||||
export async function setQuantum(quantum: number) {
|
||||
try {
|
||||
await fetch('/api/quantum', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ quantum }),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[api] set-quantum failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
export { connectPorts, disconnectPorts };
|
||||
|
||||
@@ -603,6 +603,42 @@ void WebServer::setupRoutes() {
|
||||
}
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
|
||||
m_http.Options("/api/quantum", cors_handler);
|
||||
|
||||
// Get current quantum: GET /api/quantum
|
||||
m_http.Get("/api/quantum", [](const httplib::Request &, httplib::Response &res) {
|
||||
int quantum = 0;
|
||||
FILE *fp = popen("pw-metadata 0 default.clock.quantum 2>/dev/null | grep -oP \"value:'\\K[0-9]+\"", "r");
|
||||
if (fp) {
|
||||
char buf[32] = {};
|
||||
if (fgets(buf, sizeof(buf), fp))
|
||||
quantum = atoi(buf);
|
||||
pclose(fp);
|
||||
}
|
||||
char out[64];
|
||||
snprintf(out, sizeof(out), "{\"quantum\":%d}", quantum);
|
||||
res.set_content(out, "application/json");
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
|
||||
// Set quantum: POST /api/quantum {"quantum":256}
|
||||
m_http.Post("/api/quantum", [](const httplib::Request &req, httplib::Response &res) {
|
||||
int quantum = 0;
|
||||
if (sscanf(req.body.c_str(), "{\"quantum\":%d}", &quantum) == 1 && quantum > 0) {
|
||||
char cmd[128];
|
||||
snprintf(cmd, sizeof(cmd), "pw-metadata 0 default.clock.quantum %d 2>/dev/null", quantum);
|
||||
int ret = system(cmd);
|
||||
(void)ret;
|
||||
char out[64];
|
||||
snprintf(out, sizeof(out), "{\"ok\":true,\"quantum\":%d}", quantum);
|
||||
res.set_content(out, "application/json");
|
||||
} else {
|
||||
res.status = 400;
|
||||
res.set_content("{\"error\":\"invalid json\"}", "application/json");
|
||||
}
|
||||
res.set_header("Access-Control-Allow-Origin", "*");
|
||||
});
|
||||
}
|
||||
|
||||
// end of web_server.cpp
|
||||
|
||||
Reference in New Issue
Block a user