@joewinke/jatui 0.46.0 → 0.48.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.
- package/README.md +46 -0
- package/dist/ambience/README.md +131 -0
- package/dist/ambience/breath.d.ts +22 -0
- package/dist/ambience/breath.js +43 -0
- package/dist/ambience/default.d.ts +24 -0
- package/dist/ambience/default.js +34 -0
- package/dist/ambience/engine.d.ts +21 -0
- package/dist/ambience/engine.js +313 -0
- package/dist/ambience/index.d.ts +23 -0
- package/dist/ambience/index.js +27 -0
- package/dist/ambience/master.d.ts +49 -0
- package/dist/ambience/master.js +83 -0
- package/dist/ambience/persist.d.ts +11 -0
- package/dist/ambience/persist.js +24 -0
- package/dist/ambience/registry.d.ts +11 -0
- package/dist/ambience/registry.js +16 -0
- package/dist/ambience/scapes/focus.d.ts +55 -0
- package/dist/ambience/scapes/focus.js +245 -0
- package/dist/ambience/scapes/hibiscus.d.ts +103 -0
- package/dist/ambience/scapes/hibiscus.js +345 -0
- package/dist/ambience/scapes/nature.d.ts +36 -0
- package/dist/ambience/scapes/nature.js +325 -0
- package/dist/ambience/scapes/noise.d.ts +77 -0
- package/dist/ambience/scapes/noise.js +226 -0
- package/dist/ambience/scapes/playlist.d.ts +129 -0
- package/dist/ambience/scapes/playlist.js +569 -0
- package/dist/ambience/types.d.ts +80 -0
- package/dist/ambience/types.js +9 -0
- package/dist/components/AmbienceToggle.svelte +599 -0
- package/dist/components/AmbienceToggle.svelte.d.ts +74 -0
- package/dist/components/VideoPlayer.svelte +73 -6
- package/dist/components/VideoPlayer.svelte.d.ts +9 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -0
- package/dist/video/keys.d.ts +4 -3
- package/dist/video/keys.js +5 -4
- package/dist/video/settings/remembered.d.ts +9 -0
- package/dist/video/settings/remembered.js +9 -0
- package/package.json +6 -1
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
export const HIBISCUS = Object.freeze({
|
|
2
|
+
/** Drone partials: F2 + fifth (C3), tenth (A3), twelfth (C4). */
|
|
3
|
+
DRONE_HZ: [87.31, 130.81, 220.0, 261.63],
|
|
4
|
+
/** Relative level of each drone partial, brightest at the bottom. */
|
|
5
|
+
DRONE_GAINS: [1, 0.55, 0.32, 0.2],
|
|
6
|
+
/** Stereo pair detune (± cents) — the "width" of the pad. */
|
|
7
|
+
DETUNE_CENTS: 7,
|
|
8
|
+
/** Where the detuned pairs sit in the stereo field. */
|
|
9
|
+
PAD_PAN: 0.35,
|
|
10
|
+
/** Soft wave: y = x + SOFT_3RD·x³ + SOFT_5TH·x⁵, normalised — odd
|
|
11
|
+
* symmetry, so a sine keeps its fundamental and gains a little 3rd and
|
|
12
|
+
* 5th harmonic and nothing else. */
|
|
13
|
+
SOFT_3RD: 0.16,
|
|
14
|
+
SOFT_5TH: 0.05,
|
|
15
|
+
/** Pad lowpass: resting frequency and the in-breath lift (+Hz at peak). */
|
|
16
|
+
PAD_BASE_HZ: 420,
|
|
17
|
+
PAD_BREATH_HZ: 180,
|
|
18
|
+
PAD_Q: 0.6,
|
|
19
|
+
PAD_LEVEL: 0.15,
|
|
20
|
+
/** How much of each section's level rides the breath (0..1 — the level
|
|
21
|
+
* sinks to (1 - depth)·level at the bottom of the exhale). */
|
|
22
|
+
PAD_BREATH_DEPTH: 0.25,
|
|
23
|
+
FLOAT_LEVEL: 0.09,
|
|
24
|
+
FLOAT_BREATH_DEPTH: 0.2,
|
|
25
|
+
NOISE_LEVEL: 0.01,
|
|
26
|
+
NOISE_BREATH_DEPTH: 0.25,
|
|
27
|
+
/** Floating tones: F4 → A4 → C5 → A4 (→ F4 …). */
|
|
28
|
+
TONES_HZ: [349.23, 440.0, 523.25, 440.0],
|
|
29
|
+
TONE_HOLD_S: 70,
|
|
30
|
+
TONE_JITTER_S: 8,
|
|
31
|
+
TONE_FADE_S: 16,
|
|
32
|
+
/** Chimes: the current chord an octave up (F5, A5, C6), first 3 partials. */
|
|
33
|
+
CHIME_HZ: [698.46, 880.0, 1046.5],
|
|
34
|
+
CHIME_PARTIAL_GAINS: [1, 0.35, 0.18],
|
|
35
|
+
CHIME_MIN_GAP_S: 35,
|
|
36
|
+
CHIME_MAX_GAP_S: 65,
|
|
37
|
+
CHIME_ATTACK_S: 2.5,
|
|
38
|
+
CHIME_TAU_S: 3.5,
|
|
39
|
+
/** Oscillators stop after this many decay taus. */
|
|
40
|
+
CHIME_TAILS: 7,
|
|
41
|
+
CHIME_LEVEL: 0.05,
|
|
42
|
+
CHIME_PAN_SPREAD: 0.8,
|
|
43
|
+
/** Air noise. */
|
|
44
|
+
AIR_SECONDS: 4,
|
|
45
|
+
AIR_HP_HZ: 80,
|
|
46
|
+
AIR_LP_HZ: 420,
|
|
47
|
+
/** Internal entrance fade — the engine bus fade handles crossfades; this
|
|
48
|
+
* just keeps a fresh build from stepping in at full level. */
|
|
49
|
+
ENTRANCE_S: 6,
|
|
50
|
+
/** Smoothing tau for the per-tick breath modulation. */
|
|
51
|
+
MOD_TAU_S: 0.45
|
|
52
|
+
});
|
|
53
|
+
/** A soft wave curve: fundamental plus a little 3rd and 5th, nothing else. */
|
|
54
|
+
export function softWaveCurve(n = 1024) {
|
|
55
|
+
const { SOFT_3RD: a, SOFT_5TH: b } = HIBISCUS;
|
|
56
|
+
const curve = new Float32Array(n);
|
|
57
|
+
let max = 0;
|
|
58
|
+
for (let i = 0; i < n; i++) {
|
|
59
|
+
const x = (i / (n - 1)) * 2 - 1;
|
|
60
|
+
const y = x + a * x ** 3 + b * x ** 5;
|
|
61
|
+
curve[i] = y;
|
|
62
|
+
max = Math.max(max, Math.abs(y));
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < n; i++)
|
|
65
|
+
curve[i] /= max;
|
|
66
|
+
return curve;
|
|
67
|
+
}
|
|
68
|
+
/** Pink noise (Paul Kellet's filter), one channel's worth, in place. */
|
|
69
|
+
function fillPink(data, rand) {
|
|
70
|
+
let b0 = 0, b1 = 0, b2 = 0, b3 = 0, b4 = 0, b5 = 0, b6 = 0;
|
|
71
|
+
for (let i = 0; i < data.length; i++) {
|
|
72
|
+
const w = rand() * 2 - 1;
|
|
73
|
+
b0 = 0.99886 * b0 + w * 0.0555179;
|
|
74
|
+
b1 = 0.99332 * b1 + w * 0.0750759;
|
|
75
|
+
b2 = 0.969 * b2 + w * 0.1538522;
|
|
76
|
+
b3 = 0.8665 * b3 + w * 0.3104856;
|
|
77
|
+
b4 = 0.55 * b4 + w * 0.5329522;
|
|
78
|
+
b5 = -0.7616 * b5 - w * 0.016898;
|
|
79
|
+
data[i] = (b0 + b1 + b2 + b3 + b4 + b5 + b6 + w * 0.5362) * 0.11;
|
|
80
|
+
b6 = w * 0.115926;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** A stereo panner when the context has one, a plain gain otherwise (the
|
|
84
|
+
* test fake — and any context too old to pan — falls back to mono). */
|
|
85
|
+
function makePan(audio, pan) {
|
|
86
|
+
if (typeof audio.createStereoPanner === 'function') {
|
|
87
|
+
const p = audio.createStereoPanner();
|
|
88
|
+
p.pan.value = pan;
|
|
89
|
+
return p;
|
|
90
|
+
}
|
|
91
|
+
return audio.createGain();
|
|
92
|
+
}
|
|
93
|
+
export const hibiscusScape = {
|
|
94
|
+
id: 'hibiscus',
|
|
95
|
+
label: 'Hibiscus',
|
|
96
|
+
group: 'meditation',
|
|
97
|
+
description: 'Warm F-major drone, floating tones and soft chimes — six breaths a minute.',
|
|
98
|
+
build(scapeCtx) {
|
|
99
|
+
const { audio, rand } = scapeCtx;
|
|
100
|
+
const H = HIBISCUS;
|
|
101
|
+
/** Every node this build created — the final disconnect walks it. */
|
|
102
|
+
const allNodes = [];
|
|
103
|
+
const keep = (node) => {
|
|
104
|
+
allNodes.push(node);
|
|
105
|
+
return node;
|
|
106
|
+
};
|
|
107
|
+
function disconnectAll(nodes) {
|
|
108
|
+
for (const n of nodes) {
|
|
109
|
+
try {
|
|
110
|
+
n.disconnect();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
/* already gone */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ---- pad ------------------------------------------------------------
|
|
118
|
+
const padFilter = keep(audio.createBiquadFilter());
|
|
119
|
+
padFilter.type = 'lowpass';
|
|
120
|
+
padFilter.frequency.value = H.PAD_BASE_HZ;
|
|
121
|
+
padFilter.Q.value = H.PAD_Q;
|
|
122
|
+
const padGain = keep(audio.createGain());
|
|
123
|
+
padGain.gain.value = 0;
|
|
124
|
+
padFilter.connect(padGain);
|
|
125
|
+
padGain.connect(scapeCtx.input);
|
|
126
|
+
const padOscs = [];
|
|
127
|
+
H.DRONE_HZ.forEach((hz, i) => {
|
|
128
|
+
const shaper = keep(audio.createWaveShaper());
|
|
129
|
+
shaper.curve = softWaveCurve();
|
|
130
|
+
const partial = keep(audio.createGain());
|
|
131
|
+
partial.gain.value = H.DRONE_GAINS[i];
|
|
132
|
+
shaper.connect(partial);
|
|
133
|
+
partial.connect(padFilter);
|
|
134
|
+
for (const side of [-1, 1]) {
|
|
135
|
+
const osc = keep(audio.createOscillator());
|
|
136
|
+
osc.type = 'sine';
|
|
137
|
+
osc.frequency.value = hz;
|
|
138
|
+
osc.detune.value = side * H.DETUNE_CENTS;
|
|
139
|
+
const pan = keep(makePan(audio, side * H.PAD_PAN));
|
|
140
|
+
osc.connect(pan).connect(shaper);
|
|
141
|
+
padOscs.push(osc);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
// ---- floating pair ----------------------------------------------------
|
|
145
|
+
const floatGain = keep(audio.createGain());
|
|
146
|
+
floatGain.gain.value = 0;
|
|
147
|
+
floatGain.connect(scapeCtx.input);
|
|
148
|
+
const voices = [0, 1].map(() => {
|
|
149
|
+
const osc = keep(audio.createOscillator());
|
|
150
|
+
osc.type = 'sine';
|
|
151
|
+
const gain = keep(audio.createGain());
|
|
152
|
+
gain.gain.value = 0;
|
|
153
|
+
osc.connect(gain);
|
|
154
|
+
gain.connect(floatGain);
|
|
155
|
+
return { osc, gain };
|
|
156
|
+
});
|
|
157
|
+
// ---- air --------------------------------------------------------------
|
|
158
|
+
const rate = audio.sampleRate;
|
|
159
|
+
const airBuffer = audio.createBuffer(2, Math.floor(rate * H.AIR_SECONDS), rate);
|
|
160
|
+
for (let ch = 0; ch < 2; ch++)
|
|
161
|
+
fillPink(airBuffer.getChannelData(ch), rand);
|
|
162
|
+
const airSrc = keep(audio.createBufferSource());
|
|
163
|
+
airSrc.buffer = airBuffer;
|
|
164
|
+
airSrc.loop = true;
|
|
165
|
+
const airHp = keep(audio.createBiquadFilter());
|
|
166
|
+
airHp.type = 'highpass';
|
|
167
|
+
airHp.frequency.value = H.AIR_HP_HZ;
|
|
168
|
+
const airLp = keep(audio.createBiquadFilter());
|
|
169
|
+
airLp.type = 'lowpass';
|
|
170
|
+
airLp.frequency.value = H.AIR_LP_HZ;
|
|
171
|
+
const airGain = keep(audio.createGain());
|
|
172
|
+
airGain.gain.value = 0;
|
|
173
|
+
airSrc.connect(airHp);
|
|
174
|
+
airHp.connect(airLp);
|
|
175
|
+
airLp.connect(airGain);
|
|
176
|
+
airGain.connect(scapeCtx.input);
|
|
177
|
+
// ---- chime bus ----------------------------------------------------------
|
|
178
|
+
const chimeGain = keep(audio.createGain());
|
|
179
|
+
chimeGain.gain.value = 1;
|
|
180
|
+
chimeGain.connect(scapeCtx.input);
|
|
181
|
+
// ---- scheduler state ----------------------------------------------------
|
|
182
|
+
let toneIdx = 1; // voice 0 starts on TONES[0]; the next drift goes to A4
|
|
183
|
+
let activeVoice = 0;
|
|
184
|
+
let nextToneAt = Number.POSITIVE_INFINITY;
|
|
185
|
+
let nextChimeAt = Number.POSITIVE_INFINITY;
|
|
186
|
+
let chimesOn = true;
|
|
187
|
+
let entranceUntil = 0;
|
|
188
|
+
let unsub = null;
|
|
189
|
+
let activeChimes = [];
|
|
190
|
+
const gap = () => H.CHIME_MIN_GAP_S + rand() * (H.CHIME_MAX_GAP_S - H.CHIME_MIN_GAP_S);
|
|
191
|
+
function breathMul(value, depth) {
|
|
192
|
+
return 1 - depth * (1 - value);
|
|
193
|
+
}
|
|
194
|
+
function rampTo(param, v, at, dur) {
|
|
195
|
+
param.cancelScheduledValues(at);
|
|
196
|
+
param.setValueAtTime(param.value, at);
|
|
197
|
+
param.linearRampToValueAtTime(v, at + dur);
|
|
198
|
+
}
|
|
199
|
+
/** Hand the drift to the other voice: set its tone, cross-fade the two
|
|
200
|
+
* gains over TONE_FADE_S, re-arm the next drift ~TONE_HOLD_S out. */
|
|
201
|
+
function driftTone(now) {
|
|
202
|
+
const freq = H.TONES_HZ[toneIdx % H.TONES_HZ.length];
|
|
203
|
+
toneIdx++;
|
|
204
|
+
const incoming = voices[1 - activeVoice];
|
|
205
|
+
const outgoing = voices[activeVoice];
|
|
206
|
+
incoming.osc.frequency.setValueAtTime(freq, now);
|
|
207
|
+
rampTo(incoming.gain.gain, 1, now, H.TONE_FADE_S);
|
|
208
|
+
rampTo(outgoing.gain.gain, 0, now, H.TONE_FADE_S);
|
|
209
|
+
activeVoice = 1 - activeVoice;
|
|
210
|
+
nextToneAt = now + H.TONE_HOLD_S + (rand() - 0.5) * 2 * H.TONE_JITTER_S;
|
|
211
|
+
}
|
|
212
|
+
/** One soft rounded chime: 3 partials of a chord tone, a slow swell,
|
|
213
|
+
* then a long exponential decay (setTargetAtTime), random pan. */
|
|
214
|
+
function chime(now) {
|
|
215
|
+
const f = H.CHIME_HZ[Math.floor(rand() * H.CHIME_HZ.length)];
|
|
216
|
+
const pan = keep(makePan(audio, (rand() * 2 - 1) * H.CHIME_PAN_SPREAD));
|
|
217
|
+
pan.connect(chimeGain);
|
|
218
|
+
const env = keep(audio.createGain());
|
|
219
|
+
env.gain.value = 0;
|
|
220
|
+
env.connect(pan);
|
|
221
|
+
const oscs = [];
|
|
222
|
+
const pgs = [];
|
|
223
|
+
H.CHIME_PARTIAL_GAINS.forEach((g, i) => {
|
|
224
|
+
const osc = keep(audio.createOscillator());
|
|
225
|
+
osc.type = 'sine';
|
|
226
|
+
osc.frequency.value = f * (i + 1);
|
|
227
|
+
const pg = keep(audio.createGain());
|
|
228
|
+
pg.gain.value = g;
|
|
229
|
+
osc.connect(pg);
|
|
230
|
+
pg.connect(env);
|
|
231
|
+
oscs.push(osc);
|
|
232
|
+
pgs.push(pg);
|
|
233
|
+
});
|
|
234
|
+
const tail = H.CHIME_ATTACK_S + H.CHIME_TAILS * H.CHIME_TAU_S;
|
|
235
|
+
for (const osc of oscs) {
|
|
236
|
+
osc.start(now);
|
|
237
|
+
osc.stop(now + tail);
|
|
238
|
+
}
|
|
239
|
+
// A swell, not a strike: slow attack, then exponential decay.
|
|
240
|
+
env.gain.setValueAtTime(0, now);
|
|
241
|
+
env.gain.linearRampToValueAtTime(H.CHIME_LEVEL, now + H.CHIME_ATTACK_S);
|
|
242
|
+
env.gain.setTargetAtTime(0.0001, now + H.CHIME_ATTACK_S, H.CHIME_TAU_S);
|
|
243
|
+
// Self-cleaning: disconnect on a timer once the tail is done — never
|
|
244
|
+
// on a lookahead cb (a stopped instance's cbs get retired mid-fade).
|
|
245
|
+
const nodes = [pan, env, ...pgs, ...oscs];
|
|
246
|
+
const timer = setTimeout(() => disconnectAll(nodes), (tail + 0.05) * 1000);
|
|
247
|
+
activeChimes.push({ oscs, nodes, until: now + tail, timer });
|
|
248
|
+
nextChimeAt = now + gap();
|
|
249
|
+
}
|
|
250
|
+
/** The ONE per-tick callback: breath modulation, chime retirement,
|
|
251
|
+
* tone drift, chime scheduling. Runs every engine tick (~250 ms). */
|
|
252
|
+
function onTick(now) {
|
|
253
|
+
const b = scapeCtx.breath.state;
|
|
254
|
+
// The pad filter opens ~180 Hz on the in-breath; the three section
|
|
255
|
+
// levels swell with it. During the entrance the ramps own the
|
|
256
|
+
// levels (the filter is silent-safe and starts modulating at once).
|
|
257
|
+
padFilter.frequency.setTargetAtTime(H.PAD_BASE_HZ + H.PAD_BREATH_HZ * b.value, now, H.MOD_TAU_S);
|
|
258
|
+
if (now >= entranceUntil) {
|
|
259
|
+
padGain.gain.setTargetAtTime(H.PAD_LEVEL * breathMul(b.value, H.PAD_BREATH_DEPTH), now, H.MOD_TAU_S);
|
|
260
|
+
floatGain.gain.setTargetAtTime(H.FLOAT_LEVEL * breathMul(b.value, H.FLOAT_BREATH_DEPTH), now, H.MOD_TAU_S);
|
|
261
|
+
airGain.gain.setTargetAtTime(H.NOISE_LEVEL * breathMul(b.value, H.NOISE_BREATH_DEPTH), now, H.MOD_TAU_S);
|
|
262
|
+
}
|
|
263
|
+
// Bookkeeping only — no disconnects here (teardown never rides the
|
|
264
|
+
// lookahead queue). Chimes past their tail prune themselves; their
|
|
265
|
+
// nodes were already disconnected by their own timers.
|
|
266
|
+
if (activeChimes.length) {
|
|
267
|
+
activeChimes = activeChimes.filter((c) => now < c.until);
|
|
268
|
+
}
|
|
269
|
+
if (now >= nextToneAt)
|
|
270
|
+
driftTone(now);
|
|
271
|
+
if (chimesOn && now >= nextChimeAt)
|
|
272
|
+
chime(now);
|
|
273
|
+
}
|
|
274
|
+
function tryStop(node, at) {
|
|
275
|
+
try {
|
|
276
|
+
node.stop(at);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
/* already stopped */
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
start(at) {
|
|
284
|
+
// Entrance: sections rise from silence over ENTRANCE_S; breath
|
|
285
|
+
// modulation of the levels takes over once they arrive.
|
|
286
|
+
entranceUntil = at + H.ENTRANCE_S;
|
|
287
|
+
rampTo(padGain.gain, H.PAD_LEVEL, at, H.ENTRANCE_S);
|
|
288
|
+
rampTo(floatGain.gain, H.FLOAT_LEVEL, at, H.ENTRANCE_S);
|
|
289
|
+
rampTo(airGain.gain, H.NOISE_LEVEL, at, H.ENTRANCE_S);
|
|
290
|
+
// Voice 0 holds the first tone; voice 1 waits silent.
|
|
291
|
+
voices[0].osc.frequency.setValueAtTime(H.TONES_HZ[0], at);
|
|
292
|
+
voices[0].gain.gain.setValueAtTime(1, at);
|
|
293
|
+
nextToneAt = at + H.TONE_HOLD_S + (rand() - 0.5) * 2 * H.TONE_JITTER_S;
|
|
294
|
+
nextChimeAt = at + gap();
|
|
295
|
+
for (const osc of padOscs)
|
|
296
|
+
osc.start(at);
|
|
297
|
+
voices[0].osc.start(at);
|
|
298
|
+
voices[1].osc.start(at);
|
|
299
|
+
// Random loop offset: the air never audibly repeats.
|
|
300
|
+
airSrc.start(at, rand() * H.AIR_SECONDS);
|
|
301
|
+
unsub = scapeCtx.lookahead(onTick);
|
|
302
|
+
},
|
|
303
|
+
stop(at, fade) {
|
|
304
|
+
unsub?.();
|
|
305
|
+
unsub = null;
|
|
306
|
+
const end = at + fade;
|
|
307
|
+
for (const g of [padGain, floatGain, airGain, chimeGain, voices[0].gain, voices[1].gain]) {
|
|
308
|
+
rampTo(g.gain, 0, at, fade);
|
|
309
|
+
}
|
|
310
|
+
for (const osc of padOscs)
|
|
311
|
+
tryStop(osc, end);
|
|
312
|
+
tryStop(voices[0].osc, end);
|
|
313
|
+
tryStop(voices[1].osc, end);
|
|
314
|
+
tryStop(airSrc, end);
|
|
315
|
+
for (const c of activeChimes) {
|
|
316
|
+
for (const osc of c.oscs)
|
|
317
|
+
tryStop(osc, end);
|
|
318
|
+
clearTimeout(c.timer); // the final disconnect covers them
|
|
319
|
+
}
|
|
320
|
+
// Final teardown OFF the lookahead path: the engine retires a
|
|
321
|
+
// stopped instance's cbs at fade·1000 + 250 ms, so anything
|
|
322
|
+
// waiting for one more tick can be dropped mid-fade and leave
|
|
323
|
+
// nodes connected to the shared input. Disconnect on a timer
|
|
324
|
+
// instead — after the gains have finished ramping to 0.
|
|
325
|
+
setTimeout(() => disconnectAll(allNodes), fade * 1000 + 50);
|
|
326
|
+
},
|
|
327
|
+
setParam(key, value) {
|
|
328
|
+
if (key === 'breathsPerMinute') {
|
|
329
|
+
// The SHARED clock — every sounding scape breathes together.
|
|
330
|
+
scapeCtx.breath.bpm = Math.min(15, Math.max(1, value));
|
|
331
|
+
}
|
|
332
|
+
else if (key === 'chimes') {
|
|
333
|
+
chimesOn = value !== 0;
|
|
334
|
+
if (chimesOn) {
|
|
335
|
+
// A fresh full gap, not an instant chime on re-enable.
|
|
336
|
+
nextChimeAt = audio.currentTime + gap();
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
nextChimeAt = Number.POSITIVE_INFINITY;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nature scapes — rain and waves (phase 2, jatui-5y1y7).
|
|
3
|
+
*
|
|
4
|
+
* Two scapes against the Scape contract in ../types.ts. Nothing here is
|
|
5
|
+
* registered globally — the integration task wires the default registry.
|
|
6
|
+
*
|
|
7
|
+
* rain a filtered noise bed plus sparse randomized droplet transients
|
|
8
|
+
* (short bandpassed noise bursts with varied pitch, pan and level,
|
|
9
|
+
* scheduled on the engine's lookahead tick; `density` sets how busy
|
|
10
|
+
* the patter is)
|
|
11
|
+
* waves noise through a slowly swelling bandpass with a scheduler-driven
|
|
12
|
+
* level LFO — randomized 8–12 s swell periods, each swell rising
|
|
13
|
+
* over ~45% of its period and decaying into a soft wash tail
|
|
14
|
+
* (setTargetAtTime with a multi-second tau)
|
|
15
|
+
*
|
|
16
|
+
* CPU rules (task brief): no per-sample JS — everything is AudioBufferSource
|
|
17
|
+
* loops and native filter/gain nodes. The noise buffer is generated ONCE per
|
|
18
|
+
* AudioContext (WeakMap cache) and every user of it — bed and droplets alike
|
|
19
|
+
* — loops or slices it with a randomized start offset, so the bed never
|
|
20
|
+
* audibly repeats and droplets never land on the same grain twice.
|
|
21
|
+
*
|
|
22
|
+
* Cleanup contract (per WideFrost's contract note): the engine RETIRES a
|
|
23
|
+
* stopped instance's lookahead callbacks at fade*1000 + 250 ms — so teardown
|
|
24
|
+
* must NOT wait for a lookahead tick (it would race the retire and can be
|
|
25
|
+
* dropped, leaving nodes wired to the shared input). stop(at, fade) schedules
|
|
26
|
+
* source.stop(at+fade) on the AUDIO clock and disconnects every node on a
|
|
27
|
+
* wall-clock setTimeout(fade*1000 + 50) — comfortably ahead of the retire.
|
|
28
|
+
* Lookahead ticks only ever schedule sound (and prune long-play droplets).
|
|
29
|
+
*
|
|
30
|
+
* Background tabs: browsers throttle setInterval (down to ~1/min) while the
|
|
31
|
+
* audio clock keeps running, so both schedulers resync after a tick gap
|
|
32
|
+
* instead of firing every missed event as a burst when the tab returns.
|
|
33
|
+
*/
|
|
34
|
+
import type { Scape } from '../types';
|
|
35
|
+
export declare const rain: Scape;
|
|
36
|
+
export declare const waves: Scape;
|