#!/usr/bin/env node // NISPS → OSC Bridge // WebSocket server that receives parameter updates from the browser // and forwards them as OSC messages to any OSC-capable software. // // Usage: // node bridge.mjs # defaults: ws:8765, osc:127.0.0.1:57120 // node bridge.mjs --osc-host 192.168.1.5 # send to another machine // node bridge.mjs --osc-port 9000 # SuperCollider on custom port // node bridge.mjs --ws-port 8765 # WebSocket listen port // node bridge.mjs --osc-prefix /nisps # OSC address prefix (default: /nisps) // node bridge.mjs --bundle # send OSC bundles instead of individual messages // // OSC address format: // /nisps/ // e.g. /nisps/Env_A_Att 0.35 // /nisps/SVF_Cutoff 0.72 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', '57120'), 10); const OSC_PREFIX = flag('osc-prefix', '/nisps'); const USE_BUNDLES = hasFlag('bundle'); // ---- OSC encoding (minimal, no dependencies) ---- function oscString(str) { const len = str.length + 1; // null terminator const padded = len + (4 - (len % 4)) % 4; 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), ]); } function oscBundle(messages) { const header = oscString('#bundle'); // NTP timestamp: immediately (1 in upper 32 bits) 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); } // ---- UDP socket ---- const udp = createSocket('udp4'); function sendOSC(address, value) { const msg = oscMessage(address, value); udp.send(msg, OSC_PORT, OSC_HOST); } function sendOSCBundle(params) { const messages = params.map(([name, value]) => oscMessage(`${OSC_PREFIX}/${name}`, value) ); const bundle = oscBundle(messages); udp.send(bundle, OSC_PORT, OSC_HOST); } // ---- WebSocket server ---- const wss = new WebSocketServer({ port: WS_PORT }); let clientCount = 0; wss.on('connection', (ws) => { clientCount++; console.log(`[ws] Client connected (${clientCount} total)`); // Tell the browser what we're targeting ws.send(JSON.stringify({ type: 'info', message: `OSC → ${OSC_HOST}:${OSC_PORT} (prefix: ${OSC_PREFIX})`, })); ws.on('message', (data) => { try { // Expects: [[paramName, value], ...] const batch = JSON.parse(data); if (!Array.isArray(batch)) return; if (USE_BUNDLES) { sendOSCBundle(batch); } else { for (const [name, value] of batch) { sendOSC(`${OSC_PREFIX}/${name}`, value); } } } catch (e) { console.error('[ws] Bad message:', e.message); } }); ws.on('close', () => { clientCount--; console.log(`[ws] Client disconnected (${clientCount} remaining)`); }); }); console.log(` NISPS → OSC Bridge ────────────────── WebSocket: ws://localhost:${WS_PORT} OSC target: ${OSC_HOST}:${OSC_PORT} Prefix: ${OSC_PREFIX} Mode: ${USE_BUNDLES ? 'bundles' : 'individual messages'} OSC addresses: ${OSC_PREFIX}/ e.g. ${OSC_PREFIX}/Env_A_Att 0.35 ${OSC_PREFIX}/SVF_Cut 0.72 Waiting for browser connection... `);