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
|
||||
// (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)
|
||||
//
|
||||
// Run with Deno:
|
||||
|
|
@ -20,8 +20,6 @@
|
|||
//
|
||||
// OSC address format:
|
||||
// /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;
|
||||
// also target -> webapp for visualisation)
|
||||
// /nisps/feedback <string> (webapp -> target: verdict op JSON —
|
||||
|
|
@ -253,17 +251,9 @@ function handleWs(ws: WebSocket): void {
|
|||
try {
|
||||
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) {
|
||||
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":
|
||||
// Current input VECTOR as ONE multi-float message to /nisps/input
|
||||
// (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));
|
||||
return;
|
||||
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 (USE_BUNDLES) {
|
||||
sendOSCBundle(data.payload);
|
||||
|
|
@ -290,17 +280,6 @@ function handleWs(ws: WebSocket): void {
|
|||
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) {
|
||||
console.error("[ws] Bad message:", (err as Error).message);
|
||||
}
|
||||
|
|
@ -319,23 +298,18 @@ async function udpReceiveLoop(): Promise<void> {
|
|||
const msg = parseOscMessage(data);
|
||||
if (!msg) continue;
|
||||
|
||||
// Relay parsed OSC messages to all connected WebSocket clients
|
||||
const wsMsg: Record<string, unknown> = { type: "osc", address: msg.address };
|
||||
|
||||
// Relay the module's output/input vectors to all connected WebSocket
|
||||
// clients. Other addresses have no browser receiver — drop them.
|
||||
let type: "outputs" | "inputs";
|
||||
if (msg.address === `${OSC_PREFIX}/output` || msg.address === "/nisps/output") {
|
||||
// Float array of outputs
|
||||
wsMsg.type = "outputs";
|
||||
wsMsg.values = msg.args.filter((a): a is number => typeof a === "number");
|
||||
type = "outputs";
|
||||
} else if (msg.address === `${OSC_PREFIX}/input` || msg.address === "/nisps/input") {
|
||||
// Float array of inputs
|
||||
wsMsg.type = "inputs";
|
||||
wsMsg.values = msg.args.filter((a): a is number => typeof a === "number");
|
||||
type = "inputs";
|
||||
} else {
|
||||
// Generic OSC message
|
||||
wsMsg.args = msg.args;
|
||||
continue;
|
||||
}
|
||||
|
||||
broadcastToWs(JSON.stringify(wsMsg));
|
||||
const values = msg.args.filter((a): a is number => typeof a === "number");
|
||||
broadcastToWs(JSON.stringify({ type, values }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -365,9 +339,9 @@ NISPS <-> OSC Bridge (bidirectional)
|
|||
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> }
|
||||
params: { type: "params", payload: [[name, value], ...] }
|
||||
input: { type: "input", payload: [f, ...] }
|
||||
feedback: { type: "feedback", payload: <op JSON> }
|
||||
|
||||
VCV -> Webapp:
|
||||
/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). |
|
||||
| `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. |
|
||||
| `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. |
|
||||
| `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. |
|
||||
|
|
@ -40,8 +40,7 @@ the OSC backend shows "bridge not running" (it auto-reconnects):
|
|||
cd manifold/osc-bridge
|
||||
deno run --allow-net bridge.ts
|
||||
# --osc-host 127.0.0.1 --osc-port 9000 --ws-port 8765 --listen-port 9001
|
||||
# or, without Deno:
|
||||
node bridge.mjs
|
||||
# or, without Deno: use a compiled binary (compile.sh / the osc-bridge release)
|
||||
```
|
||||
|
||||
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
|
||||
browser session.
|
||||
|
||||
It receives **`/nisps/output`** + **`/nisps/state`** back for status /
|
||||
visualisation. The verdict forwarding is gated in `BackendManager.forwardFeedback`
|
||||
It receives **`/nisps/output`** / **`/nisps/input`** back as proof the module is
|
||||
alive. The verdict forwarding is gated in `BackendManager.forwardFeedback`
|
||||
— a no-op unless VCV is the active backend (otherwise the browser engine is the
|
||||
learner). Both processes are required:
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
*
|
||||
* browser → bridge:
|
||||
* { type: 'params', payload: [[name, value], ...] } (per-param floats)
|
||||
* { type: 'state', payload: <JSON> } (full state)
|
||||
* { type: 'weights',payload: <JSON> } (weights only)
|
||||
* { type: 'input', payload: [f, ...] } (input vector)
|
||||
* { type: 'feedback', payload: <op JSON> } (verdict op)
|
||||
* bridge → browser:
|
||||
* { type: 'outputs', values: [...] }
|
||||
* { type: 'inputs', values: [...] }
|
||||
|
|
@ -113,16 +113,6 @@ export class NispsOscClient {
|
|||
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
|
||||
* `<prefix>/input` (the bridge `input` verb). Used in VCV bridged mode so the
|
||||
|
|
|
|||
|
|
@ -18,9 +18,8 @@
|
|||
* string — { op, spread, input[], output[] }
|
||||
*
|
||||
* module → browser
|
||||
* /nisps/output <f…f> module's live outputs (status / visualisation)
|
||||
* /nisps/input <f…f> module's live inputs (echo / status)
|
||||
* /nisps/state <json> module status snapshot (surfaced as a message)
|
||||
* /nisps/output <f…f> module's live outputs (proves the module is alive)
|
||||
* /nisps/input <f…f> module's live inputs (echo — same alive proof)
|
||||
*
|
||||
* 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
|
||||
|
|
@ -71,13 +70,10 @@ export class VcvBackend implements OutputBackend {
|
|||
private lastSendMs = 0;
|
||||
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 statusState: BackendStatus = { state: 'idle', message: 'VCV idle' };
|
||||
private statusListeners = new Set<(s: BackendStatus) => void>();
|
||||
private outputListeners = new Set<(v: number[]) => void>();
|
||||
private offConn: (() => void) | null = null;
|
||||
private offInfo: (() => 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.
|
||||
this.offOutputs = this.client.onOutputsReceived((v) => this.onModuleReply(v, true));
|
||||
this.offInputs = this.client.onInputsReceived((v) => this.onModuleReply(v, false));
|
||||
this.offOutputs = this.client.onOutputsReceived(() => this.onModuleReply());
|
||||
this.offInputs = this.client.onInputsReceived(() => this.onModuleReply());
|
||||
|
||||
this.setStatus({ state: 'connecting', message: `Connecting to VCV bridge (${this.client.url})…` });
|
||||
this.client.connect({ reconnect: true }).catch(() => {
|
||||
|
|
@ -208,26 +204,11 @@ export class VcvBackend implements OutputBackend {
|
|||
return spec?.bipolar ? v * 10 - 5 : v * 10;
|
||||
}
|
||||
|
||||
/** Latest module-reported output vector for visualisation (may be null). */
|
||||
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 {
|
||||
private onModuleReply(): void {
|
||||
if (!this.gotModuleReply) {
|
||||
this.gotModuleReply = true;
|
||||
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> {
|
||||
|
|
|
|||
Loading…
Reference in a new issue