File size: 3,020 Bytes
bac98f7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// 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)`);
}