refactor(manifold): extract BaseBackend mirroring the input layer's BaseSource
Phase 3 (L17). The midi/osc/vcv transports each carried their own copy of the status plumbing, throttle gate and lastSent management; the inputs layer already had this factored out as BaseSource, so BaseBackend mirrors its shape rather than inventing a second convention. AUDIT CORRECTION: L17 says "all four" backends duplicate lastSent. Only three do. The uSEQ CV backend has a genuinely different frame-level dead-zone (an Int32Array over 14 channels plus gate bits) and no per-output lastSent, so it takes the status half only — forcing it into the shared per-output path would have needed a special case, and a base class with a per-subclass escape hatch is worse than three backends sharing one. Also: BaseSource itself has no throttle or lastSent (it is status + action plumbing only), so the mirror is partial by nature; the throttle/lastSent half is the duplication the backends actually had. Three internal representation changes, none observable: the MIDI backend's lastSent moves Int16Array -> Float32Array (stored values are integers -1..127, exact in f32, so every dead-zone comparison is bit-identical); the CV backend now inherits a setContext that also allocates a per-output buffer it never reads (cold path, no effect); and onStatusChange's unsubscribe returns void rather than Set.delete's boolean (the declared type was already `() => void` and no caller used it). Phase 2's fix to vcv-backend.ts — tracking and dead-zoning the full N-dim input vector rather than 2 — is preserved. Gates: typecheck clean, 17/17 unit tests.
This commit is contained in:
parent
96737a3d42
commit
87daac4f0d
6 changed files with 137 additions and 126 deletions
|
|
@ -12,11 +12,14 @@ See `docs/specs/backends-spec.md` for the authoritative design.
|
|||
| File | Role |
|
||||
|---|---|
|
||||
| `backend.ts` | The `OutputBackend` interface + `BackendContext` / `OutputMapping` / `BackendStatus`. |
|
||||
| `base-backend.ts` | `BaseBackend` — shared status/throttle/`lastSent` plumbing for the real transports (mirrors `inputs/base-source.ts`). |
|
||||
| `mapping.ts` | Universal per-output baseline mapping (`applyCurve`, `mapOutput`) — shared by all backends, input-clamped. |
|
||||
| `manager.ts` | `BackendManager` — single spine consumer; switches/teardowns backends; **gates synth audio**. |
|
||||
| `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. |
|
||||
| `cv-backend.ts` | `UseqCvBackend` — CV/gate out over USB Web Serial to a uSEQ module (`useq-protocol.ts` frames, channel-level dead-zone). |
|
||||
| `useq-protocol.ts` | Frame encode/decode + RX parser for the uSEQ serial protocol (see `docs/specs/useq-cv-protocol.md`). |
|
||||
| `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). |
|
||||
|
|
|
|||
93
manifold/src/backends/base-backend.ts
Normal file
93
manifold/src/backends/base-backend.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/**
|
||||
* BaseBackend — shared status + throttle + lastSent plumbing for the real
|
||||
* output backends (MIDI / OSC / uSEQ CV / VCV), mirroring the inputs layer's
|
||||
* BaseSource (inputs/base-source.ts). Subclasses implement
|
||||
* isAvailable/start/send/teardown.
|
||||
*
|
||||
* What lives here:
|
||||
* - Status plumbing: `statusState` + listeners + `status()` /
|
||||
* `onStatusChange()` / `setStatus()` — byte-for-byte what each backend
|
||||
* previously carried.
|
||||
* - Throttle gate: `throttled(intervalMs)` around the shared `lastSendMs`
|
||||
* stamp. Call it first thing in `send()` (after the connected/ctx guards);
|
||||
* it stamps on pass, so a frame dropped AFTER the gate (e.g. serial
|
||||
* backpressure in the CV backend) still consumed its slot — exactly the
|
||||
* previous inline semantics. Tests poke `lastSendMs` directly to bypass
|
||||
* the gate (tests/input-vector-truncation.test.ts) — keep the field name.
|
||||
* - lastSent: the per-output dead-zone sentinel buffer (-1 = unsent),
|
||||
* unconditionally reset by `resetLastSent()` (start / config change) and
|
||||
* resized-if-needed by `setContext()`. The uSEQ CV backend tracks
|
||||
* CHANNEL-level state instead (lastCv/lastGateBits — its dead-zone is
|
||||
* frame-level over the 14 uSEQ channels) and simply never reads this
|
||||
* buffer; it still shares the status/throttle/ctx plumbing.
|
||||
*/
|
||||
import type { BackendId } from '../dock/output-state';
|
||||
import type { BackendContext, BackendStatus, OutputBackend } from './backend';
|
||||
|
||||
export abstract class BaseBackend implements OutputBackend {
|
||||
abstract readonly id: BackendId;
|
||||
|
||||
protected ctx: BackendContext | null = null;
|
||||
protected statusState: BackendStatus;
|
||||
private statusListeners = new Set<(s: BackendStatus) => void>();
|
||||
|
||||
/**
|
||||
* Per-output dead-zone sentinels (-1 = unsent). Float32 holds the MIDI
|
||||
* backend's 7-bit integers (0..127) exactly, so one buffer type serves the
|
||||
* float (OSC/VCV) and integer (MIDI) dead-zones alike.
|
||||
*/
|
||||
protected lastSent = new Float32Array(0);
|
||||
|
||||
/** Throttle-gate timestamp. Tests poke this directly — do not rename. */
|
||||
private lastSendMs = 0;
|
||||
|
||||
constructor(initial: BackendStatus) {
|
||||
this.statusState = initial;
|
||||
}
|
||||
|
||||
abstract isAvailable(): boolean;
|
||||
abstract start(ctx: BackendContext): Promise<void>;
|
||||
abstract send(routed: Float32Array): void;
|
||||
abstract teardown(): Promise<void>;
|
||||
|
||||
status(): BackendStatus {
|
||||
return this.statusState;
|
||||
}
|
||||
|
||||
onStatusChange(cb: (s: BackendStatus) => void): () => void {
|
||||
this.statusListeners.add(cb);
|
||||
return () => {
|
||||
this.statusListeners.delete(cb);
|
||||
};
|
||||
}
|
||||
|
||||
/** Apply a fresh context; resize the dead-zone buffer only when the output
|
||||
* count changed (an unchanged-width remap keeps its sentinels — matching
|
||||
* the previous per-backend behaviour). */
|
||||
setContext(ctx: BackendContext): void {
|
||||
this.ctx = ctx;
|
||||
if (this.lastSent.length !== ctx.outputCount) this.resetLastSent(ctx.outputCount);
|
||||
}
|
||||
|
||||
protected setStatus(s: BackendStatus): void {
|
||||
this.statusState = s;
|
||||
for (const cb of this.statusListeners) cb(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send-interval gate. Returns true when this call falls inside the interval
|
||||
* (caller should drop the frame); otherwise stamps `lastSendMs` and returns
|
||||
* false.
|
||||
*/
|
||||
protected throttled(intervalMs: number): boolean {
|
||||
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
if (now - this.lastSendMs < intervalMs) return true;
|
||||
this.lastSendMs = now;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** (Re)allocate the per-output dead-zone buffer, every sentinel -1. */
|
||||
protected resetLastSent(count: number): void {
|
||||
this.lastSent = new Float32Array(count).fill(-1);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,8 @@
|
|||
* store (output-state.ts CvSpec) and arrives via {@link setCvConfig}, parallel
|
||||
* to ctx.mappings — exactly like the MIDI backend.
|
||||
*/
|
||||
import type { BackendContext, BackendStatus, OutputBackend } from './backend';
|
||||
import type { BackendContext } from './backend';
|
||||
import { BaseBackend } from './base-backend';
|
||||
import { isSilent, mapOutput } from './mapping';
|
||||
import type { CvChannelId, CvSpec } from '../dock/output-state';
|
||||
import {
|
||||
|
|
@ -44,10 +45,9 @@ function resolveTarget(id: CvChannelId): Target {
|
|||
return { kind: 'gate', idx: parseInt(id.slice(4), 10) - 1 };
|
||||
}
|
||||
|
||||
export class UseqCvBackend implements OutputBackend {
|
||||
export class UseqCvBackend extends BaseBackend {
|
||||
readonly id = 'cvgate' as const;
|
||||
|
||||
private ctx: BackendContext | null = null;
|
||||
/** Per-output CV specs, index-aligned with ctx.mappings. */
|
||||
private specs: CvSpec[] = [];
|
||||
/** Pre-resolved channel targets, index-aligned with specs. */
|
||||
|
|
@ -63,15 +63,18 @@ export class UseqCvBackend implements OutputBackend {
|
|||
|
||||
private cvVals = new Uint16Array(NUM_CV); // last computed 12-bit CV per channel
|
||||
private gateBits = 0;
|
||||
/** Channel-level dead-zone state (12-bit CV + gate bits) — this backend's
|
||||
* dead-zone is per-FRAME over the 14 uSEQ channels, so the inherited
|
||||
* per-output `lastSent` buffer goes unused here. */
|
||||
private lastCv = new Int32Array(NUM_CV).fill(-1);
|
||||
private lastGateBits = -1;
|
||||
private lastSendMs = 0;
|
||||
private writing = false; // in-flight write guard (avoid serial backpressure)
|
||||
|
||||
private pendingIdentify: (() => void) | null = null;
|
||||
|
||||
private statusState: BackendStatus = { state: 'idle', message: 'CV idle' };
|
||||
private statusListeners = new Set<(s: BackendStatus) => void>();
|
||||
constructor() {
|
||||
super({ state: 'idle', message: 'CV idle' });
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return typeof navigator !== 'undefined' && 'serial' in navigator;
|
||||
|
|
@ -100,10 +103,6 @@ export class UseqCvBackend implements OutputBackend {
|
|||
this.setStatus({ state: 'ready', message: 'uSEQ ready — click Connect device' });
|
||||
}
|
||||
|
||||
setContext(ctx: BackendContext): void {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
/** Update the per-output CV specs (channel + gate threshold). */
|
||||
setCvConfig(specs: CvSpec[]): void {
|
||||
this.specs = specs;
|
||||
|
|
@ -198,9 +197,7 @@ export class UseqCvBackend implements OutputBackend {
|
|||
const w = this.writer;
|
||||
if (!ctx || !w) return;
|
||||
|
||||
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
if (now - this.lastSendMs < SEND_INTERVAL_MS) return;
|
||||
this.lastSendMs = now;
|
||||
if (this.throttled(SEND_INTERVAL_MS)) return;
|
||||
if (this.writing) return; // previous frame still draining — drop this one
|
||||
|
||||
// Rebuild the 14-channel snapshot from the routed outputs.
|
||||
|
|
@ -279,18 +276,4 @@ export class UseqCvBackend implements OutputBackend {
|
|||
await this.closePort();
|
||||
this.setStatus({ state: 'idle', message: 'CV idle' });
|
||||
}
|
||||
|
||||
status(): BackendStatus {
|
||||
return this.statusState;
|
||||
}
|
||||
|
||||
onStatusChange(cb: (s: BackendStatus) => void): () => void {
|
||||
this.statusListeners.add(cb);
|
||||
return () => this.statusListeners.delete(cb);
|
||||
}
|
||||
|
||||
private setStatus(s: BackendStatus): void {
|
||||
this.statusState = s;
|
||||
for (const cb of this.statusListeners) cb(s);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@
|
|||
* a parallel `midiSpecs` array set through {@link setMidiConfig}. The port is
|
||||
* picked in the Outputs panel and applied via {@link selectOutput}.
|
||||
*
|
||||
* No per-frame allocation: the 3-byte message array is reused; `lastSent`
|
||||
* tracks per-CC values for the dead-zone.
|
||||
* No per-frame allocation: the 3-byte message array is reused; the inherited
|
||||
* `lastSent` buffer tracks per-CC values for the dead-zone (7-bit integers —
|
||||
* exact in float32). Status/throttle/lastSent plumbing lives in BaseBackend.
|
||||
*/
|
||||
import type { BackendContext, BackendStatus, OutputBackend } from './backend';
|
||||
import type { BackendContext } from './backend';
|
||||
import { BaseBackend } from './base-backend';
|
||||
import { isSilent, mapOutput } from './mapping';
|
||||
import type { MidiCcSpec } from '../dock/output-state';
|
||||
|
||||
|
|
@ -27,7 +29,7 @@ export interface MidiBackendConfig {
|
|||
ccCount: number;
|
||||
}
|
||||
|
||||
export class WebMidiBackend implements OutputBackend {
|
||||
export class WebMidiBackend extends BaseBackend {
|
||||
readonly id = 'midi' as const;
|
||||
|
||||
private access: MIDIAccess | null = null;
|
||||
|
|
@ -35,16 +37,14 @@ export class WebMidiBackend implements OutputBackend {
|
|||
private outputId: string | null = null;
|
||||
private ccCount = 0;
|
||||
|
||||
private ctx: BackendContext | null = null;
|
||||
/** Per-output MIDI specs, index-aligned with ctx.mappings. */
|
||||
private specs: MidiCcSpec[] = [];
|
||||
|
||||
private lastSent = new Int16Array(0); // per-output last value, -1 = unsent
|
||||
private msg: number[] = [0, 0, 0]; // reused 3-byte buffer
|
||||
private lastSendMs = 0;
|
||||
|
||||
private statusState: BackendStatus = { state: 'idle', message: 'MIDI idle' };
|
||||
private statusListeners = new Set<(s: BackendStatus) => void>();
|
||||
constructor() {
|
||||
super({ state: 'idle', message: 'MIDI idle' });
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return typeof navigator !== 'undefined' && typeof navigator.requestMIDIAccess === 'function';
|
||||
|
|
@ -52,7 +52,7 @@ export class WebMidiBackend implements OutputBackend {
|
|||
|
||||
async start(ctx: BackendContext): Promise<void> {
|
||||
this.ctx = ctx;
|
||||
this.lastSent = new Int16Array(ctx.outputCount).fill(-1);
|
||||
this.resetLastSent(ctx.outputCount);
|
||||
if (!this.isAvailable()) {
|
||||
this.setStatus({ state: 'unavailable', message: 'Web MIDI not supported in this browser' });
|
||||
return;
|
||||
|
|
@ -72,13 +72,6 @@ export class WebMidiBackend implements OutputBackend {
|
|||
}
|
||||
}
|
||||
|
||||
setContext(ctx: BackendContext): void {
|
||||
this.ctx = ctx;
|
||||
if (this.lastSent.length !== ctx.outputCount) {
|
||||
this.lastSent = new Int16Array(ctx.outputCount).fill(-1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Update the per-output MIDI specs + how many CCs are mapped + the port. */
|
||||
setMidiConfig(specs: MidiCcSpec[], cfg: MidiBackendConfig): void {
|
||||
this.specs = specs;
|
||||
|
|
@ -124,9 +117,7 @@ export class WebMidiBackend implements OutputBackend {
|
|||
const ctx = this.ctx;
|
||||
if (!out || !ctx) return;
|
||||
|
||||
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
if (now - this.lastSendMs < SEND_INTERVAL_MS) return;
|
||||
this.lastSendMs = now;
|
||||
if (this.throttled(SEND_INTERVAL_MS)) return;
|
||||
|
||||
const n = Math.min(this.ccCount, routed.length, ctx.mappings.length, this.specs.length);
|
||||
for (let i = 0; i < n; i++) {
|
||||
|
|
@ -159,18 +150,4 @@ export class WebMidiBackend implements OutputBackend {
|
|||
this.output = null;
|
||||
this.setStatus({ state: 'idle', message: 'MIDI idle' });
|
||||
}
|
||||
|
||||
status(): BackendStatus {
|
||||
return this.statusState;
|
||||
}
|
||||
|
||||
onStatusChange(cb: (s: BackendStatus) => void): () => void {
|
||||
this.statusListeners.add(cb);
|
||||
return () => this.statusListeners.delete(cb);
|
||||
}
|
||||
|
||||
private setStatus(s: BackendStatus): void {
|
||||
this.statusState = s;
|
||||
for (const cb of this.statusListeners) cb(s);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
* surface "bridge not running" until the WS connects. Throttled to ~50ms with a
|
||||
* per-output dead-zone (matching the deployed osc-output.js).
|
||||
*/
|
||||
import type { BackendContext, BackendStatus, OutputBackend } from './backend';
|
||||
import type { BackendContext } from './backend';
|
||||
import { BaseBackend } from './base-backend';
|
||||
import { isSilent, mapOutput } from './mapping';
|
||||
import { NispsOscClient } from './osc-client';
|
||||
import type { OscSpec } from '../dock/output-state';
|
||||
|
|
@ -27,30 +28,29 @@ export interface OscBackendConfig {
|
|||
sendRaw: boolean;
|
||||
}
|
||||
|
||||
export class OscBridgeBackend implements OutputBackend {
|
||||
export class OscBridgeBackend extends BaseBackend {
|
||||
readonly id = 'osc' as const;
|
||||
|
||||
private client = new NispsOscClient();
|
||||
private ctx: BackendContext | null = null;
|
||||
private specs: OscSpec[] = [];
|
||||
private sendRaw = false;
|
||||
|
||||
private lastSent: Float32Array = new Float32Array(0); // last normalised value
|
||||
private batch: Array<[string, number]> = []; // reused outer; entries reused
|
||||
private lastSendMs = 0;
|
||||
|
||||
private statusState: BackendStatus = { state: 'idle', message: 'OSC idle' };
|
||||
private statusListeners = new Set<(s: BackendStatus) => void>();
|
||||
private offConn: (() => void) | null = null;
|
||||
private offInfo: (() => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
super({ state: 'idle', message: 'OSC idle' });
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return typeof WebSocket !== 'undefined';
|
||||
}
|
||||
|
||||
async start(ctx: BackendContext): Promise<void> {
|
||||
this.ctx = ctx;
|
||||
this.lastSent = new Float32Array(ctx.outputCount).fill(-1);
|
||||
this.resetLastSent(ctx.outputCount);
|
||||
if (!this.isAvailable()) {
|
||||
this.setStatus({ state: 'unavailable', message: 'WebSocket not available' });
|
||||
return;
|
||||
|
|
@ -72,13 +72,6 @@ export class OscBridgeBackend implements OutputBackend {
|
|||
});
|
||||
}
|
||||
|
||||
setContext(ctx: BackendContext): void {
|
||||
this.ctx = ctx;
|
||||
if (this.lastSent.length !== ctx.outputCount) {
|
||||
this.lastSent = new Float32Array(ctx.outputCount).fill(-1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Update per-output OSC specs + bridge URL/raw toggle. */
|
||||
setOscConfig(specs: OscSpec[], cfg: OscBackendConfig): void {
|
||||
this.specs = specs;
|
||||
|
|
@ -99,9 +92,7 @@ export class OscBridgeBackend implements OutputBackend {
|
|||
const ctx = this.ctx;
|
||||
if (!ctx || !this.client.connected) return;
|
||||
|
||||
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
if (now - this.lastSendMs < SEND_INTERVAL_MS) return;
|
||||
this.lastSendMs = now;
|
||||
if (this.throttled(SEND_INTERVAL_MS)) return;
|
||||
|
||||
const n = Math.min(routed.length, ctx.mappings.length, this.specs.length);
|
||||
this.batch.length = 0;
|
||||
|
|
@ -130,18 +121,4 @@ export class OscBridgeBackend implements OutputBackend {
|
|||
this.client.disconnect();
|
||||
this.setStatus({ state: 'idle', message: 'OSC idle' });
|
||||
}
|
||||
|
||||
status(): BackendStatus {
|
||||
return this.statusState;
|
||||
}
|
||||
|
||||
onStatusChange(cb: (s: BackendStatus) => void): () => void {
|
||||
this.statusListeners.add(cb);
|
||||
return () => this.statusListeners.delete(cb);
|
||||
}
|
||||
|
||||
private setStatus(s: BackendStatus): void {
|
||||
this.statusState = s;
|
||||
for (const cb of this.statusListeners) cb(s);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@
|
|||
* British spelling in product copy; the synth is the "Built-in Synth", never
|
||||
* "C15".
|
||||
*/
|
||||
import type { BackendContext, BackendStatus, OutputBackend } from './backend';
|
||||
import type { BackendContext } from './backend';
|
||||
import { BaseBackend } from './base-backend';
|
||||
import { isSilent, mapOutput } from './mapping';
|
||||
import { NispsOscClient } from './osc-client';
|
||||
import type { VcvSpec } from '../dock/output-state';
|
||||
|
|
@ -56,11 +57,10 @@ export interface VcvBackendConfig {
|
|||
sendRaw: boolean;
|
||||
}
|
||||
|
||||
export class VcvBackend implements OutputBackend {
|
||||
export class VcvBackend extends BaseBackend {
|
||||
readonly id = 'vcv' as const;
|
||||
|
||||
private client = new NispsOscClient();
|
||||
private ctx: BackendContext | null = null;
|
||||
private specs: VcvSpec[] = [];
|
||||
private sendRaw = false;
|
||||
|
||||
|
|
@ -68,31 +68,32 @@ export class VcvBackend implements OutputBackend {
|
|||
* net's full input arity, not fixed at 2; simplification audit S10). */
|
||||
private inputVec: number[] = [0.5, 0.5];
|
||||
|
||||
private lastSent: Float32Array = new Float32Array(0); // last normalised output
|
||||
private batch: Array<[string, number]> = []; // reused outer; entries reused
|
||||
private lastSendMs = 0;
|
||||
/** Dead-zone sentinel per input axis, sized to `inputVec` (out-of-range -1
|
||||
* forces the first send). Tracks the FULL vector, not just the first two
|
||||
* axes, so a change in axis 2+ (gamepad/MIDI beyond the XY pair) still
|
||||
* triggers a resend instead of being silently swallowed. */
|
||||
* triggers a resend instead of being silently swallowed. Separate from the
|
||||
* inherited per-OUTPUT `lastSent` buffer. */
|
||||
private lastInputSent: number[] = [];
|
||||
|
||||
private gotModuleReply = false;
|
||||
|
||||
private statusState: BackendStatus = { state: 'idle', message: 'VCV idle' };
|
||||
private statusListeners = new Set<(s: BackendStatus) => void>();
|
||||
private offConn: (() => void) | null = null;
|
||||
private offInfo: (() => void) | null = null;
|
||||
private offOutputs: (() => void) | null = null;
|
||||
private offInputs: (() => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
super({ state: 'idle', message: 'VCV idle' });
|
||||
}
|
||||
|
||||
isAvailable(): boolean {
|
||||
return typeof WebSocket !== 'undefined';
|
||||
}
|
||||
|
||||
async start(ctx: BackendContext): Promise<void> {
|
||||
this.ctx = ctx;
|
||||
this.lastSent = new Float32Array(ctx.outputCount).fill(-1);
|
||||
this.resetLastSent(ctx.outputCount);
|
||||
if (!this.isAvailable()) {
|
||||
this.setStatus({ state: 'unavailable', message: 'WebSocket not available' });
|
||||
return;
|
||||
|
|
@ -125,13 +126,6 @@ export class VcvBackend implements OutputBackend {
|
|||
});
|
||||
}
|
||||
|
||||
setContext(ctx: BackendContext): void {
|
||||
this.ctx = ctx;
|
||||
if (this.lastSent.length !== ctx.outputCount) {
|
||||
this.lastSent = new Float32Array(ctx.outputCount).fill(-1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Update per-output VCV specs (polarity) + bridge URL/raw toggle. */
|
||||
setVcvConfig(specs: VcvSpec[], cfg: VcvBackendConfig): void {
|
||||
this.specs = specs;
|
||||
|
|
@ -173,9 +167,7 @@ export class VcvBackend implements OutputBackend {
|
|||
const ctx = this.ctx;
|
||||
if (!ctx || !this.client.connected) return;
|
||||
|
||||
const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
if (now - this.lastSendMs < SEND_INTERVAL_MS) return;
|
||||
this.lastSendMs = now;
|
||||
if (this.throttled(SEND_INTERVAL_MS)) return;
|
||||
|
||||
// 1) Stream the current input vector so the browser drives the module.
|
||||
// Dead-zone over the FULL vector — any axis moving (not just the first
|
||||
|
|
@ -240,20 +232,6 @@ export class VcvBackend implements OutputBackend {
|
|||
this.gotModuleReply = false;
|
||||
this.setStatus({ state: 'idle', message: 'VCV idle' });
|
||||
}
|
||||
|
||||
status(): BackendStatus {
|
||||
return this.statusState;
|
||||
}
|
||||
|
||||
onStatusChange(cb: (s: BackendStatus) => void): () => void {
|
||||
this.statusListeners.add(cb);
|
||||
return () => this.statusListeners.delete(cb);
|
||||
}
|
||||
|
||||
private setStatus(s: BackendStatus): void {
|
||||
this.statusState = s;
|
||||
for (const cb of this.statusListeners) cb(s);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sanitise an output name into an OSC-path-safe token (the bridge prefixes it). */
|
||||
|
|
|
|||
Loading…
Reference in a new issue