Initial Commit
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
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))
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user