feat(backends): register VcvBackend in manager + roster; add defaultVcvSpec, feedback forwarding, input-vector streaming

This commit is contained in:
monkey-w1n5t0n 2026-06-28 04:22:05 +02:00
parent f05669969f
commit ecb5bf2bb6
3 changed files with 40 additions and 2 deletions

View file

@ -16,6 +16,8 @@ export { WebMidiBackend } from './midi-backend';
export type { MidiBackendConfig } from './midi-backend'; export type { MidiBackendConfig } from './midi-backend';
export { OscBridgeBackend } from './osc-backend'; export { OscBridgeBackend } from './osc-backend';
export type { OscBackendConfig } from './osc-backend'; export type { OscBackendConfig } from './osc-backend';
export { VcvBackend } from './vcv-backend';
export type { VcvBackendConfig, VcvFeedbackOp } from './vcv-backend';
export { NispsOscClient } from './osc-client'; export { NispsOscClient } from './osc-client';
export { PassthroughBackend } from './passthrough-backend'; export { PassthroughBackend } from './passthrough-backend';
export { export {

View file

@ -21,12 +21,19 @@ import type { BackendId } from '../dock/output-state';
import { WebMidiBackend } from './midi-backend'; import { WebMidiBackend } from './midi-backend';
import { OscBridgeBackend } from './osc-backend'; import { OscBridgeBackend } from './osc-backend';
import { PassthroughBackend } from './passthrough-backend'; import { PassthroughBackend } from './passthrough-backend';
import { VcvBackend, type VcvFeedbackOp } from './vcv-backend';
/** The slice of EngineApi the manager depends on (keeps it decoupled/testable). */ /** The slice of EngineApi the manager depends on (keeps it decoupled/testable). */
export interface ManagerEngine { export interface ManagerEngine {
subscribe(cb: () => void): () => void; subscribe(cb: () => void): () => void;
routedOutput(): Float32Array | null; routedOutput(): Float32Array | null;
audio: { setMuted(muted: boolean): void }; audio: { setMuted(muted: boolean): void };
/**
* Current control input vector (2-D for the fixed 2N MLP). Optional when
* present the VCV backend streams it to the module so the browser drives the
* module's inputs in bridged mode.
*/
inputVector?(): ReadonlyArray<number>;
} }
export class BackendManager { export class BackendManager {
@ -49,12 +56,17 @@ export class BackendManager {
['synth', backends?.synth ?? new PassthroughBackend('synth', 'Built-in Synth — audio plays in the engine')], ['synth', backends?.synth ?? new PassthroughBackend('synth', 'Built-in Synth — audio plays in the engine')],
['particles', backends?.particles ?? new PassthroughBackend('particles', 'Particle visualiser')], ['particles', backends?.particles ?? new PassthroughBackend('particles', 'Particle visualiser')],
['cvgate', backends?.cvgate ?? new PassthroughBackend('cvgate', 'CV / gate (via VCV bridge)')], ['cvgate', backends?.cvgate ?? new PassthroughBackend('cvgate', 'CV / gate (via VCV bridge)')],
['vcv', backends?.vcv ?? new PassthroughBackend('vcv', 'VCV bridge')], ['vcv', backends?.vcv ?? new VcvBackend()],
]); ]);
// Single subscription to the spine: forward routed → active backend. // Single subscription to the spine: forward routed → active backend. For the
// VCV backend we also stream the current input vector each tick so the
// browser drives the module's inputs in bridged mode.
this.unsub = this.engine.subscribe(() => { this.unsub = this.engine.subscribe(() => {
if (!this.active) return; if (!this.active) return;
if (this.active instanceof VcvBackend && this.engine.inputVector) {
this.active.setInputVector(this.engine.inputVector());
}
const routed = this.engine.routedOutput(); const routed = this.engine.routedOutput();
if (routed) this.active.send(routed); if (routed) this.active.send(routed);
}); });
@ -71,6 +83,25 @@ export class BackendManager {
return b instanceof OscBridgeBackend ? b : null; return b instanceof OscBridgeBackend ? b : null;
} }
vcv(): VcvBackend | null {
const b = this.backends.get('vcv');
return b instanceof VcvBackend ? b : null;
}
/**
* Forward a verdict op to the VCV module's embedded learner over the bridge.
* No-op unless the VCV backend is the ACTIVE one the verdict loop only
* trains the module when Mode = VCV (otherwise the browser engine is the
* learner). Returns true if it was forwarded.
*/
forwardFeedback(op: VcvFeedbackOp): boolean {
if (this.activeId !== 'vcv') return false;
const vcv = this.vcv();
if (!vcv) return false;
vcv.sendFeedback(op);
return true;
}
get(id: BackendId): OutputBackend | undefined { get(id: BackendId): OutputBackend | undefined {
return this.backends.get(id); return this.backends.get(id);
} }

View file

@ -130,3 +130,8 @@ export function defaultMidiSpec(index: number): MidiCcSpec {
export function defaultOscSpec(name: string): OscSpec { export function defaultOscSpec(name: string): OscSpec {
return { path: `/nisps/${name.toLowerCase()}`, rangeMin: 0, rangeMax: 1 }; return { path: `/nisps/${name.toLowerCase()}`, rangeMin: 0, rangeMax: 1 };
} }
/** Default VCV spec for an output — unipolar 010 V by default. */
export function defaultVcvSpec(): VcvSpec {
return { bipolar: false };
}