145 lines
4.3 KiB
TypeScript
145 lines
4.3 KiB
TypeScript
import type {
|
|
AmbientListenerState,
|
|
AmbientState,
|
|
AmbientTrackState,
|
|
AmbientTransportState,
|
|
} from "../base/audio-fx-plugin.types.js";
|
|
|
|
/**
|
|
* Sensible defaults for a freshly-constructed chain that has not yet
|
|
* been wired to a track or transport. Engines push real values via
|
|
* `chain.updateAmbient(patch)` once both are available.
|
|
*/
|
|
export function defaultAmbientState(): AmbientState {
|
|
return {
|
|
track: {
|
|
id: "",
|
|
gainDb: 0,
|
|
pan: 0,
|
|
mute: false,
|
|
solo: false,
|
|
layout: "stereo",
|
|
},
|
|
transport: {
|
|
tempo: 120,
|
|
timeSignatureNumerator: 4,
|
|
timeSignatureDenominator: 4,
|
|
transportTimeSec: 0,
|
|
state: "stopped",
|
|
ppqn: 960,
|
|
},
|
|
extension: {},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Deeply-merged patch type for `chain.updateAmbient(patch)`. Each top-
|
|
* level slot is independently patchable; sub-fields within a slot are
|
|
* shallow-merged onto the existing slot.
|
|
*/
|
|
export interface AmbientStatePatch {
|
|
readonly track?: Partial<AmbientTrackState>;
|
|
readonly transport?: Partial<AmbientTransportState>;
|
|
readonly listener?: AmbientListenerState | null;
|
|
readonly extension?: Readonly<Record<string, unknown>>;
|
|
}
|
|
|
|
/**
|
|
* Returns a new `AmbientState` with the patch applied. Pure — never
|
|
* mutates the input. Used by `PluginChain.updateAmbient(patch)` so the
|
|
* chain can emit `ambient-changed` with the previous + new values.
|
|
*
|
|
* Patch semantics:
|
|
*
|
|
* - `track` and `transport` slots are shallow-merged (partial fields
|
|
* replace just those keys on the slot).
|
|
* - `listener` is replaced verbatim (or removed when explicitly `null`).
|
|
* - `extension` is shallow-merged: the patch overrides matching keys
|
|
* and adds new ones; unspecified keys are preserved.
|
|
*/
|
|
export function applyAmbientPatch(
|
|
previous: AmbientState,
|
|
patch: AmbientStatePatch
|
|
): AmbientState {
|
|
const next: {
|
|
track: AmbientTrackState;
|
|
transport: AmbientTransportState;
|
|
listener?: AmbientListenerState;
|
|
extension: Readonly<Record<string, unknown>>;
|
|
} = {
|
|
track: patch.track
|
|
? { ...previous.track, ...patch.track }
|
|
: previous.track,
|
|
transport: patch.transport
|
|
? { ...previous.transport, ...patch.transport }
|
|
: previous.transport,
|
|
extension: patch.extension
|
|
? { ...previous.extension, ...patch.extension }
|
|
: previous.extension,
|
|
};
|
|
if (patch.listener === null) {
|
|
// explicit removal — leave next.listener undefined
|
|
} else if (patch.listener !== undefined) {
|
|
next.listener = patch.listener;
|
|
} else if (previous.listener !== undefined) {
|
|
next.listener = previous.listener;
|
|
}
|
|
return next as AmbientState;
|
|
}
|
|
|
|
/**
|
|
* Returns true when two ambient states are structurally equal. Used by
|
|
* `PluginChain.updateAmbient` to short-circuit no-op patches and avoid
|
|
* spurious `ambient-changed` events.
|
|
*/
|
|
export function ambientStateEquals(a: AmbientState, b: AmbientState): boolean {
|
|
if (!shallowEqual(asRecord(a.track), asRecord(b.track))) {
|
|
return false;
|
|
}
|
|
if (!shallowEqual(asRecord(a.transport), asRecord(b.transport))) {
|
|
return false;
|
|
}
|
|
if (!listenerEqual(a.listener, b.listener)) {
|
|
return false;
|
|
}
|
|
return shallowEqual(a.extension, b.extension);
|
|
}
|
|
|
|
function asRecord<T>(value: T): Readonly<Record<string, unknown>> {
|
|
return value as unknown as Readonly<Record<string, unknown>>;
|
|
}
|
|
|
|
function shallowEqual(
|
|
a: Readonly<Record<string, unknown>>,
|
|
b: Readonly<Record<string, unknown>>
|
|
): boolean {
|
|
const aKeys = Object.keys(a);
|
|
const bKeys = Object.keys(b);
|
|
if (aKeys.length !== bKeys.length) {
|
|
return false;
|
|
}
|
|
for (const key of aKeys) {
|
|
if (a[key] !== b[key]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function listenerEqual(
|
|
a: AmbientListenerState | undefined,
|
|
b: AmbientListenerState | undefined
|
|
): boolean {
|
|
if (a === b) {
|
|
return true;
|
|
}
|
|
if (a === undefined || b === undefined) {
|
|
return false;
|
|
}
|
|
return (
|
|
shallowEqual(asRecord(a.position), asRecord(b.position)) &&
|
|
shallowEqual(asRecord(a.orientation), asRecord(b.orientation)) &&
|
|
shallowEqual(asRecord(a.velocity), asRecord(b.velocity))
|
|
);
|
|
}
|