refactor(manifold): delete the OSC bridge twin and dead protocol legs
Phase 1 group 6 (S11, L16). - S11: deleted osc-bridge/bridge.mjs. It was not compiled output but a separate hand-written Node port of bridge.ts (node:dgram + ws vs Deno.listenDatagram). The completeness critic settled which twin survives: .github/workflows/osc-bridge.yml deno-compiles ONLY bridge.ts into the released cross-platform binaries, so bridge.ts plus those binaries are the distribution and the .mjs had no consumer in any workflow. - L16: dead protocol legs left over from the retired playground — sendState/sendWeights in osc-client.ts, the legacy bare-array branch in the surviving bridge, the unreceivable /nisps/state path, and the unused module-output listeners. Note for the docs phase: docs/specs/backends-spec.md still calls bridge.mjs "already compiled" (doubly false now), and vcv/ still pushes /nisps/state via OscServer::sendState with no manifold-side counterpart — flagged, not touched. Gates: run-all-tests.sh ALL GREEN.
This commit is contained in:
parent
9b686eb312
commit
232d51039d
5 changed files with 25 additions and 381 deletions
|
|
@ -1,300 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
// NISPS <-> OSC Bridge (bidirectional)
|
|
||||||
// WebSocket server that bridges the browser webapp and OSC-capable software
|
|
||||||
// (VCV Rack MEMLNaut module, SuperCollider, etc.).
|
|
||||||
//
|
|
||||||
// Usage:
|
|
||||||
// node bridge.mjs # defaults
|
|
||||||
// node bridge.mjs --osc-host 192.168.1.5 # send to another machine
|
|
||||||
// node bridge.mjs --osc-port 9000 # target port
|
|
||||||
// node bridge.mjs --ws-port 8765 # WebSocket listen port
|
|
||||||
// node bridge.mjs --listen-port 9001 # UDP listen port for incoming OSC
|
|
||||||
// node bridge.mjs --osc-prefix /nisps # OSC address prefix
|
|
||||||
// node bridge.mjs --bundle # send OSC bundles
|
|
||||||
//
|
|
||||||
// Webapp -> Bridge -> OSC target (param updates, state, weights)
|
|
||||||
// OSC target -> Bridge -> Webapp (output values, input values)
|
|
||||||
|
|
||||||
import { createSocket } from 'node:dgram';
|
|
||||||
import { WebSocketServer } from 'ws';
|
|
||||||
|
|
||||||
// ---- CLI args ----
|
|
||||||
const args = process.argv.slice(2);
|
|
||||||
function flag(name, fallback) {
|
|
||||||
const idx = args.indexOf(`--${name}`);
|
|
||||||
if (idx === -1) return fallback;
|
|
||||||
return args[idx + 1] ?? fallback;
|
|
||||||
}
|
|
||||||
const hasFlag = (name) => args.includes(`--${name}`);
|
|
||||||
|
|
||||||
const WS_PORT = parseInt(flag('ws-port', '8765'), 10);
|
|
||||||
const OSC_HOST = flag('osc-host', '127.0.0.1');
|
|
||||||
const OSC_PORT = parseInt(flag('osc-port', '9000'), 10);
|
|
||||||
const OSC_PREFIX = flag('osc-prefix', '/nisps');
|
|
||||||
const LISTEN_PORT = parseInt(flag('listen-port', '9001'), 10);
|
|
||||||
const USE_BUNDLES = hasFlag('bundle');
|
|
||||||
|
|
||||||
// ---- OSC encoding (minimal, no dependencies) ----
|
|
||||||
|
|
||||||
function oscPadded(len) {
|
|
||||||
return len + (4 - (len % 4)) % 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
function oscString(str) {
|
|
||||||
const len = str.length + 1; // null terminator
|
|
||||||
const padded = oscPadded(len);
|
|
||||||
const buf = Buffer.alloc(padded);
|
|
||||||
buf.write(str, 'ascii');
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
function oscFloat(val) {
|
|
||||||
const buf = Buffer.alloc(4);
|
|
||||||
buf.writeFloatBE(val, 0);
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
function oscMessage(address, value) {
|
|
||||||
return Buffer.concat([
|
|
||||||
oscString(address),
|
|
||||||
oscString(',f'),
|
|
||||||
oscFloat(value),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** A single OSC message carrying N floats (one ",fff…" message, not N messages). */
|
|
||||||
function oscMessageFloats(address, values) {
|
|
||||||
const tags = ',' + 'f'.repeat(values.length);
|
|
||||||
return Buffer.concat([
|
|
||||||
oscString(address),
|
|
||||||
oscString(tags),
|
|
||||||
...values.map((v) => oscFloat(v)),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function oscMessageString(address, value) {
|
|
||||||
return Buffer.concat([
|
|
||||||
oscString(address),
|
|
||||||
oscString(',s'),
|
|
||||||
oscString(value),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function oscBundle(messages) {
|
|
||||||
const header = oscString('#bundle');
|
|
||||||
const timetag = Buffer.alloc(8);
|
|
||||||
timetag.writeUInt32BE(1, 0);
|
|
||||||
|
|
||||||
const parts = [header, timetag];
|
|
||||||
for (const msg of messages) {
|
|
||||||
const size = Buffer.alloc(4);
|
|
||||||
size.writeUInt32BE(msg.length, 0);
|
|
||||||
parts.push(size, msg);
|
|
||||||
}
|
|
||||||
return Buffer.concat(parts);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- OSC decoding ----
|
|
||||||
|
|
||||||
function readOscString(buf, offset) {
|
|
||||||
let end = offset;
|
|
||||||
while (end < buf.length && buf[end] !== 0) end++;
|
|
||||||
const str = buf.toString('ascii', offset, end);
|
|
||||||
const nextOffset = offset + oscPadded(end - offset + 1);
|
|
||||||
return [str, nextOffset];
|
|
||||||
}
|
|
||||||
|
|
||||||
function readOscFloat(buf, offset) {
|
|
||||||
if (offset + 4 > buf.length) return [0, offset + 4];
|
|
||||||
return [buf.readFloatBE(offset), offset + 4];
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseOscMessage(buf) {
|
|
||||||
if (buf.length < 4) return null;
|
|
||||||
let offset = 0;
|
|
||||||
|
|
||||||
const [address, off1] = readOscString(buf, offset);
|
|
||||||
if (!address.startsWith('/')) return null;
|
|
||||||
offset = off1;
|
|
||||||
|
|
||||||
const [tags, off2] = readOscString(buf, offset);
|
|
||||||
if (!tags.startsWith(',')) return null;
|
|
||||||
offset = off2;
|
|
||||||
|
|
||||||
const types = tags.slice(1);
|
|
||||||
const args = [];
|
|
||||||
|
|
||||||
for (const t of types) {
|
|
||||||
if (t === 'f') {
|
|
||||||
const [val, off] = readOscFloat(buf, offset);
|
|
||||||
args.push(val);
|
|
||||||
offset = off;
|
|
||||||
} else if (t === 's') {
|
|
||||||
const [val, off] = readOscString(buf, offset);
|
|
||||||
args.push(val);
|
|
||||||
offset = off;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { address, types, args };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- UDP sockets ----
|
|
||||||
|
|
||||||
// Outgoing: sends OSC to target
|
|
||||||
const udpSend = createSocket('udp4');
|
|
||||||
|
|
||||||
function sendOSC(address, value) {
|
|
||||||
const msg = oscMessage(address, value);
|
|
||||||
udpSend.send(msg, OSC_PORT, OSC_HOST);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendOSCString(address, value) {
|
|
||||||
const msg = oscMessageString(address, value);
|
|
||||||
udpSend.send(msg, OSC_PORT, OSC_HOST);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendOSCFloats(address, values) {
|
|
||||||
const msg = oscMessageFloats(address, values);
|
|
||||||
udpSend.send(msg, OSC_PORT, OSC_HOST);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendOSCBundle(params) {
|
|
||||||
const messages = params.map(([name, value]) =>
|
|
||||||
oscMessage(`${OSC_PREFIX}/${name}`, value)
|
|
||||||
);
|
|
||||||
const bundle = oscBundle(messages);
|
|
||||||
udpSend.send(bundle, OSC_PORT, OSC_HOST);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Incoming: listens for OSC from target
|
|
||||||
const udpRecv = createSocket('udp4');
|
|
||||||
udpRecv.bind(LISTEN_PORT, '0.0.0.0');
|
|
||||||
|
|
||||||
udpRecv.on('message', (buf, _rinfo) => {
|
|
||||||
const msg = parseOscMessage(buf);
|
|
||||||
if (!msg) return;
|
|
||||||
|
|
||||||
const wsMsg = { type: 'osc', address: msg.address };
|
|
||||||
|
|
||||||
if (msg.address === `${OSC_PREFIX}/output` || msg.address === '/nisps/output') {
|
|
||||||
wsMsg.type = 'outputs';
|
|
||||||
wsMsg.values = msg.args.filter(a => typeof a === 'number');
|
|
||||||
} else if (msg.address === `${OSC_PREFIX}/input` || msg.address === '/nisps/input') {
|
|
||||||
wsMsg.type = 'inputs';
|
|
||||||
wsMsg.values = msg.args.filter(a => typeof a === 'number');
|
|
||||||
} else {
|
|
||||||
wsMsg.args = msg.args;
|
|
||||||
}
|
|
||||||
|
|
||||||
broadcastToWs(JSON.stringify(wsMsg));
|
|
||||||
});
|
|
||||||
|
|
||||||
udpRecv.on('error', (err) => {
|
|
||||||
console.error('[udp] Listen error:', err.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- WebSocket server ----
|
|
||||||
const wss = new WebSocketServer({ port: WS_PORT });
|
|
||||||
const wsClients = new Set();
|
|
||||||
|
|
||||||
function broadcastToWs(data) {
|
|
||||||
for (const ws of wsClients) {
|
|
||||||
try {
|
|
||||||
if (ws.readyState === 1) { // OPEN
|
|
||||||
ws.send(data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wss.on('connection', (ws) => {
|
|
||||||
wsClients.add(ws);
|
|
||||||
console.log(`[ws] Client connected (${wsClients.size} total)`);
|
|
||||||
|
|
||||||
ws.send(JSON.stringify({
|
|
||||||
type: 'info',
|
|
||||||
message: `OSC <-> ${OSC_HOST}:${OSC_PORT} (prefix: ${OSC_PREFIX}, listen: ${LISTEN_PORT})`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
ws.on('message', (raw) => {
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(raw);
|
|
||||||
|
|
||||||
// Structured message format: { type, payload }
|
|
||||||
if (data && typeof data === 'object' && data.type) {
|
|
||||||
switch (data.type) {
|
|
||||||
case 'state':
|
|
||||||
sendOSCString(`${OSC_PREFIX}/state`, JSON.stringify(data.payload));
|
|
||||||
return;
|
|
||||||
case 'weights':
|
|
||||||
sendOSCString(`${OSC_PREFIX}/weights`, JSON.stringify(data.payload));
|
|
||||||
return;
|
|
||||||
case 'input':
|
|
||||||
// Current input VECTOR as ONE multi-float message to /nisps/input.
|
|
||||||
if (Array.isArray(data.payload)) {
|
|
||||||
sendOSCFloats(`${OSC_PREFIX}/input`, data.payload);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
case 'feedback':
|
|
||||||
// Verdict op as OSC string to /nisps/feedback (trains the module).
|
|
||||||
sendOSCString(`${OSC_PREFIX}/feedback`, JSON.stringify(data.payload));
|
|
||||||
return;
|
|
||||||
case 'params':
|
|
||||||
if (Array.isArray(data.payload)) {
|
|
||||||
if (USE_BUNDLES) {
|
|
||||||
sendOSCBundle(data.payload);
|
|
||||||
} else {
|
|
||||||
for (const [name, value] of data.payload) {
|
|
||||||
sendOSC(`${OSC_PREFIX}/${name}`, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy format: [[paramName, value], ...]
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
if (USE_BUNDLES) {
|
|
||||||
sendOSCBundle(data);
|
|
||||||
} else {
|
|
||||||
for (const [name, value] of data) {
|
|
||||||
sendOSC(`${OSC_PREFIX}/${name}`, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[ws] Bad message:', e.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on('close', () => {
|
|
||||||
wsClients.delete(ws);
|
|
||||||
console.log(`[ws] Client disconnected (${wsClients.size} remaining)`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`
|
|
||||||
NISPS <-> OSC Bridge (bidirectional)
|
|
||||||
────────────────────────────────────
|
|
||||||
WebSocket: ws://localhost:${WS_PORT}
|
|
||||||
OSC target: ${OSC_HOST}:${OSC_PORT}
|
|
||||||
OSC listen: 0.0.0.0:${LISTEN_PORT}
|
|
||||||
Prefix: ${OSC_PREFIX}
|
|
||||||
Mode: ${USE_BUNDLES ? 'bundles' : 'individual messages'}
|
|
||||||
|
|
||||||
Webapp -> VCV:
|
|
||||||
params: [[name, value], ...] or { type: "params", payload: [...] }
|
|
||||||
state: { type: "state", payload: <JSON> }
|
|
||||||
weights: { type: "weights", payload: <JSON> }
|
|
||||||
|
|
||||||
VCV -> Webapp:
|
|
||||||
/nisps/output <f...f> -> { type: "outputs", values: [...] }
|
|
||||||
/nisps/input <f...f> -> { type: "inputs", values: [...] }
|
|
||||||
|
|
||||||
Waiting for connections...
|
|
||||||
`);
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
// WebSocket server that bridges the browser webapp and OSC-capable software
|
// WebSocket server that bridges the browser webapp and OSC-capable software
|
||||||
// (VCV Rack MEMLNaut module, SuperCollider, etc.).
|
// (VCV Rack MEMLNaut module, SuperCollider, etc.).
|
||||||
//
|
//
|
||||||
// Webapp -> Bridge -> OSC target (param updates, state, weights)
|
// Webapp -> Bridge -> OSC target (param updates, input vector, feedback)
|
||||||
// OSC target -> Bridge -> Webapp (output values, input values)
|
// OSC target -> Bridge -> Webapp (output values, input values)
|
||||||
//
|
//
|
||||||
// Run with Deno:
|
// Run with Deno:
|
||||||
|
|
@ -20,8 +20,6 @@
|
||||||
//
|
//
|
||||||
// OSC address format:
|
// OSC address format:
|
||||||
// /nisps/<param_name> <float> (webapp -> target)
|
// /nisps/<param_name> <float> (webapp -> target)
|
||||||
// /nisps/state <string> (webapp -> target: full JSON state)
|
|
||||||
// /nisps/weights <string> (webapp -> target: weights JSON)
|
|
||||||
// /nisps/input <f...f> (webapp -> target: input vector, ONE message;
|
// /nisps/input <f...f> (webapp -> target: input vector, ONE message;
|
||||||
// also target -> webapp for visualisation)
|
// also target -> webapp for visualisation)
|
||||||
// /nisps/feedback <string> (webapp -> target: verdict op JSON —
|
// /nisps/feedback <string> (webapp -> target: verdict op JSON —
|
||||||
|
|
@ -253,17 +251,9 @@ function handleWs(ws: WebSocket): void {
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(e.data as string);
|
const data = JSON.parse(e.data as string);
|
||||||
|
|
||||||
// New structured message format: { type, payload }
|
// Structured message format: { type, payload }
|
||||||
if (data && typeof data === "object" && data.type) {
|
if (data && typeof data === "object" && data.type) {
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
case "state":
|
|
||||||
// Send full state JSON as OSC string to /nisps/state
|
|
||||||
sendOSCString(`${OSC_PREFIX}/state`, JSON.stringify(data.payload));
|
|
||||||
return;
|
|
||||||
case "weights":
|
|
||||||
// Send weights JSON as OSC string to /nisps/weights
|
|
||||||
sendOSCString(`${OSC_PREFIX}/weights`, JSON.stringify(data.payload));
|
|
||||||
return;
|
|
||||||
case "input":
|
case "input":
|
||||||
// Current input VECTOR as ONE multi-float message to /nisps/input
|
// Current input VECTOR as ONE multi-float message to /nisps/input
|
||||||
// (browser drives the VCV module's inputs in bridged mode).
|
// (browser drives the VCV module's inputs in bridged mode).
|
||||||
|
|
@ -277,7 +267,7 @@ function handleWs(ws: WebSocket): void {
|
||||||
sendOSCString(`${OSC_PREFIX}/feedback`, JSON.stringify(data.payload));
|
sendOSCString(`${OSC_PREFIX}/feedback`, JSON.stringify(data.payload));
|
||||||
return;
|
return;
|
||||||
case "params":
|
case "params":
|
||||||
// Legacy batch format embedded in structured message
|
// Per-param batch: one single-float message per [name, value] entry
|
||||||
if (Array.isArray(data.payload)) {
|
if (Array.isArray(data.payload)) {
|
||||||
if (USE_BUNDLES) {
|
if (USE_BUNDLES) {
|
||||||
sendOSCBundle(data.payload);
|
sendOSCBundle(data.payload);
|
||||||
|
|
@ -290,17 +280,6 @@ function handleWs(ws: WebSocket): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy format: [[paramName, value], ...]
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
if (USE_BUNDLES) {
|
|
||||||
sendOSCBundle(data);
|
|
||||||
} else {
|
|
||||||
for (const [name, value] of data) {
|
|
||||||
sendOSC(`${OSC_PREFIX}/${name}`, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[ws] Bad message:", (err as Error).message);
|
console.error("[ws] Bad message:", (err as Error).message);
|
||||||
}
|
}
|
||||||
|
|
@ -319,23 +298,18 @@ async function udpReceiveLoop(): Promise<void> {
|
||||||
const msg = parseOscMessage(data);
|
const msg = parseOscMessage(data);
|
||||||
if (!msg) continue;
|
if (!msg) continue;
|
||||||
|
|
||||||
// Relay parsed OSC messages to all connected WebSocket clients
|
// Relay the module's output/input vectors to all connected WebSocket
|
||||||
const wsMsg: Record<string, unknown> = { type: "osc", address: msg.address };
|
// clients. Other addresses have no browser receiver — drop them.
|
||||||
|
let type: "outputs" | "inputs";
|
||||||
if (msg.address === `${OSC_PREFIX}/output` || msg.address === "/nisps/output") {
|
if (msg.address === `${OSC_PREFIX}/output` || msg.address === "/nisps/output") {
|
||||||
// Float array of outputs
|
type = "outputs";
|
||||||
wsMsg.type = "outputs";
|
|
||||||
wsMsg.values = msg.args.filter((a): a is number => typeof a === "number");
|
|
||||||
} else if (msg.address === `${OSC_PREFIX}/input` || msg.address === "/nisps/input") {
|
} else if (msg.address === `${OSC_PREFIX}/input` || msg.address === "/nisps/input") {
|
||||||
// Float array of inputs
|
type = "inputs";
|
||||||
wsMsg.type = "inputs";
|
|
||||||
wsMsg.values = msg.args.filter((a): a is number => typeof a === "number");
|
|
||||||
} else {
|
} else {
|
||||||
// Generic OSC message
|
continue;
|
||||||
wsMsg.args = msg.args;
|
|
||||||
}
|
}
|
||||||
|
const values = msg.args.filter((a): a is number => typeof a === "number");
|
||||||
broadcastToWs(JSON.stringify(wsMsg));
|
broadcastToWs(JSON.stringify({ type, values }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -365,9 +339,9 @@ NISPS <-> OSC Bridge (bidirectional)
|
||||||
Mode: ${USE_BUNDLES ? "bundles" : "individual messages"}
|
Mode: ${USE_BUNDLES ? "bundles" : "individual messages"}
|
||||||
|
|
||||||
Webapp -> VCV:
|
Webapp -> VCV:
|
||||||
params: [[name, value], ...] or { type: "params", payload: [...] }
|
params: { type: "params", payload: [[name, value], ...] }
|
||||||
state: { type: "state", payload: <JSON> }
|
input: { type: "input", payload: [f, ...] }
|
||||||
weights: { type: "weights", payload: <JSON> }
|
feedback: { type: "feedback", payload: <op JSON> }
|
||||||
|
|
||||||
VCV -> Webapp:
|
VCV -> Webapp:
|
||||||
/nisps/output <f...f> -> { type: "outputs", values: [...] }
|
/nisps/output <f...f> -> { type: "outputs", values: [...] }
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ See `docs/specs/backends-spec.md` for the authoritative design.
|
||||||
| `midi-backend.ts` | `WebMidiBackend` — real Web MIDI CC out (per-output CC#/channel/range/name, throttled + dead-zone). |
|
| `midi-backend.ts` | `WebMidiBackend` — real Web MIDI CC out (per-output CC#/channel/range/name, throttled + dead-zone). |
|
||||||
| `osc-client.ts` | `NispsOscClient` — WS transport to the Deno OSC bridge (JSON protocol, auto-reconnect). |
|
| `osc-client.ts` | `NispsOscClient` — WS transport to the Deno OSC bridge (JSON protocol, auto-reconnect). |
|
||||||
| `osc-backend.ts` | `OscBridgeBackend` — OSC out over WS; per-output address path + physical range. |
|
| `osc-backend.ts` | `OscBridgeBackend` — OSC out over WS; per-output address path + physical range. |
|
||||||
| `vcv-backend.ts` | `VcvBackend` — drives + **trains** the VCV Rack NISPS module over the OSC↔WS bridge (streams `/nisps/input`, forwards the verdict loop to `/nisps/feedback`, receives `/nisps/output` + `/nisps/state`). |
|
| `vcv-backend.ts` | `VcvBackend` — drives + **trains** the VCV Rack NISPS module over the OSC↔WS bridge (streams `/nisps/input`, forwards the verdict loop to `/nisps/feedback`, receives `/nisps/output` / `/nisps/input` as module-alive proof). |
|
||||||
| `passthrough-backend.ts` | No-op sink for synth (plays in-engine) / particles (rAF consumer) / editor. |
|
| `passthrough-backend.ts` | No-op sink for synth (plays in-engine) / particles (rAF consumer) / editor. |
|
||||||
| `presets.ts` | Named per-backend output-config presets (localStorage, per-backend namespace). |
|
| `presets.ts` | Named per-backend output-config presets (localStorage, per-backend namespace). |
|
||||||
| `useBackendManager.ts` | Thin React binding: builds `BackendContext` from the store, switches Mode, surfaces status. |
|
| `useBackendManager.ts` | Thin React binding: builds `BackendContext` from the store, switches Mode, surfaces status. |
|
||||||
|
|
@ -40,8 +40,7 @@ the OSC backend shows "bridge not running" (it auto-reconnects):
|
||||||
cd manifold/osc-bridge
|
cd manifold/osc-bridge
|
||||||
deno run --allow-net bridge.ts
|
deno run --allow-net bridge.ts
|
||||||
# --osc-host 127.0.0.1 --osc-port 9000 --ws-port 8765 --listen-port 9001
|
# --osc-host 127.0.0.1 --osc-port 9000 --ws-port 8765 --listen-port 9001
|
||||||
# or, without Deno:
|
# or, without Deno: use a compiled binary (compile.sh / the osc-bridge release)
|
||||||
node bridge.mjs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Default bridge URL: `ws://localhost:8765` (configurable in the OSC config panel).
|
Default bridge URL: `ws://localhost:8765` (configurable in the OSC config panel).
|
||||||
|
|
@ -60,8 +59,8 @@ When Mode = **VCV**, the browser is authoritative in *bridged mode*: the
|
||||||
output[] }`), so the module's embedded net trains in lock-step with the
|
output[] }`), so the module's embedded net trains in lock-step with the
|
||||||
browser session.
|
browser session.
|
||||||
|
|
||||||
It receives **`/nisps/output`** + **`/nisps/state`** back for status /
|
It receives **`/nisps/output`** / **`/nisps/input`** back as proof the module is
|
||||||
visualisation. The verdict forwarding is gated in `BackendManager.forwardFeedback`
|
alive. The verdict forwarding is gated in `BackendManager.forwardFeedback`
|
||||||
— a no-op unless VCV is the active backend (otherwise the browser engine is the
|
— a no-op unless VCV is the active backend (otherwise the browser engine is the
|
||||||
learner). Both processes are required:
|
learner). Both processes are required:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
*
|
*
|
||||||
* browser → bridge:
|
* browser → bridge:
|
||||||
* { type: 'params', payload: [[name, value], ...] } (per-param floats)
|
* { type: 'params', payload: [[name, value], ...] } (per-param floats)
|
||||||
* { type: 'state', payload: <JSON> } (full state)
|
* { type: 'input', payload: [f, ...] } (input vector)
|
||||||
* { type: 'weights',payload: <JSON> } (weights only)
|
* { type: 'feedback', payload: <op JSON> } (verdict op)
|
||||||
* bridge → browser:
|
* bridge → browser:
|
||||||
* { type: 'outputs', values: [...] }
|
* { type: 'outputs', values: [...] }
|
||||||
* { type: 'inputs', values: [...] }
|
* { type: 'inputs', values: [...] }
|
||||||
|
|
@ -113,16 +113,6 @@ export class NispsOscClient {
|
||||||
this.send({ type: 'params', payload: params });
|
this.send({ type: 'params', payload: params });
|
||||||
}
|
}
|
||||||
|
|
||||||
sendState(stateJson: object | string): void {
|
|
||||||
const payload = typeof stateJson === 'string' ? JSON.parse(stateJson) : stateJson;
|
|
||||||
this.send({ type: 'state', payload });
|
|
||||||
}
|
|
||||||
|
|
||||||
sendWeights(weightsObj: object | string): void {
|
|
||||||
const payload = typeof weightsObj === 'string' ? JSON.parse(weightsObj) : weightsObj;
|
|
||||||
this.send({ type: 'weights', payload });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send the current input VECTOR as ONE multi-float OSC message to
|
* Send the current input VECTOR as ONE multi-float OSC message to
|
||||||
* `<prefix>/input` (the bridge `input` verb). Used in VCV bridged mode so the
|
* `<prefix>/input` (the bridge `input` verb). Used in VCV bridged mode so the
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,8 @@
|
||||||
* string — { op, spread, input[], output[] }
|
* string — { op, spread, input[], output[] }
|
||||||
*
|
*
|
||||||
* module → browser
|
* module → browser
|
||||||
* /nisps/output <f…f> module's live outputs (status / visualisation)
|
* /nisps/output <f…f> module's live outputs (proves the module is alive)
|
||||||
* /nisps/input <f…f> module's live inputs (echo / status)
|
* /nisps/input <f…f> module's live inputs (echo — same alive proof)
|
||||||
* /nisps/state <json> module status snapshot (surfaced as a message)
|
|
||||||
*
|
*
|
||||||
* The bridge process AND the VCV module must both be running — until the WS
|
* The bridge process AND the VCV module must both be running — until the WS
|
||||||
* connects we surface "bridge not running"; until the module replies we stay
|
* connects we surface "bridge not running"; until the module replies we stay
|
||||||
|
|
@ -71,13 +70,10 @@ export class VcvBackend implements OutputBackend {
|
||||||
private lastSendMs = 0;
|
private lastSendMs = 0;
|
||||||
private lastInputSent: [number, number] = [-1, -1];
|
private lastInputSent: [number, number] = [-1, -1];
|
||||||
|
|
||||||
/** Latest module-reported outputs (for visualisation), null until first echo. */
|
|
||||||
private moduleOutputs: number[] | null = null;
|
|
||||||
private gotModuleReply = false;
|
private gotModuleReply = false;
|
||||||
|
|
||||||
private statusState: BackendStatus = { state: 'idle', message: 'VCV idle' };
|
private statusState: BackendStatus = { state: 'idle', message: 'VCV idle' };
|
||||||
private statusListeners = new Set<(s: BackendStatus) => void>();
|
private statusListeners = new Set<(s: BackendStatus) => void>();
|
||||||
private outputListeners = new Set<(v: number[]) => void>();
|
|
||||||
private offConn: (() => void) | null = null;
|
private offConn: (() => void) | null = null;
|
||||||
private offInfo: (() => void) | null = null;
|
private offInfo: (() => void) | null = null;
|
||||||
private offOutputs: (() => void) | null = null;
|
private offOutputs: (() => void) | null = null;
|
||||||
|
|
@ -113,8 +109,8 @@ export class VcvBackend implements OutputBackend {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Module → browser: a reply on either channel proves the module is alive.
|
// Module → browser: a reply on either channel proves the module is alive.
|
||||||
this.offOutputs = this.client.onOutputsReceived((v) => this.onModuleReply(v, true));
|
this.offOutputs = this.client.onOutputsReceived(() => this.onModuleReply());
|
||||||
this.offInputs = this.client.onInputsReceived((v) => this.onModuleReply(v, false));
|
this.offInputs = this.client.onInputsReceived(() => this.onModuleReply());
|
||||||
|
|
||||||
this.setStatus({ state: 'connecting', message: `Connecting to VCV bridge (${this.client.url})…` });
|
this.setStatus({ state: 'connecting', message: `Connecting to VCV bridge (${this.client.url})…` });
|
||||||
this.client.connect({ reconnect: true }).catch(() => {
|
this.client.connect({ reconnect: true }).catch(() => {
|
||||||
|
|
@ -208,26 +204,11 @@ export class VcvBackend implements OutputBackend {
|
||||||
return spec?.bipolar ? v * 10 - 5 : v * 10;
|
return spec?.bipolar ? v * 10 - 5 : v * 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Latest module-reported output vector for visualisation (may be null). */
|
private onModuleReply(): void {
|
||||||
moduleStatusOutputs(): number[] | null {
|
|
||||||
return this.moduleOutputs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Subscribe to module-reported outputs (visualisation feed). */
|
|
||||||
onModuleOutputs(cb: (v: number[]) => void): () => void {
|
|
||||||
this.outputListeners.add(cb);
|
|
||||||
return () => this.outputListeners.delete(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onModuleReply(v: number[], isOutput: boolean): void {
|
|
||||||
if (!this.gotModuleReply) {
|
if (!this.gotModuleReply) {
|
||||||
this.gotModuleReply = true;
|
this.gotModuleReply = true;
|
||||||
this.setStatus({ state: 'ready', message: `VCV module connected (${this.client.url})` });
|
this.setStatus({ state: 'ready', message: `VCV module connected (${this.client.url})` });
|
||||||
}
|
}
|
||||||
if (isOutput) {
|
|
||||||
this.moduleOutputs = v;
|
|
||||||
for (const cb of this.outputListeners) cb(v);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async teardown(): Promise<void> {
|
async teardown(): Promise<void> {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue