|
|
/**
|
|
|
* Sema channel contract.
|
|
|
*
|
|
|
* This is the single source for runtime channel ids, channel signatures and
|
|
|
* per-channel overrides. `sema-map.ts` owns data; this file owns the open
|
|
|
* channel type surface that both the map and event declarations consume.
|
|
|
*/
|
|
|
|
|
|
export interface SoundSignature {
|
|
|
pitch: number;
|
|
|
centroid: number;
|
|
|
roughness: number;
|
|
|
attack: number;
|
|
|
decay: number;
|
|
|
duration: number;
|
|
|
contour: 'flat' | 'ascending' | 'descending' | 'arc' | 'bell';
|
|
|
gain: number;
|
|
|
sampleUrl?: string;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Haptic / vibration feedback. Categorical `kind` lets future channels map
|
|
|
* to platform haptic primitives (iOS Haptic Engine, Android
|
|
|
* HapticFeedbackConstants) without breaking consumers; V1 of `HapticChannel`
|
|
|
* implements the Vibration API path with intensity-modulated durations.
|
|
|
*/
|
|
|
export interface HapticSignature {
|
|
|
/**
|
|
|
* Categorical pattern. Maps to system primitives where available; falls
|
|
|
* back to a Vibration API pattern derived from `duration` + `intensity`.
|
|
|
*/
|
|
|
kind: 'tick' | 'tap' | 'pulse' | 'thud' | 'success' | 'warning' | 'error';
|
|
|
|
|
|
/** 0..1. Modulates Vibration API duration; ignored on categorical-only platforms. */
|
|
|
intensity: number;
|
|
|
|
|
|
/** Base pulse duration in ms (single Vibration API pulse). */
|
|
|
duration: number;
|
|
|
|
|
|
/** Explicit on/off/on/off… pattern in ms. */
|
|
|
pattern?: readonly number[];
|
|
|
|
|
|
/** Delay relative to signal start (ms). */
|
|
|
delay?: number;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Canonical channel registry. Apps extend it via declaration merging:
|
|
|
*
|
|
|
* declare module '$uix/sema' {
|
|
|
* interface SemaChannelSignatures {
|
|
|
* a11y: A11ySignature;
|
|
|
* voice: VoiceSignature;
|
|
|
* }
|
|
|
* }
|
|
|
*/
|
|
|
export interface SemaChannelSignatures {
|
|
|
sound: SoundSignature;
|
|
|
haptic: HapticSignature;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Open string id of a channel. Known channels autocomplete in editors;
|
|
|
* arbitrary strings are accepted for channels registered with the engine but
|
|
|
* without a typed signature slice.
|
|
|
*/
|
|
|
export type SemaChannelId = keyof SemaChannelSignatures | (string & {});
|
|
|
|
|
|
export type DeltaOp =
|
|
|
| { op: 'multiply'; factor: number }
|
|
|
| { op: 'replace'; value: number | string | boolean | null | readonly DeltaValue[] }
|
|
|
| { op: 'add'; value: number };
|
|
|
|
|
|
export type DeltaValue =
|
|
|
| number
|
|
|
| string
|
|
|
| boolean
|
|
|
| null
|
|
|
| DeltaOp
|
|
|
| readonly DeltaValue[]
|
|
|
| { [key: string]: DeltaValue };
|
|
|
|
|
|
/**
|
|
|
* Reduction level for a perceptual channel (book ch. 32 §12, ch. 33 §7:
|
|
|
* `BK-REDUCTIONS`). The book asks for a symmetric per-channel reduction story —
|
|
|
* "controles de sonido", reduced motion, etc. — not a single global switch.
|
|
|
*
|
|
|
* - `full` — normal expression (default).
|
|
|
* - `reduce` — attenuated but present (lower gain / intensity).
|
|
|
* - `off` — the channel is silenced; meaning migrates per `SEMA_MIGRATION`.
|
|
|
*/
|
|
|
export type SemaReduceLevel = 'full' | 'reduce' | 'off';
|
|
|
|
|
|
/**
|
|
|
* App-controlled per-channel reduction preferences, read by the channels at
|
|
|
* dispatch time (so an app may back this with reactive state). Sound has no OS
|
|
|
* media query, so its reduction is an explicit app control; haptic ALSO honors
|
|
|
* `prefers-reduced-motion` from the active-dom on top of this. Visual-motion
|
|
|
* reduction lives in eidos CSS (`@media prefers-reduced-motion`), not here.
|
|
|
*/
|
|
|
export interface SemaPreferences {
|
|
|
sound?: SemaReduceLevel;
|
|
|
haptic?: SemaReduceLevel;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* THE canonical silence — one value for the whole system.
|
|
|
*
|
|
|
* Silence admits no family, no verb and no intent: there is nothing to
|
|
|
* modulate, so there is nothing to parameterise. A channel slice set to
|
|
|
* `SILENT` means «this event does not speak here», and the channel that owns
|
|
|
* the slice MUST ignore it and dispatch nothing downstream.
|
|
|
*
|
|
|
* It is a VALUE, not an arithmetic. The framework used to express silence by
|
|
|
* subtracting a family's base gain (`form.toggle.silent = gain -0.3`), which
|
|
|
* needed one key per family and produced a number, not an absence — and a
|
|
|
* number is something later layers can move. It was measured: an intent delta
|
|
|
* that raises `roughness` past the AM threshold turned a signature declared at
|
|
|
* gain 0 into a raw tremolo at −13.9 dBFS (`risk`) and −6.7 dBFS (`threat`),
|
|
|
* louder than a real button press. The engine had already written the rule it
|
|
|
* was breaking — «muting DROPS the channel rather than scaling to zero,
|
|
|
* because a `0` still buzzes» (`engine.ts`). This value is that rule, applied
|
|
|
* to the signature instead of to the channel list.
|
|
|
*
|
|
|
* Absorbing by construction: intent deltas do not apply on top of `SILENT`
|
|
|
* (layer 2 cannot un-silence). A later override CAN lift it by REPLACING the
|
|
|
* slice with a full signature — declaring sound is a decision, not a delta.
|
|
|
*/
|
|
|
export const SILENT = 'silent' as const;
|
|
|
|
|
|
/** The type of {@link SILENT}. */
|
|
|
export type Silent = typeof SILENT;
|
|
|
|
|
|
/**
|
|
|
* Per-channel partial override applied at resolution time.
|
|
|
*/
|
|
|
export type SemaSignatureOverride = {
|
|
|
/**
|
|
|
* Replace the active-channel list for matching signals. Last-write-wins
|
|
|
* across the cascade. Empty array silences the signal entirely.
|
|
|
*/
|
|
|
channels?: readonly SemaChannelId[];
|
|
|
} & {
|
|
|
/**
|
|
|
* `sound` is a NAME from the active pack, or `SILENT` — never a signature.
|
|
|
* Nobody authors a sound inline: not a morfo, not a pack, not an app, not a
|
|
|
* per-emit override. A product that needs a sound the pack lacks REGISTERS
|
|
|
* it (name + definition, once) and then names it.
|
|
|
*/
|
|
|
sound?: SoundNameRef | Silent;
|
|
|
} & {
|
|
|
[K in Exclude<keyof SemaChannelSignatures, 'sound'>]?:
|
|
|
| Partial<SemaChannelSignatures[K]>
|
|
|
| Record<string, DeltaValue>
|
|
|
| Silent;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Loose reference to a pack entry. The precise union (`SoundName`) is derived
|
|
|
* from the map, which imports this module — so the tight type is applied where
|
|
|
* it can be: `Sema['cascade'].sound` and `SemaCascadeRule.sound`.
|
|
|
*/
|
|
|
export type SoundNameRef = string;
|