Back to Funclet
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.

src/control-sounds.ts
/** 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;
  }
}