Skip to content
71 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { SOUND_CUES } from "@g1t/contracts/sounds";
5
6import { PEAK, cueLength, cueSpec, envelope, renderCue, volumeGain } from "./sound-synth.ts";
7
8const RATE = 48_000;
9
10test("every cue in both sets is short, starts and ends in silence, and never clips", () => {
11 for (const set of ["soft", "bright"] as const) {
12 for (const cue of [...SOUND_CUES, "call"] as const) {
13 const spec = cueSpec(set, cue);
14 const length = cueLength(spec);
15 assert.ok(length >= 40 && length <= 260, `${set} ${cue} lasts ${length} ms`);
16 const samples = renderCue(spec, RATE);
17 assert.equal(samples.length, Math.ceil((length / 1000) * RATE));
18 // No click: the first and last millisecond are quiet.
19 const ms = RATE / 1000;
20 for (let i = 0; i < ms; i++) {
21 assert.ok(Math.abs(samples[i]!) < 0.15, `${set} ${cue} clicks on at sample ${i}: ${samples[i]}`);
22 assert.ok(Math.abs(samples[samples.length - 1 - i]!) < 0.05, `${set} ${cue} clicks off`);
23 }
24 assert.ok(Math.abs(samples[0]!) < 1e-6);
25 assert.ok(Math.abs(samples[samples.length - 1]!) < 1e-3);
26 let peak = 0;
27 let energy = 0;
28 for (const s of samples) {
29 peak = Math.max(peak, Math.abs(s));
30 energy += s * s;
31 }
32 assert.ok(Math.abs(peak - PEAK) < 1e-6, `${set} ${cue} peaks at ${peak}`);
33 assert.ok(energy > 1, `${set} ${cue} is audible`);
34 }
35 }
36});
37
38test("the cues are told apart: different figures, and Bright is higher and shorter than Soft", () => {
39 const soft = [...SOUND_CUES].map((cue) => cueSpec("soft", cue));
40 const figures = new Set(soft.map((spec) => spec.notes.map((n) => `${n.freq.toFixed(0)}@${n.at}+${n.dur}`).join(" ")));
41 assert.equal(figures.size, soft.length, "no two Soft cues are the same figure");
42 for (const cue of SOUND_CUES) {
43 const s = cueSpec("soft", cue);
44 const b = cueSpec("bright", cue);
45 assert.equal(b.notes.length, s.notes.length, "the same roles");
46 for (const [i, note] of b.notes.entries()) {
47 assert.ok(note.freq > s.notes[i]!.freq, `${cue}: Bright is higher`);
48 assert.ok(note.dur < s.notes[i]!.dur, `${cue}: Bright is shorter`);
49 }
50 assert.ok(cueLength(b) < cueLength(s));
51 }
52 // The tick as you send is the quietest and shortest.
53 assert.ok(cueLength(cueSpec("soft", "sent")) < 80);
54});
55
56test("the envelope rises from nothing, falls, and lands on exactly nothing", () => {
57 assert.equal(envelope(-1, 100), 0);
58 assert.equal(envelope(0, 100), 0);
59 assert.ok(envelope(3, 100) > 0 && envelope(3, 100) < envelope(6, 100));
60 assert.ok(envelope(50, 100) < envelope(10, 100), "it decays");
61 assert.ok(envelope(99.9, 100) < 0.01);
62 assert.equal(envelope(100, 100), 0);
63});
64
65test("volume is a curve: half way reads as half as loud, not a third", () => {
66 assert.equal(volumeGain(0), 0);
67 assert.equal(volumeGain(100), 1);
68 assert.ok(volumeGain(50) > 0.3 && volumeGain(50) < 0.35);
69 assert.equal(volumeGain(200), 1);
70 assert.equal(volumeGain(-5), 0);
71});