You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/sema/sounds.ts

161 lines
5.2 KiB

import type { SemaSignatureOverride, Silent, SoundSignature } from './channels';
import type { SoundVoice } from '$sound';
/**
* A tuning is a DELTA over a family's base signature — never an absence.
* `Silent` is excluded on purpose: silence is `SILENT`, a value the resolver
* honours by dropping the channel, and a catalogue entry that resolved to
* silence would be the arithmetic this design replaced.
*/
export type SoundOverride = Exclude<NonNullable<SemaSignatureOverride['sound']>, Silent>;
/** Name sema's voice registers under in the sound orchestrator. */
export const SEMA_SOUND_VOICE_NAME = 'sema';
/**
* Sema's synthesis voice — the perceptual calibration its vocabulary was tuned
* against (note durations, hold windows, the audible distance between intents):
* the second oscillator at a fifth mixed at 0.3, the envelope clamps (attack
* ≤ 15 % of the note, release ≤ 40 %), the ±400-cent contour sweep and the
* roughness→AM mapping.
*
* Moved here from the art's baked-in constants (redesign 2026-07-31, D-SR.3 in
* `PLAN-sound-redesign.md`): the engine owns the synthesis MACHINERY, the
* consumer owns the CALIBRATION. Today it matches the engine's default voice
* value-for-value, so registering it changes nothing audible — it establishes
* ownership.
*/
export const SEMA_SOUND_VOICE: SoundVoice = {
interval: { ratio: 1.5, mix: 0.3 },
envelope: { maxAttackFraction: 0.15, maxReleaseFraction: 0.4 },
contour: { sweepCents: 400 },
am: { threshold: 0.2, baseHz: 30, slopeHz: 150, depthScale: 0.5 }
};
export type SoundDirection = SoundSignature['contour'] extends infer T
? Extract<T, 'ascending' | 'descending'>
: never;
export interface SynthSoundDefinition {
kind: 'synth';
signature: SoundSignature;
}
export interface SampleSoundDefinition {
kind: 'sample';
sampleUrl: string;
fallback: SoundSignature;
preload?: boolean;
}
export type SoundDefinition = SynthSoundDefinition | SampleSoundDefinition;
export interface DragSoundParams {
value01: number;
velocity01: number;
direction: SoundDirection;
}
const synth = (signature: SoundSignature): SynthSoundDefinition => ({
kind: 'synth',
signature
});
export const sample = (
sampleUrl: string,
fallback: SoundSignature,
opts: { preload?: boolean } = {}
): SampleSoundDefinition => ({
kind: 'sample',
sampleUrl,
fallback,
preload: opts.preload
});
export function resolveSoundDefinition(
definition: SoundDefinition,
overrides?: Partial<SoundSignature>
): SoundSignature {
const base =
definition.kind === 'sample'
? { ...definition.fallback, sampleUrl: definition.sampleUrl }
: definition.signature;
return mergeSound(base, overrides);
}
export function resolveHandleDragSound(params: DragSoundParams): SoundSignature {
const value01 = clamp(params.value01, 0, 1);
const velocity01 = clamp(params.velocity01, 0, 1);
return {
pitch: lerp(960, 1500, value01),
gain: clamp(0.012 + velocity01 * 0.018, 0.012, 0.03),
centroid: lerp(5200, 8200, velocity01),
duration: 96,
contour: params.direction,
attack: 26,
decay: 90,
roughness: 0.002
};
}
export function resolveSliderDragSound(params: DragSoundParams): SoundSignature {
const value01 = clamp(params.value01, 0, 1);
const velocity01 = clamp(params.velocity01, 0, 1);
return {
pitch: Math.round(lerp(760, 1280, value01) + velocity01 * 120),
centroid: Math.round(lerp(3600, 6800, velocity01)),
roughness: 0.003,
attack: 8,
decay: 44,
duration: 46,
contour: params.direction,
gain: Number((0.04 + velocity01 * 0.04).toFixed(3))
};
}
/**
* Splitter handle drag sound resolver.
*
* Splitters move whole panels, not a thumb on a track. The gesture has
* more body and lower kinetic register than a slider, so the curve is
* tuned to read as a heavier, thicker timbre:
*
* - Lower pitch range (480..820 Hz vs slider's 760..1280) — "thicker"
* - Lower centroid ceiling (2400..4800 vs 3600..6800) — less brilliant
* - Slightly more roughness (0.006) — woody texture, not metallic
* - Slightly longer envelope (attack 16, decay 72, duration 76) —
* the surface being moved has perceptual mass
* - Modest gain floor (0.025) climbing with velocity — splitters can
* drag for long swipes, gain ceiling stays restrained
*
* `value01` = current handle position in `[0, 1]` (size of the panel
* before the handle as a fraction of total container size). `velocity01`
* = normalized motion speed computed by soma against frame deltas.
*/
export function resolveSplitterDragSound(params: DragSoundParams): SoundSignature {
const value01 = clamp(params.value01, 0, 1);
const velocity01 = clamp(params.velocity01, 0, 1);
return {
pitch: Math.round(lerp(480, 820, value01) + velocity01 * 90),
centroid: Math.round(lerp(2400, 4800, velocity01)),
roughness: 0.006,
attack: 16,
decay: 72,
duration: 76,
contour: params.direction,
gain: Number((0.025 + velocity01 * 0.03).toFixed(3))
};
}
function mergeSound(base: SoundSignature, overrides?: Partial<SoundSignature>): SoundSignature {
const next = structuredClone(base);
return overrides ? { ...next, ...overrides } : next;
}
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}

Powered by TurnKey Linux.