Built by the Opus session per docs/briefs/SOCIAL_RESET.md; integrated and verified by Fable (typecheck clean, relay validation reviewed, 3D avatar gear/face pixel-confirmed live — the one item the build session couldn't capture in its frozen preview pane). - Avatars: hue/gear/face enums in src/net/avatarLook.ts shared by the 3D build and the Menu AVATAR tab's 2D mirror; live sync with client-side coalescing (two-tab testing caught the relay rate limit permanently desyncing rapid editors - trailing send fixes convergence). - Emotes Z/X/C/V: wave, beat-synced nod, point, rate-limited airhorn (detuned saws + confetti), server + client caps. - Reset ritual: after a win the fuse takes a deliberate 5s hold (reuses the workshop radial meter as the warning dial) -> server-authoritative reset with 10-min cooldown; music brakes to silence, lights die over 3s, workshop resets to wires-off with a SERVER-generated random weight notch (both tabs land identical), player builds survive, quest replayable. - Roster: hold Tab for who's-in-the-booth with hue swatches. - interact/ addition (flagged friction): additive setBreakGuard hook so interaction.ts never learns what a fuse is. - launch.json: main dev config uses autoPort (5173 collisions with parallel sessions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
412 lines
17 KiB
TypeScript
412 lines
17 KiB
TypeScript
// TURNCRAFT — Lane E. One-shot diegetic SFX, all synthesized and short.
|
|
// Each function synthesizes into `dest` (AudioEngine routes `dest` through a
|
|
// PannerNode when a world position is supplied). None of these hold state.
|
|
|
|
import type { BlockDef } from '../core/blocks';
|
|
|
|
type SoundCat = BlockDef['sound']; // 'metal'|'wood'|'plastic'|'soft'|'glass'|'none'
|
|
|
|
/** Short filtered noise burst, body tuned per surface category. */
|
|
function noiseHit(
|
|
ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number,
|
|
o: { type: BiquadFilterType; freq: number; q: number; dur: number; gain: number },
|
|
): void {
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noise;
|
|
src.loop = true;
|
|
const f = ctx.createBiquadFilter();
|
|
f.type = o.type;
|
|
f.frequency.value = o.freq;
|
|
f.Q.value = o.q;
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(o.gain, t);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + o.dur);
|
|
src.connect(f); f.connect(g); g.connect(dest);
|
|
src.start(t);
|
|
src.stop(t + o.dur + 0.02);
|
|
}
|
|
|
|
const STEP_BY_CAT: Record<SoundCat, { type: BiquadFilterType; freq: number; q: number; dur: number; gain: number }> = {
|
|
metal: { type: 'bandpass', freq: 3200, q: 3, dur: 0.06, gain: 0.18 },
|
|
wood: { type: 'lowpass', freq: 900, q: 1, dur: 0.07, gain: 0.22 },
|
|
plastic: { type: 'highpass', freq: 1800, q: 0.7, dur: 0.045, gain: 0.16 },
|
|
soft: { type: 'lowpass', freq: 420, q: 0.7, dur: 0.09, gain: 0.20 },
|
|
glass: { type: 'bandpass', freq: 6000, q: 6, dur: 0.05, gain: 0.14 },
|
|
none: { type: 'lowpass', freq: 700, q: 1, dur: 0.05, gain: 0.10 },
|
|
};
|
|
|
|
export function footstep(
|
|
ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number, cat: SoundCat,
|
|
): void {
|
|
const p = STEP_BY_CAT[cat] ?? STEP_BY_CAT.none;
|
|
noiseHit(ctx, dest, noise, t, p);
|
|
}
|
|
|
|
/** Land thump: low body scaled by impact speed + a surface tick. */
|
|
export function land(
|
|
ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number,
|
|
impactSpeed: number, cat: SoundCat,
|
|
): void {
|
|
const amt = Math.min(1, impactSpeed / 12);
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.0001, t);
|
|
g.gain.exponentialRampToValueAtTime(0.25 + 0.5 * amt, t + 0.005);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.12 + 0.1 * amt);
|
|
g.connect(dest);
|
|
const osc = ctx.createOscillator();
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(120 + 60 * amt, t);
|
|
osc.frequency.exponentialRampToValueAtTime(50, t + 0.12);
|
|
osc.connect(g);
|
|
osc.start(t); osc.stop(t + 0.24);
|
|
const p = STEP_BY_CAT[cat] ?? STEP_BY_CAT.none;
|
|
noiseHit(ctx, dest, noise, t, { ...p, gain: p.gain * (0.6 + amt) });
|
|
}
|
|
|
|
/** Break/place click: brighter for break, softer for place, category-flavoured. */
|
|
export function blockHit(
|
|
ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number,
|
|
cat: SoundCat, kind: 'break' | 'place',
|
|
): void {
|
|
const p = STEP_BY_CAT[cat] ?? STEP_BY_CAT.none;
|
|
const dur = kind === 'break' ? 0.09 : 0.05;
|
|
noiseHit(ctx, dest, noise, t, { ...p, dur, gain: p.gain * (kind === 'break' ? 1.4 : 0.9) });
|
|
if (kind === 'break') {
|
|
// little pitch-down thock so breaks feel chunkier
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.14, t);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.08);
|
|
g.connect(dest);
|
|
const osc = ctx.createOscillator();
|
|
osc.type = 'triangle';
|
|
osc.frequency.setValueAtTime(320, t);
|
|
osc.frequency.exponentialRampToValueAtTime(120, t + 0.08);
|
|
osc.connect(g); osc.start(t); osc.stop(t + 0.1);
|
|
}
|
|
}
|
|
|
|
/** Fader zip: filtered noise sweep, quick. */
|
|
export function faderZip(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noise; src.loop = true;
|
|
const bp = ctx.createBiquadFilter();
|
|
bp.type = 'bandpass'; bp.Q.value = 4;
|
|
bp.frequency.setValueAtTime(1200, t);
|
|
bp.frequency.exponentialRampToValueAtTime(3600, t + 0.12);
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.12, t);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.13);
|
|
src.connect(bp); bp.connect(g); g.connect(dest);
|
|
src.start(t); src.stop(t + 0.15);
|
|
}
|
|
|
|
/** Button clunk: short low thock with a click. */
|
|
export function buttonClunk(ctx: BaseAudioContext, dest: AudioNode, t: number): void {
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.3, t);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.09);
|
|
g.connect(dest);
|
|
const osc = ctx.createOscillator();
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(220, t);
|
|
osc.frequency.exponentialRampToValueAtTime(90, t + 0.08);
|
|
osc.connect(g); osc.start(t); osc.stop(t + 0.1);
|
|
}
|
|
|
|
/** RCA plug seating: "clunk-clunk-CLICK", three rising transients. */
|
|
export function rcaClick(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
const steps = [
|
|
{ off: 0.0, f: 140, g: 0.22 },
|
|
{ off: 0.12, f: 200, g: 0.26 },
|
|
{ off: 0.26, f: 340, g: 0.34 },
|
|
];
|
|
for (const s of steps) {
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(s.g, t + s.off);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + s.off + 0.06);
|
|
g.connect(dest);
|
|
const osc = ctx.createOscillator();
|
|
osc.type = 'triangle';
|
|
osc.frequency.setValueAtTime(s.f, t + s.off);
|
|
osc.frequency.exponentialRampToValueAtTime(s.f * 0.6, t + s.off + 0.05);
|
|
osc.connect(g); osc.start(t + s.off); osc.stop(t + s.off + 0.08);
|
|
}
|
|
noiseHit(ctx, dest, noise, t + 0.26, { type: 'highpass', freq: 4000, q: 2, dur: 0.04, gain: 0.18 });
|
|
}
|
|
|
|
/** Fuse zap: buzzy electric discharge. */
|
|
export function fuseZap(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noise; src.loop = true;
|
|
const bp = ctx.createBiquadFilter();
|
|
bp.type = 'bandpass'; bp.frequency.value = 2200; bp.Q.value = 8;
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.28, t);
|
|
g.gain.setValueAtTime(0.05, t + 0.02);
|
|
g.gain.setValueAtTime(0.24, t + 0.05);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.22);
|
|
src.connect(bp); bp.connect(g); g.connect(dest);
|
|
src.start(t); src.stop(t + 0.24);
|
|
// buzzy saw underneath
|
|
const osc = ctx.createOscillator();
|
|
osc.type = 'sawtooth';
|
|
osc.frequency.setValueAtTime(320, t);
|
|
osc.frequency.exponentialRampToValueAtTime(80, t + 0.2);
|
|
const og = ctx.createGain();
|
|
og.gain.setValueAtTime(0.12, t);
|
|
og.gain.exponentialRampToValueAtTime(0.0001, t + 0.2);
|
|
osc.connect(og); og.connect(dest); osc.start(t); osc.stop(t + 0.22);
|
|
}
|
|
|
|
/** Tonearm cue lever creak: slow filtered-noise groan. */
|
|
export function cueCreak(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noise; src.loop = true;
|
|
const bp = ctx.createBiquadFilter();
|
|
bp.type = 'bandpass'; bp.Q.value = 12;
|
|
bp.frequency.setValueAtTime(420, t);
|
|
bp.frequency.linearRampToValueAtTime(280, t + 0.3);
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.0001, t);
|
|
g.gain.linearRampToValueAtTime(0.1, t + 0.06);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.34);
|
|
src.connect(bp); bp.connect(g); g.connect(dest);
|
|
src.start(t); src.stop(t + 0.36);
|
|
}
|
|
|
|
/** Needle drop: crackle burst as the stylus meets the groove (used at win). */
|
|
export function needleDrop(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noise; src.loop = true;
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.0001, t);
|
|
g.gain.linearRampToValueAtTime(0.35, t + 0.01);
|
|
g.gain.exponentialRampToValueAtTime(0.04, t + 0.18);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.5);
|
|
const hp = ctx.createBiquadFilter();
|
|
hp.type = 'highpass'; hp.frequency.value = 1500;
|
|
src.connect(hp); hp.connect(g); g.connect(dest);
|
|
src.start(t); src.stop(t + 0.55);
|
|
// the "thunk" of the tonearm landing
|
|
const tg = ctx.createGain();
|
|
tg.gain.setValueAtTime(0.3, t);
|
|
tg.gain.exponentialRampToValueAtTime(0.0001, t + 0.12);
|
|
tg.connect(dest);
|
|
const osc = ctx.createOscillator();
|
|
osc.type = 'sine';
|
|
osc.frequency.setValueAtTime(90, t);
|
|
osc.frequency.exponentialRampToValueAtTime(45, t + 0.1);
|
|
osc.connect(tg); osc.start(t); osc.stop(t + 0.14);
|
|
}
|
|
|
|
// ── Headshell Workshop SFX (WORKSHOP_CARTRIDGE.md) ──────────────────────────
|
|
|
|
/** Crimping a tag-wire onto a pin: a stubborn ratchet tick (call while holding). */
|
|
export function crimp(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
noiseHit(ctx, dest, noise, t, { type: 'bandpass', freq: 2600, q: 8, dur: 0.03, gain: 0.12 });
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.08, t);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.04);
|
|
g.connect(dest);
|
|
const o = ctx.createOscillator();
|
|
o.type = 'square';
|
|
o.frequency.setValueAtTime(180, t);
|
|
o.frequency.exponentialRampToValueAtTime(320, t + 0.03);
|
|
o.connect(g); o.start(t); o.stop(t + 0.05);
|
|
}
|
|
|
|
/** Wire seats home: a crisp little snap. */
|
|
export function wireOn(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
noiseHit(ctx, dest, noise, t, { type: 'highpass', freq: 4200, q: 2, dur: 0.035, gain: 0.2 });
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.18, t);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.05);
|
|
g.connect(dest);
|
|
const o = ctx.createOscillator();
|
|
o.type = 'triangle'; o.frequency.setValueAtTime(520, t);
|
|
o.frequency.exponentialRampToValueAtTime(760, t + 0.04);
|
|
o.connect(g); o.start(t); o.stop(t + 0.06);
|
|
}
|
|
|
|
/** Wire pulled off a pin: a soft dull pop. */
|
|
export function wireOff(ctx: BaseAudioContext, dest: AudioNode, t: number): void {
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.16, t);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.07);
|
|
g.connect(dest);
|
|
const o = ctx.createOscillator();
|
|
o.type = 'sine'; o.frequency.setValueAtTime(300, t);
|
|
o.frequency.exponentialRampToValueAtTime(140, t + 0.06);
|
|
o.connect(g); o.start(t); o.stop(t + 0.08);
|
|
}
|
|
|
|
/** One click of the screwdriver ratchet (call repeatedly while torquing). */
|
|
export function screwTick(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
noiseHit(ctx, dest, noise, t, { type: 'bandpass', freq: 3400, q: 10, dur: 0.02, gain: 0.09 });
|
|
}
|
|
|
|
/** Both screws seat evenly: a satisfying two-tone CLUNK. */
|
|
export function screwClunk(ctx: BaseAudioContext, dest: AudioNode, t: number): void {
|
|
for (const [f0, f1, off, gain] of [[240, 90, 0, 0.32], [420, 160, 0.02, 0.18]] as const) {
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(gain, t + off);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + off + 0.12);
|
|
g.connect(dest);
|
|
const o = ctx.createOscillator();
|
|
o.type = 'sine'; o.frequency.setValueAtTime(f0, t + off);
|
|
o.frequency.exponentialRampToValueAtTime(f1, t + off + 0.1);
|
|
o.connect(g); o.start(t + off); o.stop(t + off + 0.14);
|
|
}
|
|
}
|
|
|
|
/** The cartridge tips under uneven torque: a low groaning creak. */
|
|
export function tiltCreak(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noise; src.loop = true;
|
|
const bp = ctx.createBiquadFilter();
|
|
bp.type = 'bandpass'; bp.Q.value = 16;
|
|
bp.frequency.setValueAtTime(340, t);
|
|
bp.frequency.linearRampToValueAtTime(220, t + 0.35);
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.0001, t);
|
|
g.gain.linearRampToValueAtTime(0.11, t + 0.08);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.4);
|
|
src.connect(bp); bp.connect(g); g.connect(dest);
|
|
src.start(t); src.stop(t + 0.42);
|
|
}
|
|
|
|
/** Counterweight steps a notch along the rail: a mechanical detent tick. */
|
|
export function weightDetent(ctx: BaseAudioContext, dest: AudioNode, t: number): void {
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.16, t);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.045);
|
|
g.connect(dest);
|
|
const o = ctx.createOscillator();
|
|
o.type = 'square'; o.frequency.setValueAtTime(430, t);
|
|
o.frequency.exponentialRampToValueAtTime(210, t + 0.04);
|
|
o.connect(g); o.start(t); o.stop(t + 0.055);
|
|
}
|
|
|
|
/** Bubble level centers on the 2 g mark: a soft confirming two-note chime. */
|
|
export function bubbleLock(ctx: BaseAudioContext, dest: AudioNode, t: number): void {
|
|
for (const [f, off] of [[880, 0], [1320, 0.08]] as const) {
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.0001, t + off);
|
|
g.gain.exponentialRampToValueAtTime(0.12, t + off + 0.01);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + off + 0.3);
|
|
g.connect(dest);
|
|
const o = ctx.createOscillator();
|
|
o.type = 'sine'; o.frequency.value = f;
|
|
o.connect(g); o.start(t + off); o.stop(t + off + 0.34);
|
|
}
|
|
}
|
|
|
|
/** The needle skates when the cartridge is rotated wrong: descending zip. */
|
|
export function skate(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noise; src.loop = true;
|
|
const bp = ctx.createBiquadFilter();
|
|
bp.type = 'bandpass'; bp.Q.value = 6;
|
|
bp.frequency.setValueAtTime(5200, t);
|
|
bp.frequency.exponentialRampToValueAtTime(400, t + 0.6);
|
|
const g = ctx.createGain();
|
|
g.gain.setValueAtTime(0.26, t);
|
|
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.7);
|
|
src.connect(bp); bp.connect(g); g.connect(dest);
|
|
src.start(t); src.stop(t + 0.75);
|
|
}
|
|
|
|
/**
|
|
* The classic rave airhorn (SOCIAL_RESET emote V): three detuned saws through a
|
|
* resonant bandpass, with the signature pitch droop on the tail. Rate-limited by
|
|
* the caller — 1 per 4 s per peer, client AND relay — or the booth becomes hell.
|
|
*/
|
|
export function airhorn(ctx: BaseAudioContext, dest: AudioNode, t: number): void {
|
|
const dur = 0.85;
|
|
const out = ctx.createGain();
|
|
out.gain.setValueAtTime(0.0001, t);
|
|
out.gain.exponentialRampToValueAtTime(0.22, t + 0.03); // fast attack
|
|
out.gain.setValueAtTime(0.22, t + dur - 0.22);
|
|
out.gain.exponentialRampToValueAtTime(0.0001, t + dur);
|
|
|
|
// Resonant horn body
|
|
const bp = ctx.createBiquadFilter();
|
|
bp.type = 'bandpass'; bp.frequency.value = 1400; bp.Q.value = 3.5;
|
|
const shaper = ctx.createWaveShaper();
|
|
const curve = new Float32Array(257);
|
|
for (let i = 0; i < 257; i++) {
|
|
const x = (i / 128) - 1;
|
|
curve[i] = Math.tanh(x * 2.6); // a bit of grit
|
|
}
|
|
shaper.curve = curve;
|
|
bp.connect(shaper); shaper.connect(out); out.connect(dest);
|
|
|
|
// Three saws, detuned; each droops ~12% at the end (the "hoooonk-eugh")
|
|
for (const [mult, det, gain] of [[1, 0, 0.5], [1, 7, 0.34], [2, -5, 0.2]] as const) {
|
|
const osc = ctx.createOscillator();
|
|
osc.type = 'sawtooth';
|
|
osc.detune.value = det;
|
|
const f0 = 233 * mult; // ~Bb3
|
|
osc.frequency.setValueAtTime(f0, t);
|
|
osc.frequency.setValueAtTime(f0, t + dur - 0.3);
|
|
osc.frequency.exponentialRampToValueAtTime(f0 * 0.86, t + dur); // droop
|
|
const g = ctx.createGain();
|
|
g.gain.value = gain;
|
|
osc.connect(g); g.connect(bp);
|
|
osc.start(t); osc.stop(t + dur + 0.02);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Blow-the-fuse power-down (SOCIAL_RESET reset ritual): the mains-thunk half of
|
|
* the moment. The stems' pitch brake lives in AudioEngine (it owns the
|
|
* transport); this is the transformer clunk + dying hum underneath it.
|
|
*/
|
|
export function powerDown(ctx: BaseAudioContext, dest: AudioNode, noise: AudioBuffer, t: number): void {
|
|
// Contactor thunk
|
|
const thunk = ctx.createOscillator();
|
|
thunk.type = 'sine';
|
|
thunk.frequency.setValueAtTime(90, t);
|
|
thunk.frequency.exponentialRampToValueAtTime(28, t + 0.18);
|
|
const tg = ctx.createGain();
|
|
tg.gain.setValueAtTime(0.5, t);
|
|
tg.gain.exponentialRampToValueAtTime(0.0001, t + 0.3);
|
|
thunk.connect(tg); tg.connect(dest);
|
|
thunk.start(t); thunk.stop(t + 0.32);
|
|
|
|
// 50 Hz hum sagging away as the rails collapse
|
|
const hum = ctx.createOscillator();
|
|
hum.type = 'sawtooth';
|
|
hum.frequency.setValueAtTime(50, t);
|
|
hum.frequency.exponentialRampToValueAtTime(14, t + 1.6);
|
|
const lp = ctx.createBiquadFilter();
|
|
lp.type = 'lowpass';
|
|
lp.frequency.setValueAtTime(1200, t);
|
|
lp.frequency.exponentialRampToValueAtTime(90, t + 1.6);
|
|
const hg = ctx.createGain();
|
|
hg.gain.setValueAtTime(0.14, t);
|
|
hg.gain.exponentialRampToValueAtTime(0.0001, t + 1.7);
|
|
hum.connect(lp); lp.connect(hg); hg.connect(dest);
|
|
hum.start(t); hum.stop(t + 1.72);
|
|
|
|
// Spark crackle off the blown fuse
|
|
const src = ctx.createBufferSource();
|
|
src.buffer = noise; src.loop = true;
|
|
const bp = ctx.createBiquadFilter();
|
|
bp.type = 'bandpass'; bp.frequency.value = 3200; bp.Q.value = 6;
|
|
const ng = ctx.createGain();
|
|
ng.gain.setValueAtTime(0.2, t);
|
|
ng.gain.exponentialRampToValueAtTime(0.0001, t + 0.25);
|
|
src.connect(bp); bp.connect(ng); ng.connect(dest);
|
|
src.start(t); src.stop(t + 0.27);
|
|
}
|
|
|
|
export type SfxName =
|
|
| 'footstep' | 'land' | 'break' | 'place'
|
|
| 'fader' | 'button' | 'rca' | 'fuse' | 'cue' | 'needle'
|
|
// Headshell Workshop:
|
|
| 'crimp' | 'wireOn' | 'wireOff' | 'screwTick' | 'screwClunk'
|
|
| 'tiltCreak' | 'weightDetent' | 'bubbleLock' | 'skate'
|
|
// Social & Reset Ritual:
|
|
| 'airhorn' | 'powerDown';
|