// One-shot generator for the bundled CC0 sound effects. Output is // committed (public/sounds/*.wav) in the robot's wire format: // 16 kHz mono 16-bit PCM WAV. Re-run with `npm run make-sounds`. // // Durations here must match src/audio/sounds.ts. import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const RATE = 16000; const OUT_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "public", "sounds"); function wavBytes(samples) { const dataSize = samples.length * 2; const buf = Buffer.alloc(44 + dataSize); buf.write("RIFF", 0); buf.writeUInt32LE(36 + dataSize, 4); buf.write("WAVE", 8); buf.write("fmt ", 12); buf.writeUInt32LE(16, 16); buf.writeUInt16LE(1, 20); // PCM buf.writeUInt16LE(1, 22); // mono buf.writeUInt32LE(RATE, 24); buf.writeUInt32LE(RATE * 2, 28); buf.writeUInt16LE(2, 32); buf.writeUInt16LE(16, 34); buf.write("data", 36); buf.writeUInt32LE(dataSize, 40); samples.forEach((v, i) => { const s = Math.max(-1, Math.min(1, v)); buf.writeInt16LE(Math.round(s < 0 ? s * 0x8000 : s * 0x7fff), 44 + i * 2); }); return buf; } const seconds = (s) => Math.round(s * RATE); const silence = (s) => new Array(seconds(s)).fill(0); /** Sine tone with attack/release fades (avoids clicks). */ function tone(freq, durS, { gain = 0.6, fadeS = 0.012, glideTo = null } = {}) { const n = seconds(durS); const out = new Array(n); for (let i = 0; i < n; i++) { const t = i / RATE; const u = i / n; const f = glideTo === null ? freq : freq * Math.pow(glideTo / freq, u); const fade = Math.min(1, t / fadeS, (durS - t) / fadeS); out[i] = gain * fade * Math.sin(2 * Math.PI * f * t); } return out; } function decay(samples, halfLifeS) { return samples.map((v, i) => v * Math.pow(0.5, i / RATE / halfLifeS)); } const sounds = { // 350 ms friendly double beep. beep: [...tone(880, 0.15), ...silence(0.05), ...tone(1175, 0.15)], // 900 ms rising arpeggio + sparkle. tada: [ ...tone(523, 0.16), ...tone(659, 0.16), ...tone(784, 0.2), ...decay(tone(1047, 0.38, { gain: 0.7 }), 0.12), ], // 600 ms springy downward glide with wobble. boing: decay( tone(320, 0.6, { glideTo: 70, gain: 0.8 }).map( (v, i) => v * (1 + 0.35 * Math.sin(2 * Math.PI * 18 * (i / RATE))), ), 0.22, ), // 250 ms bubble pop: tiny noise burst + low thump. pop: [ ...decay(Array.from({ length: seconds(0.03) }, () => (Math.random() * 2 - 1) * 0.7), 0.008), ...decay(tone(180, 0.22, { gain: 0.8 }), 0.05), ], // 750 ms descending "uh-oh". uhoh: [...decay(tone(420, 0.25, { gain: 0.7 }), 0.2), ...silence(0.12), ...decay(tone(300, 0.38, { gain: 0.7 }), 0.2)], }; mkdirSync(OUT_DIR, { recursive: true }); for (const [name, samples] of Object.entries(sounds)) { const file = join(OUT_DIR, `${name}.wav`); writeFileSync(file, wavBytes(samples)); console.log(`${file} (${(samples.length / RATE).toFixed(2)}s)`); }