feat(backends): register VcvBackend in manager + roster; add defaultVcvSpec, feedback forwarding, input-vector streaming
This commit is contained in:
parent
f05669969f
commit
ecb5bf2bb6
3 changed files with 40 additions and 2 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 2→N 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 0–10 V by default. */
|
||||||
|
export function defaultVcvSpec(): VcvSpec {
|
||||||
|
return { bipolar: false };
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue