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.
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);
}
}
