SOURCE / PINNED RELEASE
Made of little things.
Funclet
- Release
- 0b497cfc2f66…
- Author-recorded commit
- 1e531b24f490…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6b7a37cac1d
Archive hash verified: 71d97a89ed8c40c2…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
/** Short, local mechanical feedback; radio playback remains shell-owned. */
export class ControlSounds {
private context: AudioContext | undefined;
private noise: AudioBuffer | undefined;
private lastTick = -1;
private disposed = false;
play(kind: 'tick' | 'chunk'): void {
if (this.disposed) return;
try {
const context = this.context ??= new AudioContext();
if (context.state === 'suspended') void context.resume().catch(() => {});
// Never queue delayed clicks if the browser has not granted audio activation.
if (context.state !== 'running') return;
const now = context.currentTime;
if (kind === 'tick' && now - this.lastTick < .018) return;
if (kind === 'tick') this.lastTick = now;
if (!this.noise) {
this.noise = context.createBuffer(1, Math.ceil(context.sampleRate * .09), context.sampleRate);
const samples = this.noise.getChannelData(0);
for (let i = 0; i < samples.length; i++) samples[i] = Math.random() * 2 - 1;
}
const chunk = kind === 'chunk';
const duration = chunk ? .075 : .022;
const source = context.createBufferSource();
source.buffer = this.noise;
const filter = context.createBiquadFilter();
filter.type = 'bandpass';
filter.frequency.value = chunk ? 950 : 3100;
filter.Q.value = chunk ? .8 : 1.7;
const gain = context.createGain();
gain.gain.setValueAtTime(.0001, now);
gain.gain.exponentialRampToValueAtTime(chunk ? .3 : .17, now + .001);
gain.gain.exponentialRampToValueAtTime(.0001, now + duration);
source.connect(filter).connect(gain).connect(context.destination);
source.onended = () => { source.disconnect(); filter.disconnect(); gain.disconnect(); };
source.start(now); source.stop(now + duration);
if (chunk) {
const body = context.createOscillator();
const envelope = context.createGain();
body.frequency.setValueAtTime(180, now);
body.frequency.exponentialRampToValueAtTime(65, now + duration);
envelope.gain.setValueAtTime(.18, now);
envelope.gain.exponentialRampToValueAtTime(.0001, now + duration);
body.connect(envelope).connect(context.destination);
body.onended = () => { body.disconnect(); envelope.disconnect(); };
body.start(now); body.stop(now + duration);
}
} catch { /* Feedback must never block playback controls. */ }
}
dispose(): void {
this.disposed = true;
if (this.context) void this.context.close().catch(() => {});
this.noise = undefined;
}
}
