Back to Obsidian Breaker
SOURCE / PINNED RELEASE

Made of little things.

Obsidian Breaker

Release
44c589c52043…
Author-recorded commit
9ee5b61ce1c9…
License
LICENSE
Author’s source reference
nostr://npub1dmmh0futyszl4445mv4xrygkgsgj8vpu79yx3qyy9qxmz4pemtzqlqmc87/wss%3A%2F%2Fgit.napplet.soy%2F/n-de5dc6a02e7

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

src/audio.ts
type SoundName =
  | 'launch'
  | 'paddle'
  | 'brick'
  | 'crack'
  | 'boom'
  | 'power'
  | 'life'
  | 'level'
  | 'win'
  | 'bullet'
  | 'warp';

const VOLUME = 0.24;

/** Tiny synthesized soundscape. No assets, no network, unlocked by a gesture. */
export class SoundEngine {
  private context: AudioContext | null = null;
  private master: GainNode | null = null;
  private noiseBuffer: AudioBuffer | null = null;
  private muted = false;
  private lastPlayed = new Map<SoundName, number>();

  unlock(): void {
    if (this.muted) return;
    try {
      if (!this.context) {
        this.context = new AudioContext();
        this.master = this.context.createGain();
        this.master.gain.value = VOLUME;
        this.master.connect(this.context.destination);

        const length = Math.floor(this.context.sampleRate * 0.4);
        const buffer = this.context.createBuffer(1, length, this.context.sampleRate);
        const data = buffer.getChannelData(0);
        for (let index = 0; index < length; index += 1) {
          data[index] = Math.random() * 2 - 1;
        }
        this.noiseBuffer = buffer;
      }
      if (this.context.state === 'suspended') {
        void this.context.resume();
      }
    } catch {
      this.context = null;
      this.master = null;
      this.noiseBuffer = null;
    }
  }

  setMuted(muted: boolean): void {
    this.muted = muted;
    if (muted) {
      if (this.master && this.context) this.master.gain.value = 0;
      return;
    }
    this.unlock();
    if (this.master && this.context) this.master.gain.value = VOLUME;
  }

  play(name: SoundName): void {
    const context = this.context;
    const master = this.master;
    if (this.muted || !context || !master) return;

    const now = context.currentTime;
    const throttle = name === 'brick' ? 0.028 : name === 'paddle' ? 0.04 : 0;
    const previous = this.lastPlayed.get(name);
    if (previous !== undefined && now - previous < throttle) return;
    this.lastPlayed.set(name, now);

    try {
      switch (name) {
        case 'launch':
          this.tone(320, 760, 0.16, 'triangle', 0.2);
          this.noise(0.1, 0.12, 1800);
          break;
        case 'paddle':
          this.tone(190, 330, 0.09, 'square', 0.13);
          break;
        case 'brick':
          this.tone(520 + Math.random() * 240, 360, 0.055, 'triangle', 0.1);
          break;
        case 'crack':
          this.noise(0.12, 0.18, 2100);
          this.tone(170, 80, 0.12, 'sawtooth', 0.08);
          break;
        case 'boom':
          this.noise(0.42, 0.4, 620);
          this.tone(130, 32, 0.5, 'sine', 0.42);
          break;
        case 'power':
          this.tone(440, 520, 0.09, 'triangle', 0.14);
          this.tone(660, 720, 0.09, 'triangle', 0.13, 0.06);
          this.tone(880, 980, 0.14, 'triangle', 0.12, 0.12);
          break;
        case 'life':
          this.tone(240, 60, 0.5, 'sawtooth', 0.18);
          this.noise(0.18, 0.14, 500);
          break;
        case 'level':
          this.tone(392, 392, 0.12, 'triangle', 0.13);
          this.tone(523, 523, 0.12, 'triangle', 0.12, 0.1);
          this.tone(659, 659, 0.2, 'triangle', 0.12, 0.2);
          break;
        case 'win':
          this.tone(523, 523, 0.16, 'triangle', 0.14);
          this.tone(659, 659, 0.16, 'triangle', 0.14, 0.12);
          this.tone(784, 784, 0.16, 'triangle', 0.14, 0.24);
          this.tone(1046, 1046, 0.45, 'triangle', 0.12, 0.36);
          break;
        case 'bullet':
          this.tone(460, 150, 0.55, 'sine', 0.16);
          this.noise(0.28, 0.08, 620);
          break;
        case 'warp':
          this.tone(170, 980, 0.34, 'sawtooth', 0.12);
          this.tone(340, 1560, 0.28, 'triangle', 0.08, 0.06);
          this.noise(0.32, 0.16, 2400);
          break;
      }
    } catch {
      // Audio is a flourish; never let it break the run.
    }
  }

  private tone(
    startFrequency: number,
    endFrequency: number,
    duration: number,
    type: OscillatorType,
    gainValue: number,
    delay = 0,
  ): void {
    const context = this.context;
    const master = this.master;
    if (!context || !master) return;
    const start = context.currentTime + delay;
    const oscillator = context.createOscillator();
    const gain = context.createGain();
    oscillator.type = type;
    oscillator.frequency.setValueAtTime(Math.max(20, startFrequency), start);
    oscillator.frequency.exponentialRampToValueAtTime(Math.max(20, endFrequency), start + duration);
    gain.gain.setValueAtTime(0.0001, start);
    gain.gain.exponentialRampToValueAtTime(gainValue, start + 0.01);
    gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
    oscillator.connect(gain);
    gain.connect(master);
    oscillator.start(start);
    oscillator.stop(start + duration + 0.03);
  }

  private noise(duration: number, gainValue: number, cutoff: number): void {
    const context = this.context;
    const master = this.master;
    const buffer = this.noiseBuffer;
    if (!context || !master || !buffer) return;
    const start = context.currentTime;
    const source = context.createBufferSource();
    source.buffer = buffer;
    const filter = context.createBiquadFilter();
    filter.type = 'lowpass';
    filter.frequency.setValueAtTime(cutoff, start);
    filter.frequency.exponentialRampToValueAtTime(Math.max(80, cutoff * 0.35), start + duration);
    const gain = context.createGain();
    gain.gain.setValueAtTime(gainValue, start);
    gain.gain.exponentialRampToValueAtTime(0.0001, start + duration);
    source.connect(filter);
    filter.connect(gain);
    gain.connect(master);
    source.start(start);
    source.stop(start + duration + 0.02);
  }
}