Back to Soybert 3D
SOURCE / PINNED RELEASE

Made of little things.

Soybert 3D

Release
23b6785d0ac6…
Author-recorded commit
4b065931154c…
License
LICENSE
Author’s source reference
nostr://npub1n8ga89w8h6tvwamxusfyzexw8gjy84yxu9rxgnmk955cxtml4ujswzxydd/wss%3A%2F%2Fgit.napplet.soy%2F/n-9021e1b6a96

Archive hash verified: af90956bb91e7a26…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/audio.ts
import { media } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
import { CoordinatedAudio } from '../docs/examples/coordinated-audio.js';

/** Short synthesized effects: no media files, downloads, or autoplay. */
export class Sound {
  private coordinator = new CoordinatedAudio(runtimeHasDomain('media') ? media : undefined, 'Soybert 3D');
  private soundEnabled = true;
  get enabled() { return this.soundEnabled; }
  set enabled(value: boolean) { this.soundEnabled = value; this.coordinator.setMuted(!value); }
  private context?: AudioContext;
  unlock() {
    if (!this.enabled) return;
    this.coordinator.activate(); this.context = this.coordinator.context;
  }
  play(kind: 'jump' | 'shoot' | 'land' | 'celebrate') {
    if (!this.enabled || !this.context || !this.coordinator.audible) return;
    const c = this.context, now = c.currentTime;
    if (kind === 'shoot') {
      const buffer = c.createBuffer(1, c.sampleRate * .19, c.sampleRate);
      const channel = buffer.getChannelData(0);
      for (let i = 0; i < channel.length; i++) channel[i] = (Math.random() * 2 - 1) * (1 - i / channel.length) ** 3;
      const source = c.createBufferSource(); source.buffer = buffer;
      const filter = c.createBiquadFilter(); filter.type = 'lowpass'; filter.frequency.value = 1700;
      const gain = c.createGain(); gain.gain.value = .27;
      source.connect(filter).connect(gain).connect(this.coordinator.output!); source.start();
      source.onended = () => { source.disconnect(); filter.disconnect(); gain.disconnect(); };
    }
    const notes = kind === 'celebrate' ? [523, 659, 784, 1047] : [kind === 'jump' ? 250 : kind === 'land' ? 90 : 70];
    notes.forEach((frequency, i) => {
      const oscillator = c.createOscillator(), gain = c.createGain(), start = now + i * .105;
      oscillator.type = kind === 'shoot' ? 'triangle' : 'sine';
      oscillator.frequency.setValueAtTime(frequency, start);
      oscillator.frequency.exponentialRampToValueAtTime(kind === 'jump' ? 580 : frequency * .7, start + .17);
      gain.gain.setValueAtTime(.001, start); gain.gain.linearRampToValueAtTime(.12, start + .012); gain.gain.exponentialRampToValueAtTime(.001, start + .2);
      oscillator.connect(gain).connect(this.coordinator.output!); oscillator.start(start); oscillator.stop(start + .22);
      oscillator.onended = () => { oscillator.disconnect(); gain.disconnect(); };
    });
  }
  dispose() { this.coordinator.dispose(); }
}