@johnmorrisdotca/kyogijo-stadiums 0.60.1 → 0.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ import { SOUND_BEDS, SOUND_LEVELS, SOUND_OF_SPORT, SOUND_SETS } from "./sound.sets.js";
2
+ const clamp = (value, least, greatest) => Math.min(greatest, Math.max(least, value));
3
+ const isNum = (value) => typeof value === "number" && Number.isFinite(value);
4
+ const round = (value) => Math.round(value * 1000) / 1000;
5
+ /** The set of a surface's sport, or `undefined` when it has none (a surface with no sound shows no sound controls). */
6
+ export function soundSetOf(sport) {
7
+ const id = SOUND_OF_SPORT[sport];
8
+ return id === undefined ? undefined : SOUND_SETS[id];
9
+ }
10
+ /** The crowd levels a surface offers, quietest first: none for a surface with no sound. */
11
+ export function soundLevelsOf(sport) {
12
+ const found = soundSetOf(sport);
13
+ if (found === undefined)
14
+ return [];
15
+ return found.levels ?? SOUND_BEDS[found.bed].levels;
16
+ }
17
+ /** The voices a surface's cues use, each once, in the order of `CUE_KINDS`: what a page loads when it has files for them. */
18
+ export function soundVoicesOf(sport) {
19
+ const found = soundSetOf(sport);
20
+ if (found === undefined)
21
+ return [];
22
+ return [...new Set(Object.values(found.cues).flat())];
23
+ }
24
+ /** How closed in each kind of roof leaves the crowd. */
25
+ const ENCLOSURE = { open: 0, canopy: 0.3, cable: 0.7, panels: 0.6, retractable: 0.6, fixed: 1, dome: 1, saddle: 1, barrel: 1 };
26
+ /**
27
+ * The room of a surface in its context: how closed in the venue is (its roof, else its bed's), how long it rings (longer closed in and larger), how much echo, and how loud the crowd is for the
28
+ * people in it (the seats times the share taken, on a scale that grows as a crowd's sound does, slower than its number). `undefined` for a surface with no sound.
29
+ * @example
30
+ * ```ts
31
+ * import { soundRoom } from "@johnmorrisdotca/kyogijo-stadiums/sound";
32
+ * const arena = soundRoom({ sport: "nhl", venue: { roof: "fixed", seats: 18000 } })!;
33
+ * const field = soundRoom({ sport: "soccer", venue: { roof: "open", seats: 50000 } })!;
34
+ * console.log(arena.bed.id, arena.decay > field.decay, arena.wet > field.wet);
35
+ * console.log(soundRoom({ sport: "nhl", crowd: 0 })!.crowd);
36
+ * // arena true true
37
+ * // 0
38
+ * ```
39
+ */
40
+ export function soundRoom(context) {
41
+ const found = soundSetOf(context.sport);
42
+ if (found === undefined)
43
+ return undefined;
44
+ const bed = SOUND_BEDS[found.bed];
45
+ const roof = context.venue?.roof;
46
+ const enclosure = roof !== undefined && Object.hasOwn(ENCLOSURE, roof) ? ENCLOSURE[roof] : bed.indoor ? 1 : 0;
47
+ const seats = isNum(context.venue?.seats) && context.venue.seats > 0 ? context.venue.seats : bed.seats;
48
+ const share = isNum(context.crowd) ? clamp(context.crowd, 0, 1) : 1;
49
+ const people = Math.round(seats * share);
50
+ const size = clamp((seats / bed.seats) ** 0.25, 0.7, 1.3);
51
+ const flat = context.view === "2d";
52
+ const decay = round((0.6 + 1.8 * enclosure) * size);
53
+ const wet = round((0.12 + 0.33 * enclosure) * (flat ? 0.5 : 1));
54
+ const crowd = people === 0 ? 0 : round(clamp(0.35 + (0.65 * Math.log10(1 + people)) / Math.log10(1 + bed.seats), 0, 1.15));
55
+ return { bed, enclosure, decay, wet, people, crowd, gain: flat ? 0.7 : 1 };
56
+ }
57
+ /** Each layer's gain at each level, before the crowd's size: the room and the water stay, the voices come up. */
58
+ export const LEVEL_MIX = {
59
+ quiet: { room: 1, water: 0.8, murmur: 0.12, applause: 0, roar: 0 },
60
+ murmur: { room: 0.7, water: 0.7, murmur: 0.55, applause: 0.03, roar: 0.04 },
61
+ engaged: { room: 0.5, water: 0.6, murmur: 0.7, applause: 0.2, roar: 0.18 },
62
+ loud: { room: 0.4, water: 0.5, murmur: 0.6, applause: 0.4, roar: 0.5 },
63
+ roar: { room: 0.3, water: 0.4, murmur: 0.45, applause: 0.55, roar: 0.95 },
64
+ };
65
+ const VOICES = ["murmur", "applause", "roar"];
66
+ /** The nearest level a surface offers to the one asked for: a dojo asked to roar is loud. */
67
+ export function levelFor(sport, level) {
68
+ const offered = soundLevelsOf(sport);
69
+ if (offered.length === 0 || offered.includes(level))
70
+ return level;
71
+ const want = SOUND_LEVELS.indexOf(level);
72
+ return offered.reduce((best, l) => (Math.abs(SOUND_LEVELS.indexOf(l) - want) < Math.abs(SOUND_LEVELS.indexOf(best) - want) ? l : best), offered[0]);
73
+ }
74
+ /**
75
+ * The mix of a surface in its context at a crowd level: each of its bed's layers' gain (the crowd's voices scaled by the people in the room; a layer the bed does not have is 0) and the room.
76
+ * `undefined` for a surface with no sound.
77
+ * @example
78
+ * ```ts
79
+ * import { soundMix } from "@johnmorrisdotca/kyogijo-stadiums/sound";
80
+ * const quiet = soundMix({ sport: "nhl" }, "quiet")!;
81
+ * const roar = soundMix({ sport: "nhl" }, "roar")!;
82
+ * console.log(quiet.layers.roar, roar.layers.roar > roar.layers.murmur, soundMix({ sport: "kyudo" }, "roar")!.level);
83
+ * // 0 true murmur
84
+ * ```
85
+ */
86
+ export function soundMix(context, level) {
87
+ const room = soundRoom(context);
88
+ if (room === undefined)
89
+ return undefined;
90
+ const at = levelFor(context.sport, level);
91
+ const base = LEVEL_MIX[at];
92
+ const layers = Object.fromEntries(["room", "water", "murmur", "applause", "roar"].map((layer) => [layer, room.bed.layers.includes(layer) ? round(base[layer] * (VOICES.includes(layer) ? room.crowd : 1)) : 0]));
93
+ return { ...room, level: at, layers };
94
+ }
95
+ /**
96
+ * How excited the crowd is at a moment of a game: a murmur before the start, between periods and after the end; engaged while it plays; loud late in a close game; a roar for a few seconds
97
+ * after a goal; and back to a murmur when one side is far ahead late on.
98
+ * @example
99
+ * ```ts
100
+ * import { excitementAt } from "@johnmorrisdotca/kyogijo-stadiums/sound";
101
+ * const periods = [{ start: 0, end: 100 }, { start: 120, end: 220 }];
102
+ * console.log(excitementAt({ t: 50, start: 0, end: 220, periods, score: { home: 0, away: 0 } }));
103
+ * console.log(excitementAt({ t: 110, start: 0, end: 220, periods }));
104
+ * console.log(excitementAt({ t: 200, start: 0, end: 220, periods, score: { home: 2, away: 1 } }));
105
+ * console.log(excitementAt({ t: 60, start: 0, end: 220, periods, sinceGoal: 2 }));
106
+ * // engaged
107
+ * // murmur
108
+ * // loud
109
+ * // roar
110
+ * ```
111
+ */
112
+ export function excitementAt(moment) {
113
+ const periods = moment.periods !== undefined && moment.periods.length > 0 ? moment.periods : [{ start: moment.start, end: moment.end }];
114
+ const inPlay = periods.some((p) => moment.t >= p.start && moment.t < p.end);
115
+ if (!inPlay)
116
+ return "murmur";
117
+ if (isNum(moment.sinceGoal) && moment.sinceGoal >= 0 && moment.sinceGoal < 6)
118
+ return "roar";
119
+ const first = periods[0].start;
120
+ const last = periods[periods.length - 1].end;
121
+ const progress = last > first ? (moment.t - first) / (last - first) : 0;
122
+ const gap = moment.score === undefined ? 0 : Math.abs(moment.score.home - moment.score.away);
123
+ if (gap >= 3 && progress > 0.6)
124
+ return "murmur";
125
+ if (gap <= 1 && progress >= 2 / 3)
126
+ return "loud";
127
+ return "engaged";
128
+ }
129
+ /** The words of a timeline's events that are read as each kind of moment (lower case): a timeline of a sport with its own words (a `try`, a `touchdown`) calls the cues of its kind. */
130
+ const KIND_WORDS = {
131
+ goal: "goal",
132
+ touchdown: "goal",
133
+ try: "goal",
134
+ run: "goal",
135
+ "home-run": "goal",
136
+ basket: "point",
137
+ point: "point",
138
+ score: "point",
139
+ "field-goal": "point",
140
+ behind: "point",
141
+ ippon: "goal",
142
+ knockout: "goal",
143
+ ko: "goal",
144
+ shot: "shot",
145
+ save: "shot",
146
+ miss: "shot",
147
+ post: "shot",
148
+ whistle: "whistle",
149
+ foul: "whistle",
150
+ penalty: "whistle",
151
+ offside: "whistle",
152
+ stoppage: "whistle",
153
+ "period-start": "period-start",
154
+ "faceoff": "period-start",
155
+ "kick-off": "period-start",
156
+ kickoff: "period-start",
157
+ "period-end": "period-end",
158
+ "half-time": "period-end",
159
+ halftime: "period-end",
160
+ "game-end": "game-end",
161
+ "full-time": "game-end",
162
+ final: "game-end",
163
+ start: "start",
164
+ gun: "start",
165
+ finish: "finish",
166
+ "round-start": "round-start",
167
+ "round-end": "round-end",
168
+ bell: "round-end",
169
+ };
170
+ /** The kind of moment a timeline's event is, or `undefined` for one with no cue (a pass). */
171
+ export function cueKindOf(event) {
172
+ const word = String(event.kind).toLowerCase().trim();
173
+ return Object.hasOwn(KIND_WORDS, word) ? KIND_WORDS[word] : undefined;
174
+ }
175
+ /**
176
+ * The voices an event calls on a surface: its kind's cue in the sport's set, or none.
177
+ * @example
178
+ * ```ts
179
+ * import { cuesOf } from "@johnmorrisdotca/kyogijo-stadiums/sound";
180
+ * console.log(cuesOf({ kind: "goal" }, "nhl").join(), cuesOf({ kind: "goal" }, "soccer").join(), cuesOf({ kind: "whistle" }, "nba").join());
181
+ * console.log(cuesOf({ kind: "pass" }, "nhl").length, cuesOf({ kind: "goal" }, "a-surface-with-no-sound").length);
182
+ * // horn,swell swell whistle
183
+ * // 0 0
184
+ * ```
185
+ */
186
+ export function cuesOf(event, sport) {
187
+ const kind = cueKindOf(event);
188
+ const found = soundSetOf(sport);
189
+ if (kind === undefined || found === undefined)
190
+ return [];
191
+ return found.cues[kind] ?? [];
192
+ }
193
+ /**
194
+ * The cues of a move of a replay: the events it went by and the periods it began or ended, each with its voices. Only a move made by the clock playing (`tick`) or by a step forward calls
195
+ * cues; a seek, a jump back, a pause or a change of speed calls none, as scrubbing announces nothing. The end of the last period is the game's end.
196
+ * @example
197
+ * ```ts
198
+ * import { cuesOfChange } from "@johnmorrisdotca/kyogijo-stadiums/sound";
199
+ * const state = { t: 10, start: 0, end: 60, playing: true, ended: false, speed: 1, loop: false, motion: "smooth" as const, follow: "off" as const, followHeld: false };
200
+ * const change = { state, reason: "tick" as const, crossed: [{ t: 9.5, kind: "goal", team: "home" as const }] };
201
+ * console.log(cuesOfChange(change, 9, "nhl").map((c) => c.voice).join());
202
+ * console.log(cuesOfChange({ ...change, reason: "seek" as const }, 9, "nhl").length);
203
+ * // horn,swell
204
+ * // 0
205
+ * ```
206
+ */
207
+ export function cuesOfChange(change, from, sport, periods = []) {
208
+ if (change.reason !== "tick" && change.reason !== "step")
209
+ return [];
210
+ const to = change.state.t;
211
+ if (!(to > from))
212
+ return [];
213
+ const found = soundSetOf(sport);
214
+ if (found === undefined)
215
+ return [];
216
+ const marks = [];
217
+ for (const event of change.crossed) {
218
+ const kind = cueKindOf(event);
219
+ if (kind !== undefined)
220
+ marks.push(event.team === undefined ? { t: event.t, kind } : { t: event.t, kind, team: event.team });
221
+ }
222
+ periods.forEach((p, i) => {
223
+ if (p.start > from && p.start <= to)
224
+ marks.push({ t: p.start, kind: "period-start" });
225
+ if (p.end > from && p.end <= to)
226
+ marks.push({ t: p.end, kind: i === periods.length - 1 ? "game-end" : "period-end" });
227
+ });
228
+ marks.sort((a, b) => a.t - b.t);
229
+ const out = [];
230
+ for (const mark of marks)
231
+ for (const voice of found.cues[mark.kind] ?? [])
232
+ out.push(mark.team === undefined ? { voice, kind: mark.kind, t: mark.t } : { voice, kind: mark.kind, t: mark.t, team: mark.team });
233
+ return out;
234
+ }
@@ -0,0 +1,70 @@
1
+ import type { Sport } from "./types.ts";
2
+ /** How excited a crowd is, from a near-empty room to a roar. */
3
+ export type SoundLevel = "quiet" | "murmur" | "engaged" | "loud" | "roar";
4
+ /** The levels, quietest first. */
5
+ export declare const SOUND_LEVELS: readonly SoundLevel[];
6
+ /**
7
+ * The layers of a crowd bed, each a loop the engine keeps running while the sound is on, and turns up and down: `room` the air of the building, `murmur` people talking, `applause` hands, `roar`
8
+ * a crowd's voice together. The pool's bed has `water` as well.
9
+ */
10
+ export type SoundLayer = "room" | "murmur" | "applause" | "roar" | "water";
11
+ /** The voices a cue may use. Each is a short sound the engine makes in the browser, or plays from a file a page gives (`files`). */
12
+ export type SoundVoice =
13
+ /** An arena's goal horn: one long blast of a low chord, never a tune. */
14
+ "horn"
15
+ /** A referee's pea whistle, with its trill. */
16
+ | "whistle"
17
+ /** A long whistle and two short ones before it: the end of a game in football and the field games. */
18
+ | "whistle-end"
19
+ /** A scoreboard's buzzer at the end of a period. */
20
+ | "buzzer"
21
+ /** A ring's bell, struck three times. */
22
+ | "bell"
23
+ /** A starting gun. */
24
+ | "gun"
25
+ /** The electronic start of a swimming race. */
26
+ | "beep"
27
+ /** A crowd that rises for a moment: "ooh", a near miss. */
28
+ | "ooh"
29
+ /** A cheer of a few people, for a point in a quiet hall. */
30
+ | "cheer"
31
+ /** Applause that comes and goes. */
32
+ | "applause"
33
+ /** The whole crowd's roar, swelling and settling over some seconds: a goal. */
34
+ | "swell";
35
+ /** What a kind of venue sounds like: the bed's layers and the room they are heard in. */
36
+ export interface SoundBed {
37
+ /** The kind of venue, as in `SOUND_BEDS`. */
38
+ id: SoundBedId;
39
+ /** Indoors (an arena, a hall, a pool) or out (a stadium, a ballpark, a track) when the venue says nothing of its roof. */
40
+ indoor: boolean;
41
+ /** How long the room rings, in seconds, for a venue of `seats` people: a full arena is about 2 s, a stadium in the open about 0.6 s. */
42
+ decay: number;
43
+ /** The people the bed is made for: a venue of other size is louder or quieter than the bed by the share of people (the context's `seats`). */
44
+ seats: number;
45
+ /** The layers the bed has. */
46
+ layers: readonly SoundLayer[];
47
+ /** The crowd levels it offers, quietest first: a dojo for kyudo has no roar. */
48
+ levels: readonly SoundLevel[];
49
+ }
50
+ /** The kinds of venue that have a bed. */
51
+ export type SoundBedId = "arena" | "stadium" | "ballpark" | "hall" | "pool" | "dojo" | "ring" | "track";
52
+ /** The crowd beds, one for each kind of venue. */
53
+ export declare const SOUND_BEDS: Readonly<Record<SoundBedId, SoundBed>>;
54
+ /** The kinds of moment a cue answers. A timeline's own event kinds are read into these by `cueKindOf`. */
55
+ export type CueKind = "goal" | "point" | "shot" | "whistle" | "period-start" | "period-end" | "game-end" | "start" | "finish" | "round-start" | "round-end";
56
+ /** The kinds of moment, in the order the docs list them. */
57
+ export declare const CUE_KINDS: readonly CueKind[];
58
+ /** A sport's sound: the bed of its usual venue and what plays for each kind of moment (nothing, for a kind it does not list). */
59
+ export interface SoundSet {
60
+ /** The set's id: the sport's family (`hockey`, `soccer`, `court`, ...). */
61
+ id: string;
62
+ bed: SoundBedId;
63
+ cues: Readonly<Partial<Record<CueKind, readonly SoundVoice[]>>>;
64
+ /** The crowd levels this sport offers, when fewer than its bed's (kyudo: a still hall). */
65
+ levels?: readonly SoundLevel[];
66
+ }
67
+ /** The sound sets, by family: a sport takes its family's (`SOUND_OF_SPORT`). */
68
+ export declare const SOUND_SETS: Readonly<Record<string, SoundSet>>;
69
+ /** Which set each sport takes: every surface the package draws has one. */
70
+ export declare const SOUND_OF_SPORT: Readonly<Record<Sport, string>>;
@@ -0,0 +1,85 @@
1
+ /** The levels, quietest first. */
2
+ export const SOUND_LEVELS = ["quiet", "murmur", "engaged", "loud", "roar"];
3
+ const ALL = SOUND_LEVELS;
4
+ const CROWD = ["room", "murmur", "applause", "roar"];
5
+ /** The crowd beds, one for each kind of venue. */
6
+ export const SOUND_BEDS = {
7
+ arena: { id: "arena", indoor: true, decay: 2.2, seats: 18000, layers: CROWD, levels: ALL },
8
+ stadium: { id: "stadium", indoor: false, decay: 0.7, seats: 50000, layers: CROWD, levels: ALL },
9
+ ballpark: { id: "ballpark", indoor: false, decay: 0.6, seats: 40000, layers: CROWD, levels: ALL },
10
+ hall: { id: "hall", indoor: true, decay: 1.6, seats: 4000, layers: CROWD, levels: ALL },
11
+ pool: { id: "pool", indoor: true, decay: 2.6, seats: 5000, layers: ["room", "water", "murmur", "applause", "roar"], levels: ALL },
12
+ dojo: { id: "dojo", indoor: true, decay: 1.3, seats: 2000, layers: ["room", "murmur", "applause"], levels: ["quiet", "murmur", "engaged", "loud"] },
13
+ ring: { id: "ring", indoor: true, decay: 1.8, seats: 8000, layers: CROWD, levels: ALL },
14
+ track: { id: "track", indoor: false, decay: 0.8, seats: 30000, layers: CROWD, levels: ALL },
15
+ };
16
+ /** The kinds of moment, in the order the docs list them. */
17
+ export const CUE_KINDS = ["goal", "point", "shot", "whistle", "period-start", "period-end", "game-end", "start", "finish", "round-start", "round-end"];
18
+ const FIELD = { goal: ["swell"], point: ["cheer"], shot: ["ooh"], whistle: ["whistle"], "period-start": ["whistle"], "period-end": ["whistle-end"], "game-end": ["whistle-end", "swell"] };
19
+ const COURT = { goal: ["applause"], point: ["applause"], shot: ["ooh"], whistle: ["whistle"], "period-end": ["whistle"], "game-end": ["applause", "swell"] };
20
+ const set = (id, bed, cues, levels) => (levels === undefined ? { id, bed, cues } : { id, bed, cues, levels });
21
+ /** The sound sets, by family: a sport takes its family's (`SOUND_OF_SPORT`). */
22
+ export const SOUND_SETS = {
23
+ hockey: set("hockey", "arena", { goal: ["horn", "swell"], point: ["horn", "swell"], shot: ["ooh"], whistle: ["whistle"], "period-end": ["buzzer"], "game-end": ["buzzer", "horn", "swell"] }),
24
+ basketball: set("basketball", "arena", { goal: ["cheer"], point: ["cheer"], shot: ["ooh"], whistle: ["whistle"], "period-end": ["buzzer"], "game-end": ["buzzer", "swell"] }),
25
+ football: set("football", "stadium", { ...FIELD, "period-end": ["whistle"], "game-end": ["whistle", "swell"] }),
26
+ soccer: set("soccer", "stadium", FIELD),
27
+ field: set("field", "stadium", FIELD),
28
+ baseball: set("baseball", "ballpark", { goal: ["swell"], point: ["swell"], shot: ["ooh"], "game-end": ["swell"] }),
29
+ cricket: set("cricket", "ballpark", { goal: ["applause"], point: ["applause"], shot: ["ooh"], "game-end": ["applause"] }),
30
+ court: set("court", "hall", COURT),
31
+ curling: set("curling", "hall", { goal: ["applause"], point: ["applause"], shot: ["ooh"], "game-end": ["applause"] }, ["quiet", "murmur", "engaged"]),
32
+ pool: set("pool", "pool", { start: ["beep"], goal: ["swell"], point: ["cheer"], finish: ["swell"], whistle: ["whistle"], "game-end": ["swell"] }),
33
+ track: set("track", "track", { start: ["gun"], finish: ["swell"], goal: ["swell"], point: ["cheer"], "game-end": ["applause"] }),
34
+ skating: set("skating", "arena", { start: ["gun"], finish: ["swell"], goal: ["swell"], point: ["applause"], "game-end": ["applause"] }),
35
+ budo: set("budo", "dojo", { goal: ["applause"], point: ["applause"], "game-end": ["applause"] }),
36
+ kyudo: set("kyudo", "dojo", { goal: ["applause"], point: ["applause"], "game-end": ["applause"] }, ["quiet", "murmur"]),
37
+ sumo: set("sumo", "ring", { goal: ["swell"], point: ["swell"], "game-end": ["swell"] }),
38
+ combat: set("combat", "ring", { goal: ["swell"], point: ["ooh"], shot: ["ooh"], "round-start": ["bell"], "round-end": ["bell"], "game-end": ["bell", "swell"] }),
39
+ };
40
+ /** Which set each sport takes: every surface the package draws has one. */
41
+ export const SOUND_OF_SPORT = {
42
+ nfl: "football",
43
+ cfl: "football",
44
+ ifaf: "football",
45
+ nhl: "hockey",
46
+ nba: "basketball",
47
+ mlb: "baseball",
48
+ soccer: "soccer",
49
+ curling: "curling",
50
+ tennis: "court",
51
+ volleyball: "court",
52
+ badminton: "court",
53
+ pickleball: "court",
54
+ tabletennis: "court",
55
+ padel: "court",
56
+ squash: "court",
57
+ racquetball: "court",
58
+ sepaktakraw: "court",
59
+ teqball: "court",
60
+ roundnet: "court",
61
+ netball: "court",
62
+ handball: "court",
63
+ swimming: "pool",
64
+ waterpolo: "pool",
65
+ diving: "pool",
66
+ artisticswimming: "pool",
67
+ athletics: "track",
68
+ skating: "skating",
69
+ judo: "budo",
70
+ karate: "budo",
71
+ kendo: "budo",
72
+ dojo: "budo",
73
+ kyudo: "kyudo",
74
+ sumo: "sumo",
75
+ mma: "combat",
76
+ boxing: "combat",
77
+ wrestling: "combat",
78
+ prowrestling: "combat",
79
+ lacrosse: "field",
80
+ rugby: "field",
81
+ afl: "field",
82
+ gaelic: "field",
83
+ fieldhockey: "field",
84
+ cricket: "cricket",
85
+ };
@@ -0,0 +1,38 @@
1
+ import type { SoundLayer, SoundVoice } from "./sound.sets.ts";
2
+ /** A seeded generator of numbers from 0 to 1 (mulberry32): the noise is the same every time. */
3
+ export declare function seeded(seed: number): () => number;
4
+ /** The colours of noise: `white` flat, `pink` falling 3 dB an octave (a crowd's), `brown` falling 6 (a building's air). */
5
+ export type NoiseColour = "white" | "pink" | "brown";
6
+ /** Samples of noise of a colour, from a seed, between -1 and 1. */
7
+ export declare function noiseSamples(length: number, seed: number, colour?: NoiseColour): Float32Array;
8
+ /** Samples of hands clapping: many short bursts of noise at random moments, `density` claps a second, for `length` samples at `rate`. */
9
+ export declare function applauseSamples(length: number, rate: number, seed: number, density?: number): Float32Array;
10
+ /** Samples of people talking: noise whose level rises and falls with syllables, about four a second from each of many voices, summed. */
11
+ export declare function murmurSamples(length: number, rate: number, seed: number, voices?: number): Float32Array;
12
+ /** A room's echo as an impulse response: noise falling 60 dB over `decay` seconds, two channels a little apart. */
13
+ export declare function impulseSamples(decay: number, rate: number, seed: number): [Float32Array, Float32Array];
14
+ /** A buffer of the context from samples (one channel, or two). */
15
+ export declare function bufferOf(context: BaseAudioContext, channels: readonly Float32Array[]): AudioBuffer;
16
+ /** How many seconds each made layer loops for: long enough that the loop is not heard. */
17
+ export declare const LAYER_SECONDS = 6;
18
+ /**
19
+ * How much each made layer is turned down or up so that, through its filter, it is as loud as the demo's recordings are made (an RMS of -20 dBFS; the pool's water -22, and the building's air
20
+ * a quiet -30): measured once in a browser by rendering each layer offline.
21
+ */
22
+ export declare const LAYER_TRIM: Readonly<Record<SoundLayer, number>>;
23
+ /** The buffer a layer loops when it has no recording. */
24
+ export declare function layerBuffer(context: BaseAudioContext, layer: SoundLayer): AudioBuffer;
25
+ /** The filter that shapes a made layer: the building's low air, water's hiss, talk's middle, a crowd's voice. */
26
+ export declare function layerFilter(context: BaseAudioContext, layer: SoundLayer): AudioNode;
27
+ /** The loops a made voice is cut from, made the first time a voice needs one and kept by the caller. */
28
+ export interface VoiceBuffers {
29
+ /** Pink noise. */
30
+ noise(): AudioBuffer;
31
+ /** Hands clapping. */
32
+ applause(): AudioBuffer;
33
+ }
34
+ /**
35
+ * Make a voice in the browser at `when` on the context's clock, into `out`, from the noise of `noise` (a pink loop). Returns when it ends. The horn is one chord held, never a tune; the
36
+ * swell is not here, as it moves the bed's roar (the engine does it).
37
+ */
38
+ export declare function makeVoice(context: BaseAudioContext, voice: Exclude<SoundVoice, "swell">, out: AudioNode, buffers: VoiceBuffers, when: number): number;