SOURCE / PINNED RELEASE
Made of little things.
Funclet
- Release
- fea4f4287961…
- Author-recorded commit
- 4871c32e0e6e…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-6b7a37cac1d
Archive hash verified: 8b936703385cc8ee…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { runtimeHasDomain } from './domain-availability.js';
import { media } from '@napplet/sdk';
import { CoordinatedAudio } from './coordinated-audio.js';
/** Short, local mechanical feedback; radio playback remains shell-owned. */
export class ControlSounds {
private audio = new CoordinatedAudio(runtimeHasDomain('media') ? media : undefined, 'Funclet control feedback');
private radioActive = false;
private noise: AudioBuffer | undefined;
private lastTick = -1;
private disposed = false;
play(kind: 'tick' | 'chunk'): void {
if (this.disposed || this.radioActive || !this.audio.activate()) return;
try {
const context = this.audio.context!;
// 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(this.audio.output!);
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(this.audio.output!);
body.onended = () => { body.disconnect(); envelope.disconnect(); };
body.start(now); body.stop(now + duration);
}
} catch { /* Feedback must never block playback controls. */ }
}
/** Decorative effects must not take focus from the main radio session. */
setRadioActive(active: boolean): void {
if (active === this.radioActive || this.disposed) return;
this.radioActive = active;
this.audio.dispose();
this.noise = undefined;
if (!active) this.audio = new CoordinatedAudio(runtimeHasDomain('media') ? media : undefined, 'Funclet control feedback');
}
dispose(): void {
this.disposed = true;
this.audio.dispose();
this.noise = undefined;
}
}
