@maccesar/titools 5.2.0 → 5.2.2
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/CONTRIBUTING.md +82 -0
- package/README.md +83 -72
- package/agents/ti-pro.md +1 -1
- package/commands/ti-audit.md +2 -2
- package/docs/examples.md +91 -0
- package/docs/knowledge-index.md +57 -0
- package/lib/commands/uninstall.js +19 -16
- package/lib/commands/update.js +1 -1
- package/lib/downloader.js +1 -1
- package/lib/utils.js +2 -0
- package/package.json +4 -2
- package/skills/purgetss/references/multi-density-images.md +1 -1
- package/skills/ti-admob-unified/SKILL.md +21 -7
- package/skills/ti-admob-unified/assets/example/app.js +229 -25
- package/skills/ti-admob-unified/assets/example/lib/ads.js +30 -2
- package/skills/ti-admob-unified/assets/example/library-usage.js +33 -1
- package/skills/ti-admob-unified/references/api-reference.md +39 -16
- package/skills/ti-admob-unified/references/banners.md +9 -9
- package/skills/ti-admob-unified/references/contributing.md +14 -9
- package/skills/ti-admob-unified/references/dependencies.md +31 -25
- package/skills/ti-admob-unified/references/fullscreen.md +43 -28
- package/skills/ti-admob-unified/references/game-monetization.md +138 -7
- package/skills/ti-admob-unified/references/getting-started.md +75 -31
- package/skills/ti-admob-unified/references/index.md +5 -2
- package/skills/ti-admob-unified/references/integration.md +8 -6
- package/skills/ti-admob-unified/references/library.md +41 -10
- package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
- package/skills/ti-admob-unified/references/mediation.md +22 -9
- package/skills/ti-admob-unified/references/migration.md +11 -7
- package/skills/ti-admob-unified/references/module-changelog.md +41 -11
- package/skills/ti-admob-unified/references/privacy.md +35 -8
- package/skills/ti-admob-unified/references/production.md +67 -0
- package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
- package/skills/ti-admob-unified/references/roadmap.md +28 -31
- package/skills/ti-admob-unified/references/source-notes.md +51 -4
- package/skills/ti-admob-unified/references/source-snapshot.json +249 -45
- package/skills/ti-admob-unified/references/third-party-notices.md +4 -4
- package/skills/ti-game/SKILL.md +23 -6
- package/skills/ti-game/references/api.md +34 -30
- package/skills/ti-game/references/debugging-performance.md +18 -9
- package/skills/ti-game/references/examples.md +103 -20
- package/skills/ti-game/references/input.md +101 -0
- package/skills/ti-game/references/patterns.md +39 -15
- package/skills/ti-game/references/physics.md +80 -52
- package/skills/ti-game/references/project-setup.md +118 -56
- package/skills/ti-game/references/recipes-action.md +107 -8
- package/skills/ti-game/references/recipes-adventure-effects.md +41 -9
- package/skills/ti-game/references/roadmap.md +10 -3
- package/skills/ti-game/references/source-notes.md +81 -0
- package/skills/ti-game/references/tilemaps.md +12 -4
- package/skills/ti-synthengine/SKILL.md +81 -95
- package/skills/ti-synthengine/assets/example/README.md +84 -0
- package/skills/ti-synthengine/assets/example/app.js +1178 -0
- package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
- package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
- package/skills/ti-synthengine/references/advanced-examples.md +174 -0
- package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
- package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
- package/skills/ti-synthengine/references/api.md +434 -380
- package/skills/ti-synthengine/references/audio-engine.md +157 -230
- package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
- package/skills/ti-synthengine/references/examples.md +123 -253
- package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
- package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
- package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
- package/skills/ti-synthengine/references/guide.md +188 -156
- package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
- package/skills/ti-synthengine/references/performance-review.json +1146 -0
- package/skills/ti-synthengine/references/physical-validation.json +1457 -0
- package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
- package/skills/ti-synthengine/references/recipes.md +423 -599
- package/skills/ti-synthengine/references/recommendations.md +63 -113
- package/skills/ti-synthengine/references/sound-design.md +134 -258
- package/skills/ti-synthengine/references/source-notes.md +93 -105
- package/skills/ti-synthengine/references/source-snapshot.json +379 -32
- package/skills/ti-synthengine/references/stats-performance.md +64 -20
- package/skills/ti-synthengine/references/studio-validation.json +6202 -0
- package/skills/ti-synthengine/references/troubleshooting.md +109 -121
- package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
- package/skills/ti-synthengine/references/unified-validation.json +314 -0
- package/AGENTS-VERCEL-RESEARCH.md +0 -175
|
@@ -2,24 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
These recipes are tuned starting points for `ti.synthengine` 1.0.0. They use
|
|
4
4
|
only public option keys, keep every explicit envelope valid, and obtain
|
|
5
|
-
waveforms from the runtime contract instead of hardcoding integers.
|
|
5
|
+
waveforms from the runtime contract instead of hardcoding integers. Install the
|
|
6
|
+
matching official Android/iOS packages and follow the owning-engine
|
|
7
|
+
[installation and lifecycle guide](guide.md) before using them.
|
|
6
8
|
|
|
7
9
|
Unless a section says otherwise, the snippets assume one owner has already
|
|
8
10
|
started the engine:
|
|
9
11
|
|
|
10
12
|
```javascript
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const synth = require('ti.synthengine')
|
|
14
|
+
const contract = synth.getDefaults()
|
|
15
|
+
const WAVE = contract.waveTypes
|
|
14
16
|
```
|
|
15
17
|
|
|
16
18
|
Treat levels as starting points, not mastering targets. Test on the actual
|
|
17
19
|
phone speaker and leave headroom when release tails or independent events can
|
|
18
20
|
overlap.
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
Instrument interactions, layered chords and expressive synthesis are in
|
|
23
|
+
[the interaction recipes](recipes-interaction.md). Their standard synthesis
|
|
24
|
+
starting ranges require listening on the target device before shipping.
|
|
23
25
|
|
|
24
26
|
<!-- TOC-START -->
|
|
25
27
|
## Contents
|
|
@@ -43,7 +45,7 @@ and acceleration. The short release removes the edge without blurring the
|
|
|
43
45
|
motion.
|
|
44
46
|
|
|
45
47
|
```javascript
|
|
46
|
-
|
|
48
|
+
const accepted = synth.playTone({
|
|
47
49
|
frequency: 196,
|
|
48
50
|
frequencyEnd: 523,
|
|
49
51
|
duration: 350,
|
|
@@ -110,24 +112,55 @@ were chosen for it.
|
|
|
110
112
|
|
|
111
113
|
### Three-note bonus cue
|
|
112
114
|
|
|
113
|
-
Short ascending square notes read as collection or reward.
|
|
114
|
-
sets both its voice length and the time before the next step; it overrides
|
|
115
|
-
`stepDuration` and `noteValue`. Explicit rests separate 95 ms notes by 110 ms.
|
|
115
|
+
Short ascending square notes read as collection or reward. Native millisecond intervals preserve the 110 ms onset spacing; explicit `length` starts release early enough to preserve the original 95/95/130 ms total envelopes.
|
|
116
116
|
|
|
117
117
|
```javascript
|
|
118
|
-
synth.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
118
|
+
const sequenceId = synth.playSequence({
|
|
119
|
+
"timeUnit": "ms",
|
|
120
|
+
"loop": false,
|
|
121
|
+
"swing": 0,
|
|
122
|
+
"voice": {
|
|
123
|
+
"attack": 2,
|
|
124
|
+
"release": 30,
|
|
125
|
+
"volume": 0.55,
|
|
126
|
+
"waveType": WAVE.SQUARE
|
|
127
|
+
},
|
|
128
|
+
"steps": [
|
|
129
|
+
{
|
|
130
|
+
"frequencies": [
|
|
131
|
+
523.25
|
|
132
|
+
],
|
|
133
|
+
"duration": 95,
|
|
134
|
+
"length": 65,
|
|
135
|
+
"overrides": {}
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"rest": true,
|
|
139
|
+
"duration": 15
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"frequencies": [
|
|
143
|
+
587.33
|
|
144
|
+
],
|
|
145
|
+
"duration": 95,
|
|
146
|
+
"length": 65,
|
|
147
|
+
"overrides": {}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"rest": true,
|
|
151
|
+
"duration": 15
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"frequencies": [
|
|
155
|
+
659.25
|
|
156
|
+
],
|
|
157
|
+
"duration": 130,
|
|
158
|
+
"length": 100,
|
|
159
|
+
"overrides": {}
|
|
160
|
+
}
|
|
129
161
|
]
|
|
130
162
|
})
|
|
163
|
+
if (!sequenceId) throw new Error('Sequence rejected')
|
|
131
164
|
```
|
|
132
165
|
|
|
133
166
|
### Fast four-note arpeggio
|
|
@@ -137,21 +170,64 @@ edge while reducing uncontrolled clicks. The 10 ms rests keep the first three
|
|
|
137
170
|
40 ms notes on a 50 ms grid; the final note rings for 80 ms.
|
|
138
171
|
|
|
139
172
|
```javascript
|
|
140
|
-
synth.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
{
|
|
152
|
-
|
|
173
|
+
const sequenceId = synth.playSequence({
|
|
174
|
+
"timeUnit": "ms",
|
|
175
|
+
"loop": false,
|
|
176
|
+
"swing": 0,
|
|
177
|
+
"voice": {
|
|
178
|
+
"attack": 5,
|
|
179
|
+
"release": 15,
|
|
180
|
+
"volume": 0.5,
|
|
181
|
+
"waveType": WAVE.SQUARE
|
|
182
|
+
},
|
|
183
|
+
"steps": [
|
|
184
|
+
{
|
|
185
|
+
"frequencies": [
|
|
186
|
+
261.63
|
|
187
|
+
],
|
|
188
|
+
"duration": 40,
|
|
189
|
+
"length": 25,
|
|
190
|
+
"overrides": {}
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
"rest": true,
|
|
194
|
+
"duration": 10
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
"frequencies": [
|
|
198
|
+
329.63
|
|
199
|
+
],
|
|
200
|
+
"duration": 40,
|
|
201
|
+
"length": 25,
|
|
202
|
+
"overrides": {}
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"rest": true,
|
|
206
|
+
"duration": 10
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
"frequencies": [
|
|
210
|
+
392
|
|
211
|
+
],
|
|
212
|
+
"duration": 40,
|
|
213
|
+
"length": 25,
|
|
214
|
+
"overrides": {}
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
"rest": true,
|
|
218
|
+
"duration": 10
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"frequencies": [
|
|
222
|
+
523.25
|
|
223
|
+
],
|
|
224
|
+
"duration": 80,
|
|
225
|
+
"length": 65,
|
|
226
|
+
"overrides": {}
|
|
227
|
+
}
|
|
153
228
|
]
|
|
154
229
|
})
|
|
230
|
+
if (!sequenceId) throw new Error('Sequence rejected')
|
|
155
231
|
```
|
|
156
232
|
|
|
157
233
|
### Rapid score ticks
|
|
@@ -161,23 +237,168 @@ release values make every 60 ms tone valid without relying on shortened
|
|
|
161
237
|
defaults. A 10 ms rest after each tone places the ticks 70 ms apart.
|
|
162
238
|
|
|
163
239
|
```javascript
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
240
|
+
const sequenceId = synth.playSequence({
|
|
241
|
+
"timeUnit": "ms",
|
|
242
|
+
"loop": false,
|
|
243
|
+
"swing": 0,
|
|
244
|
+
"voice": {
|
|
245
|
+
"volume": 0.42,
|
|
246
|
+
"waveType": WAVE.SQUARE
|
|
247
|
+
},
|
|
248
|
+
"steps": [
|
|
249
|
+
{
|
|
250
|
+
"frequencies": [
|
|
251
|
+
1046.5
|
|
252
|
+
],
|
|
253
|
+
"duration": 60,
|
|
254
|
+
"length": 40,
|
|
255
|
+
"overrides": {
|
|
256
|
+
"attack": 2,
|
|
257
|
+
"release": 20
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
"rest": true,
|
|
262
|
+
"duration": 10
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"frequencies": [
|
|
266
|
+
1046.5
|
|
267
|
+
],
|
|
268
|
+
"duration": 60,
|
|
269
|
+
"length": 40,
|
|
270
|
+
"overrides": {
|
|
271
|
+
"attack": 2,
|
|
272
|
+
"release": 20
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
"rest": true,
|
|
277
|
+
"duration": 10
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"frequencies": [
|
|
281
|
+
1046.5
|
|
282
|
+
],
|
|
283
|
+
"duration": 60,
|
|
284
|
+
"length": 40,
|
|
285
|
+
"overrides": {
|
|
286
|
+
"attack": 2,
|
|
287
|
+
"release": 20
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
"rest": true,
|
|
292
|
+
"duration": 10
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"frequencies": [
|
|
296
|
+
1046.5
|
|
297
|
+
],
|
|
298
|
+
"duration": 60,
|
|
299
|
+
"length": 40,
|
|
300
|
+
"overrides": {
|
|
301
|
+
"attack": 2,
|
|
302
|
+
"release": 20
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
"rest": true,
|
|
307
|
+
"duration": 10
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
"frequencies": [
|
|
311
|
+
1046.5
|
|
312
|
+
],
|
|
313
|
+
"duration": 60,
|
|
314
|
+
"length": 40,
|
|
315
|
+
"overrides": {
|
|
316
|
+
"attack": 2,
|
|
317
|
+
"release": 20
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"rest": true,
|
|
322
|
+
"duration": 10
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
"frequencies": [
|
|
326
|
+
1046.5
|
|
327
|
+
],
|
|
328
|
+
"duration": 60,
|
|
329
|
+
"length": 40,
|
|
330
|
+
"overrides": {
|
|
331
|
+
"attack": 2,
|
|
332
|
+
"release": 20
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"rest": true,
|
|
337
|
+
"duration": 10
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"frequencies": [
|
|
341
|
+
1046.5
|
|
342
|
+
],
|
|
343
|
+
"duration": 60,
|
|
344
|
+
"length": 40,
|
|
345
|
+
"overrides": {
|
|
346
|
+
"attack": 2,
|
|
347
|
+
"release": 20
|
|
348
|
+
}
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"rest": true,
|
|
352
|
+
"duration": 10
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
"frequencies": [
|
|
356
|
+
1046.5
|
|
357
|
+
],
|
|
358
|
+
"duration": 60,
|
|
359
|
+
"length": 40,
|
|
360
|
+
"overrides": {
|
|
361
|
+
"attack": 2,
|
|
362
|
+
"release": 20
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"rest": true,
|
|
367
|
+
"duration": 10
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
"frequencies": [
|
|
371
|
+
1046.5
|
|
372
|
+
],
|
|
373
|
+
"duration": 60,
|
|
374
|
+
"length": 40,
|
|
375
|
+
"overrides": {
|
|
376
|
+
"attack": 2,
|
|
377
|
+
"release": 20
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
"rest": true,
|
|
382
|
+
"duration": 10
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"frequencies": [
|
|
386
|
+
1046.5
|
|
387
|
+
],
|
|
388
|
+
"duration": 60,
|
|
389
|
+
"length": 40,
|
|
390
|
+
"overrides": {
|
|
391
|
+
"attack": 2,
|
|
392
|
+
"release": 20
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
"rest": true,
|
|
397
|
+
"duration": 10
|
|
398
|
+
}
|
|
399
|
+
]
|
|
180
400
|
})
|
|
401
|
+
if (!sequenceId) throw new Error('Sequence rejected')
|
|
181
402
|
```
|
|
182
403
|
|
|
183
404
|
### Hovering craft
|
|
@@ -208,22 +429,140 @@ speaker. Each 120 ms tone has a fitted envelope (`10 + 100 <= 120`), followed
|
|
|
208
429
|
by a 10 ms rest, so the pitch changes every 130 ms.
|
|
209
430
|
|
|
210
431
|
```javascript
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
432
|
+
const sequenceId = synth.playSequence({
|
|
433
|
+
"timeUnit": "ms",
|
|
434
|
+
"loop": false,
|
|
435
|
+
"swing": 0,
|
|
436
|
+
"voice": {
|
|
437
|
+
"attack": 10,
|
|
438
|
+
"release": 100,
|
|
439
|
+
"volume": 0.45,
|
|
440
|
+
"waveType": WAVE.SQUARE
|
|
441
|
+
},
|
|
442
|
+
"steps": [
|
|
443
|
+
{
|
|
444
|
+
"frequencies": [
|
|
445
|
+
800
|
|
446
|
+
],
|
|
447
|
+
"duration": 120,
|
|
448
|
+
"length": 20,
|
|
449
|
+
"overrides": {}
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
"rest": true,
|
|
453
|
+
"duration": 10
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"frequencies": [
|
|
457
|
+
400
|
|
458
|
+
],
|
|
459
|
+
"duration": 120,
|
|
460
|
+
"length": 20,
|
|
461
|
+
"overrides": {}
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
"rest": true,
|
|
465
|
+
"duration": 10
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
"frequencies": [
|
|
469
|
+
800
|
|
470
|
+
],
|
|
471
|
+
"duration": 120,
|
|
472
|
+
"length": 20,
|
|
473
|
+
"overrides": {}
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
"rest": true,
|
|
477
|
+
"duration": 10
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
"frequencies": [
|
|
481
|
+
400
|
|
482
|
+
],
|
|
483
|
+
"duration": 120,
|
|
484
|
+
"length": 20,
|
|
485
|
+
"overrides": {}
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
"rest": true,
|
|
489
|
+
"duration": 10
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
"frequencies": [
|
|
493
|
+
800
|
|
494
|
+
],
|
|
495
|
+
"duration": 120,
|
|
496
|
+
"length": 20,
|
|
497
|
+
"overrides": {}
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
"rest": true,
|
|
501
|
+
"duration": 10
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
"frequencies": [
|
|
505
|
+
400
|
|
506
|
+
],
|
|
507
|
+
"duration": 120,
|
|
508
|
+
"length": 20,
|
|
509
|
+
"overrides": {}
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
"rest": true,
|
|
513
|
+
"duration": 10
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
"frequencies": [
|
|
517
|
+
800
|
|
518
|
+
],
|
|
519
|
+
"duration": 120,
|
|
520
|
+
"length": 20,
|
|
521
|
+
"overrides": {}
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"rest": true,
|
|
525
|
+
"duration": 10
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
"frequencies": [
|
|
529
|
+
400
|
|
530
|
+
],
|
|
531
|
+
"duration": 120,
|
|
532
|
+
"length": 20,
|
|
533
|
+
"overrides": {}
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
"rest": true,
|
|
537
|
+
"duration": 10
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
"frequencies": [
|
|
541
|
+
800
|
|
542
|
+
],
|
|
543
|
+
"duration": 120,
|
|
544
|
+
"length": 20,
|
|
545
|
+
"overrides": {}
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
"rest": true,
|
|
549
|
+
"duration": 10
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
"frequencies": [
|
|
553
|
+
400
|
|
554
|
+
],
|
|
555
|
+
"duration": 120,
|
|
556
|
+
"length": 20,
|
|
557
|
+
"overrides": {}
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"rest": true,
|
|
561
|
+
"duration": 10
|
|
562
|
+
}
|
|
563
|
+
]
|
|
226
564
|
})
|
|
565
|
+
if (!sequenceId) throw new Error('Sequence rejected')
|
|
227
566
|
```
|
|
228
567
|
|
|
229
568
|
### Low heartbeat pair
|
|
@@ -234,567 +573,52 @@ phone speaker, try 180 and 160 Hz while preserving the rhythm.
|
|
|
234
573
|
|
|
235
574
|
```javascript
|
|
236
575
|
function playHeartbeatPair () {
|
|
237
|
-
synth.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
waveType: WAVE.TRIANGLE
|
|
576
|
+
return synth.playSequence({ timeUnit: 'ms', loop: false,
|
|
577
|
+
voice: {waveType: WAVE.TRIANGLE, attack: 10, release: 120, volume: 0.55},
|
|
578
|
+
steps: [
|
|
579
|
+
{frequencies: [90], duration: 220, length: 30},
|
|
580
|
+
{frequencies: [80], duration: 150, length: 30, overrides: {volume: 0.48}}
|
|
581
|
+
]
|
|
244
582
|
})
|
|
245
|
-
|
|
246
|
-
setTimeout(function () {
|
|
247
|
-
synth.playTone({
|
|
248
|
-
frequency: 80,
|
|
249
|
-
duration: 150,
|
|
250
|
-
attack: 10,
|
|
251
|
-
release: 120,
|
|
252
|
-
volume: 0.48,
|
|
253
|
-
waveType: WAVE.TRIANGLE
|
|
254
|
-
})
|
|
255
|
-
}, 220)
|
|
256
583
|
}
|
|
257
584
|
```
|
|
258
585
|
|
|
259
|
-
|
|
260
|
-
[Cancellable repetition](#cancellable-repetition).
|
|
586
|
+
The native clock owns both attacks. Keep the returned ID to stop only this cue.
|
|
261
587
|
|
|
262
588
|
## Playable xylophone
|
|
263
589
|
|
|
264
|
-
|
|
265
|
-
harmonics. A 2 ms attack makes the strike immediate, while a 180 ms release
|
|
266
|
-
lets adjacent bars overlap musically.
|
|
267
|
-
|
|
268
|
-
```javascript
|
|
269
|
-
var XYLOPHONE = [
|
|
270
|
-
{ label: 'C5', frequency: 523.25 },
|
|
271
|
-
{ label: 'D5', frequency: 587.33 },
|
|
272
|
-
{ label: 'E5', frequency: 659.25 },
|
|
273
|
-
{ label: 'F5', frequency: 698.46 },
|
|
274
|
-
{ label: 'G5', frequency: 783.99 },
|
|
275
|
-
{ label: 'A5', frequency: 880 },
|
|
276
|
-
{ label: 'B5', frequency: 987.77 },
|
|
277
|
-
{ label: 'C6', frequency: 1046.5 }
|
|
278
|
-
]
|
|
279
|
-
|
|
280
|
-
function playXylophoneBar (index, pan) {
|
|
281
|
-
var bar = XYLOPHONE[index]
|
|
282
|
-
if (!bar || !isFinite(pan) || pan < -1 || pan > 1) return false
|
|
283
|
-
|
|
284
|
-
return synth.playTone({
|
|
285
|
-
frequency: bar.frequency,
|
|
286
|
-
duration: 420,
|
|
287
|
-
attack: 2,
|
|
288
|
-
release: 180,
|
|
289
|
-
volume: 0.65,
|
|
290
|
-
waveType: WAVE.TRIANGLE,
|
|
291
|
-
pan: pan
|
|
292
|
-
})
|
|
293
|
-
}
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
Map the bars across the stereo field with
|
|
297
|
-
`pan = -0.8 + (1.6 * index / (barCount - 1))`. Do not make pan carry essential
|
|
298
|
-
meaning because the device may have a mono output.
|
|
299
|
-
|
|
300
|
-
### Drag interaction without note storms
|
|
301
|
-
|
|
302
|
-
A `touchmove` event may fire many times while the finger remains on one bar.
|
|
303
|
-
Track the last entered bar and strike only when the index changes:
|
|
304
|
-
|
|
305
|
-
```javascript
|
|
306
|
-
function createBarGesture (playIndex) {
|
|
307
|
-
var tracking = false
|
|
308
|
-
var activeIndex = -1
|
|
309
|
-
|
|
310
|
-
function enter (index) {
|
|
311
|
-
if (!tracking || index < 0 || index === activeIndex) return
|
|
312
|
-
activeIndex = index
|
|
313
|
-
playIndex(index)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return {
|
|
317
|
-
start: function (index) {
|
|
318
|
-
tracking = true
|
|
319
|
-
activeIndex = -1
|
|
320
|
-
enter(index)
|
|
321
|
-
},
|
|
322
|
-
move: enter,
|
|
323
|
-
finish: function () {
|
|
324
|
-
tracking = false
|
|
325
|
-
activeIndex = -1
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
```
|
|
330
|
-
|
|
331
|
-
In a Titanium view:
|
|
332
|
-
|
|
333
|
-
- compute each bar's hit rectangle after layout with `convertPointToView()`;
|
|
334
|
-
- convert move coordinates into one shared container before hit testing;
|
|
335
|
-
- call `gesture.start(index)` on `touchstart` and `gesture.move(index)` only
|
|
336
|
-
after resolving a valid bar;
|
|
337
|
-
- call `gesture.finish()` on both `touchend` and `touchcancel`;
|
|
338
|
-
- keep visual pressed-state counts separate if multiple touches are supported.
|
|
339
|
-
|
|
340
|
-
The audio release may continue after the finger leaves a bar. That is desired
|
|
341
|
-
for a xylophone; call `stopAll()` only for an explicit all-notes-off action.
|
|
590
|
+
Continue in [the interaction recipes](recipes-interaction.md#playable-xylophone).
|
|
342
591
|
|
|
343
592
|
## Four-pad memory cues
|
|
344
593
|
|
|
345
|
-
|
|
346
|
-
durations. Keep each bank as app metadata, then build a fresh closed options
|
|
347
|
-
object for the module.
|
|
348
|
-
|
|
349
|
-
```javascript
|
|
350
|
-
var PAD_BANKS = {
|
|
351
|
-
low: [415, 310, 252, 209],
|
|
352
|
-
equalTempered: [391.995, 329.628, 261.626, 195.998]
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function playMemoryPad (bankName, index) {
|
|
356
|
-
var bank = PAD_BANKS[bankName]
|
|
357
|
-
var frequency = bank && bank[index]
|
|
358
|
-
if (!isFinite(frequency)) return false
|
|
359
|
-
|
|
360
|
-
return synth.playTone({
|
|
361
|
-
frequency: frequency,
|
|
362
|
-
duration: 420,
|
|
363
|
-
attack: 6,
|
|
364
|
-
release: 100,
|
|
365
|
-
volume: 0.62,
|
|
366
|
-
waveType: WAVE.SQUARE
|
|
367
|
-
})
|
|
368
|
-
}
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
To demonstrate the whole pitch bank as one event, use a chord rather than four
|
|
372
|
-
independent full-gain calls:
|
|
373
|
-
|
|
374
|
-
```javascript
|
|
375
|
-
function playMemoryBank (bankName) {
|
|
376
|
-
var bank = PAD_BANKS[bankName]
|
|
377
|
-
if (!bank) return false
|
|
378
|
-
|
|
379
|
-
return synth.playChord({
|
|
380
|
-
frequencies: bank.slice(),
|
|
381
|
-
duration: 480,
|
|
382
|
-
attack: 8,
|
|
383
|
-
release: 120,
|
|
384
|
-
volume: 0.56,
|
|
385
|
-
waveType: WAVE.SQUARE,
|
|
386
|
-
pans: [-0.75, -0.25, 0.25, 0.75]
|
|
387
|
-
})
|
|
388
|
-
}
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
### Controlled timbre comparison
|
|
392
|
-
|
|
393
|
-
For an honest A/B test, hold pitches, duration, envelope and pan constant.
|
|
394
|
-
Change only waveform and its comparison level. Never pass a profile object
|
|
395
|
-
containing labels or other app metadata directly to `playChord()` because its
|
|
396
|
-
options dictionary is closed.
|
|
397
|
-
|
|
398
|
-
```javascript
|
|
399
|
-
var TIMBRE_PROFILES = {
|
|
400
|
-
square: { waveType: WAVE.SQUARE, volume: 0.52 },
|
|
401
|
-
sine: { waveType: WAVE.SINE, volume: 0.68 },
|
|
402
|
-
triangle: { waveType: WAVE.TRIANGLE, volume: 0.64 },
|
|
403
|
-
sawtooth: { waveType: WAVE.SAWTOOTH, volume: 0.46 }
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
function auditionTimbre (profileName) {
|
|
407
|
-
var profile = TIMBRE_PROFILES[profileName]
|
|
408
|
-
if (!profile) return false
|
|
409
|
-
|
|
410
|
-
return synth.playChord({
|
|
411
|
-
frequencies: PAD_BANKS.low.slice(),
|
|
412
|
-
duration: 420,
|
|
413
|
-
attack: 8,
|
|
414
|
-
release: 100,
|
|
415
|
-
volume: profile.volume,
|
|
416
|
-
waveType: profile.waveType,
|
|
417
|
-
pans: [-0.75, -0.25, 0.25, 0.75]
|
|
418
|
-
})
|
|
419
|
-
}
|
|
420
|
-
```
|
|
594
|
+
Continue in [the interaction recipes](recipes-interaction.md#four-pad-memory-cues).
|
|
421
595
|
|
|
422
596
|
## Polyphony and layered chords
|
|
423
597
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
The standard A4 = 440 Hz formula makes it easy to build interval recipes
|
|
427
|
-
without embedding a long frequency table.
|
|
428
|
-
|
|
429
|
-
```javascript
|
|
430
|
-
function midiToHz (midi) {
|
|
431
|
-
return 440 * Math.pow(2, (midi - 69) / 12)
|
|
432
|
-
}
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
Detuning needs no conversion: `detune` on a tone or pattern and `detunes` on a
|
|
436
|
-
chord take cents, one hundredth of a semitone.
|
|
437
|
-
|
|
438
|
-
### Build a wide layered chord
|
|
439
|
-
|
|
440
|
-
The interval cycle `[0, 4, 7, ...]` stacks major-triad chord tones across
|
|
441
|
-
octaves. Later layers repeat those tones with small cent offsets, passed as
|
|
442
|
-
`detunes`, which creates width and beating.
|
|
443
|
-
|
|
444
|
-
```javascript
|
|
445
|
-
var CHORD_INTERVALS = [0, 4, 7, 12, 16, 19, 24, 28, 31, 36]
|
|
446
|
-
var LAYER_DETUNE_CENTS = [0, -8, 8, -12]
|
|
447
|
-
|
|
448
|
-
function normalizeVoiceCount (value) {
|
|
449
|
-
var count = Math.round(Number(value))
|
|
450
|
-
if (!isFinite(count)) return 1
|
|
451
|
-
return Math.max(1, Math.min(32, count))
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function buildLayeredFrequencies (voiceCount) {
|
|
455
|
-
var count = normalizeVoiceCount(voiceCount)
|
|
456
|
-
var frequencies = []
|
|
457
|
-
|
|
458
|
-
for (var index = 0; index < count; index++) {
|
|
459
|
-
var intervalIndex = index % CHORD_INTERVALS.length
|
|
460
|
-
frequencies.push(midiToHz(60.12 + CHORD_INTERVALS[intervalIndex]))
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return frequencies
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
function buildLayerDetunes (voiceCount) {
|
|
467
|
-
var count = normalizeVoiceCount(voiceCount)
|
|
468
|
-
var detunes = []
|
|
469
|
-
|
|
470
|
-
for (var index = 0; index < count; index++) {
|
|
471
|
-
var layer = Math.floor(index / CHORD_INTERVALS.length)
|
|
472
|
-
detunes.push(LAYER_DETUNE_CENTS[layer % LAYER_DETUNE_CENTS.length])
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return detunes
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function buildSymmetricPans (voiceCount, spread) {
|
|
479
|
-
var count = normalizeVoiceCount(voiceCount)
|
|
480
|
-
var numericSpread = Number(spread)
|
|
481
|
-
var safeSpread = isFinite(numericSpread)
|
|
482
|
-
? Math.max(0, Math.min(1, numericSpread))
|
|
483
|
-
: 0
|
|
484
|
-
var pans = []
|
|
485
|
-
|
|
486
|
-
if (count === 1) return [0]
|
|
487
|
-
|
|
488
|
-
for (var index = 0; index < count; index++) {
|
|
489
|
-
pans.push(-safeSpread + (2 * safeSpread * index / (count - 1)))
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return pans
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function playLayeredChord (voiceCount) {
|
|
496
|
-
var count = normalizeVoiceCount(voiceCount)
|
|
497
|
-
|
|
498
|
-
return synth.playChord({
|
|
499
|
-
frequencies: buildLayeredFrequencies(count),
|
|
500
|
-
detunes: buildLayerDetunes(count),
|
|
501
|
-
duration: 800,
|
|
502
|
-
attack: 35,
|
|
503
|
-
release: 220,
|
|
504
|
-
volume: 0.48,
|
|
505
|
-
waveType: WAVE.SINE,
|
|
506
|
-
pans: buildSymmetricPans(count, 0.8)
|
|
507
|
-
})
|
|
508
|
-
}
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
The engine must have `maxVoices` at least as large as the intended simultaneous
|
|
512
|
-
chord. Four to twelve voices are a practical musical starting point. Counts
|
|
513
|
-
near 32 are useful for deliberate capacity tests, not as a default.
|
|
514
|
-
|
|
515
|
-
### Clean reference versus overlap stress
|
|
516
|
-
|
|
517
|
-
A single sustained chord is the clean reference for judging waveform quality.
|
|
518
|
-
If a new 220 ms chord starts every 120 ms, release tails overlap and the summed
|
|
519
|
-
level rises and falls. Limiter pumping is expected in that stress condition;
|
|
520
|
-
it is not proof that a single chord is defective.
|
|
521
|
-
|
|
522
|
-
For a musical pulse, keep the event shorter than its period:
|
|
523
|
-
|
|
524
|
-
```javascript
|
|
525
|
-
var pulseTimer = null
|
|
526
|
-
var pulseGeneration = 0
|
|
527
|
-
|
|
528
|
-
function stopChordPulse () {
|
|
529
|
-
pulseGeneration++
|
|
530
|
-
if (pulseTimer !== null) {
|
|
531
|
-
clearTimeout(pulseTimer)
|
|
532
|
-
pulseTimer = null
|
|
533
|
-
}
|
|
534
|
-
synth.stopAll()
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function startChordPulse () {
|
|
538
|
-
stopChordPulse()
|
|
539
|
-
var generation = pulseGeneration
|
|
540
|
-
|
|
541
|
-
function tick () {
|
|
542
|
-
if (generation !== pulseGeneration) return
|
|
543
|
-
|
|
544
|
-
synth.playChord({
|
|
545
|
-
frequencies: buildLayeredFrequencies(4),
|
|
546
|
-
duration: 180,
|
|
547
|
-
attack: 10,
|
|
548
|
-
release: 60,
|
|
549
|
-
volume: 0.42,
|
|
550
|
-
waveType: WAVE.SINE,
|
|
551
|
-
pans: buildSymmetricPans(4, 0.7)
|
|
552
|
-
})
|
|
553
|
-
|
|
554
|
-
pulseTimer = setTimeout(tick, 260)
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
tick()
|
|
558
|
-
}
|
|
559
|
-
```
|
|
560
|
-
|
|
561
|
-
### Cancellable repetition
|
|
562
|
-
|
|
563
|
-
`stopAll()` silences native voices, but it cannot cancel JavaScript timers that
|
|
564
|
-
will launch new calls later. Every repeating UI or game effect therefore needs
|
|
565
|
-
an owner-controlled cancellation mechanism. The generation token above guards
|
|
566
|
-
against a callback that was already queued when `clearTimeout()` ran.
|
|
567
|
-
|
|
568
|
-
When the owning window closes:
|
|
569
|
-
|
|
570
|
-
```javascript
|
|
571
|
-
function cleanupAudio () {
|
|
572
|
-
stopChordPulse()
|
|
573
|
-
synth.shutdown()
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
win.addEventListener('close', cleanupAudio)
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
Do not call `shutdown()` from a child control if another visible screen owns
|
|
580
|
-
the shared engine.
|
|
598
|
+
Continue in [the interaction recipes](recipes-interaction.md#polyphony-and-layered-chords).
|
|
581
599
|
|
|
582
600
|
## Expressive envelopes, filters and FM
|
|
583
601
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
A short attack, a decay that falls to a low sustain and a release that fits
|
|
587
|
-
give the struck-and-ring shape of a mallet or plucked string. A sustain below 1
|
|
588
|
-
needs a decay, and `attack + decay` must fit inside `duration`.
|
|
589
|
-
|
|
590
|
-
```javascript
|
|
591
|
-
synth.playTone({
|
|
592
|
-
note: 'C5',
|
|
593
|
-
duration: 700,
|
|
594
|
-
attack: 2,
|
|
595
|
-
decay: 260,
|
|
596
|
-
sustain: 0.12,
|
|
597
|
-
release: 240,
|
|
598
|
-
waveType: WAVE.TRIANGLE,
|
|
599
|
-
volume: 0.62
|
|
600
|
-
})
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
### Kick drum
|
|
604
|
-
|
|
605
|
-
A sine that falls in pitch while its level decays reads as a drum. The pitch
|
|
606
|
-
sweep is linear over the whole event.
|
|
607
|
-
|
|
608
|
-
```javascript
|
|
609
|
-
synth.playTone({
|
|
610
|
-
frequency: 150,
|
|
611
|
-
frequencyEnd: 45,
|
|
612
|
-
duration: 320,
|
|
613
|
-
attack: 1,
|
|
614
|
-
decay: 140,
|
|
615
|
-
sustain: 0.35,
|
|
616
|
-
release: 140,
|
|
617
|
-
volume: 0.9
|
|
618
|
-
})
|
|
619
|
-
```
|
|
620
|
-
|
|
621
|
-
### Closed hi-hat
|
|
622
|
-
|
|
623
|
-
White noise through a high-pass filter, with a decay that drops to a quiet
|
|
624
|
-
tail. The filter fields only work together with a `filterType`.
|
|
625
|
-
|
|
626
|
-
```javascript
|
|
627
|
-
synth.playTone({
|
|
628
|
-
waveType: WAVE.NOISE,
|
|
629
|
-
duration: 90,
|
|
630
|
-
attack: 1,
|
|
631
|
-
decay: 20,
|
|
632
|
-
sustain: 0.3,
|
|
633
|
-
release: 60,
|
|
634
|
-
filterType: 'highpass',
|
|
635
|
-
filterHz: 6500,
|
|
636
|
-
volume: 0.45
|
|
637
|
-
})
|
|
638
|
-
```
|
|
639
|
-
|
|
640
|
-
### Electric piano
|
|
641
|
-
|
|
642
|
-
Frequency modulation with a 1:1 ratio gives a tine-like tone. Letting the
|
|
643
|
-
modulation index fall through `fmDepthEnd` makes the note mellow as it rings,
|
|
644
|
-
which is what the ear expects from a struck tine. FM needs a sine carrier.
|
|
645
|
-
|
|
646
|
-
```javascript
|
|
647
|
-
synth.playTone({
|
|
648
|
-
note: 'E3',
|
|
649
|
-
duration: 1400,
|
|
650
|
-
attack: 3,
|
|
651
|
-
decay: 700,
|
|
652
|
-
sustain: 0.25,
|
|
653
|
-
release: 500,
|
|
654
|
-
fmRatio: 1,
|
|
655
|
-
fmDepth: 2.2,
|
|
656
|
-
fmDepthEnd: 0.3,
|
|
657
|
-
volume: 0.55
|
|
658
|
-
})
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
### Bell
|
|
662
|
-
|
|
663
|
-
A non-integer ratio produces an inharmonic spectrum, and an index that decays
|
|
664
|
-
to nothing leaves a pure ring. `fmDepthEnd: 0` is a real target, so it is
|
|
665
|
-
accepted; omit the key when no sweep is wanted.
|
|
666
|
-
|
|
667
|
-
```javascript
|
|
668
|
-
synth.playTone({
|
|
669
|
-
note: 'A5',
|
|
670
|
-
duration: 2400,
|
|
671
|
-
attack: 1,
|
|
672
|
-
decay: 1800,
|
|
673
|
-
sustain: 0,
|
|
674
|
-
release: 500,
|
|
675
|
-
fmRatio: 3.5,
|
|
676
|
-
fmDepth: 4,
|
|
677
|
-
fmDepthEnd: 0,
|
|
678
|
-
volume: 0.5
|
|
679
|
-
})
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
### Wah sweep
|
|
683
|
-
|
|
684
|
-
A resonant band-pass opening on a sawtooth. The cutoff moves exponentially, so
|
|
685
|
-
equal distances in pitch take equal time. Keep `filterQ` moderate: high values
|
|
686
|
-
make the resonance peak loud enough to engage the limiter.
|
|
687
|
-
|
|
688
|
-
```javascript
|
|
689
|
-
synth.playTone({
|
|
690
|
-
note: 'A2',
|
|
691
|
-
duration: 900,
|
|
692
|
-
attack: 20,
|
|
693
|
-
release: 200,
|
|
694
|
-
waveType: WAVE.SAWTOOTH,
|
|
695
|
-
filterType: 'bandpass',
|
|
696
|
-
filterHz: 350,
|
|
697
|
-
filterHzEnd: 2200,
|
|
698
|
-
filterQ: 4,
|
|
699
|
-
volume: 0.7
|
|
700
|
-
})
|
|
701
|
-
```
|
|
702
|
-
|
|
703
|
-
### Thick unison pad
|
|
704
|
-
|
|
705
|
-
Repeat a pitch and spread the copies a few cents apart. The slow beating
|
|
706
|
-
between them is what makes the sound wide. `detunes` is aligned with the
|
|
707
|
-
pitches and can be shorter than the array.
|
|
708
|
-
|
|
709
|
-
```javascript
|
|
710
|
-
synth.playChord({
|
|
711
|
-
notes: ['A3', 'A3', 'A3', 'E4', 'E4', 'E4'],
|
|
712
|
-
detunes: [-9, 0, 9, -7, 0, 7],
|
|
713
|
-
pans: [-0.6, 0, 0.6, -0.4, 0, 0.4],
|
|
714
|
-
duration: 2400,
|
|
715
|
-
attack: 350,
|
|
716
|
-
release: 900,
|
|
717
|
-
waveType: WAVE.SAWTOOTH,
|
|
718
|
-
volume: 0.45
|
|
719
|
-
})
|
|
720
|
-
```
|
|
721
|
-
|
|
722
|
-
### Chaining after a pattern
|
|
723
|
-
|
|
724
|
-
`patternend` fires once for every accepted pattern. `completed` is true when it
|
|
725
|
-
ran to its end and false when a newer pattern replaced it, so only the first
|
|
726
|
-
case should start the next cue. `stopAll()` and `shutdown()` fire nothing.
|
|
727
|
-
|
|
728
|
-
```javascript
|
|
729
|
-
function onPatternEnd (event) {
|
|
730
|
-
if (event.completed) playOutroCue()
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
synth.addEventListener('patternend', onPatternEnd)
|
|
734
|
-
|
|
735
|
-
synth.playPattern({
|
|
736
|
-
noteValue: '8n',
|
|
737
|
-
waveType: WAVE.SQUARE,
|
|
738
|
-
volume: 0.5,
|
|
739
|
-
steps: [{ note: 'C5' }, { note: 'E5' }, { note: 'G5' }, { note: 'C6' }]
|
|
740
|
-
})
|
|
741
|
-
|
|
742
|
-
function cleanupPatternEvents () {
|
|
743
|
-
synth.removeEventListener('patternend', onPatternEnd)
|
|
744
|
-
}
|
|
745
|
-
```
|
|
602
|
+
Continue in [the interaction recipes](recipes-interaction.md#expressive-envelopes-filters-and-fm).
|
|
746
603
|
|
|
747
604
|
## Preset architecture for strict contracts
|
|
748
605
|
|
|
749
|
-
|
|
750
|
-
closed object at the call boundary so keys such as `label`, `category`, or
|
|
751
|
-
`description` never reach the module.
|
|
752
|
-
|
|
753
|
-
```javascript
|
|
754
|
-
var PRESETS = {
|
|
755
|
-
success: {
|
|
756
|
-
label: 'Success',
|
|
757
|
-
frequency: 760,
|
|
758
|
-
frequencyEnd: 1040,
|
|
759
|
-
waveType: WAVE.TRIANGLE
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function playPreset (name) {
|
|
764
|
-
var preset = PRESETS[name]
|
|
765
|
-
if (!preset) return false
|
|
766
|
-
|
|
767
|
-
return synth.playTone({
|
|
768
|
-
frequency: preset.frequency,
|
|
769
|
-
frequencyEnd: preset.frequencyEnd,
|
|
770
|
-
duration: 150,
|
|
771
|
-
attack: 3,
|
|
772
|
-
release: 90,
|
|
773
|
-
volume: 0.58,
|
|
774
|
-
waveType: preset.waveType
|
|
775
|
-
})
|
|
776
|
-
}
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
Avoid `synth.playTone(PRESETS[name])`: the preset contains `label`, which makes
|
|
780
|
-
the native call reject the entire dictionary.
|
|
606
|
+
Continue in [the interaction recipes](recipes-interaction.md#preset-architecture-for-strict-contracts).
|
|
781
607
|
|
|
782
608
|
## Recipe verification checklist
|
|
783
609
|
|
|
784
610
|
Before returning or shipping a variation:
|
|
785
611
|
|
|
786
|
-
- use
|
|
787
|
-
chord;
|
|
788
|
-
-
|
|
789
|
-
-
|
|
790
|
-
|
|
791
|
-
- verify `attack < duration` and `release <= duration - attack` for every
|
|
792
|
-
explicit event or step, and `attack + decay <= duration` when a decay is set;
|
|
612
|
+
- use at most one pitch source for a tone (omission uses the documented default)
|
|
613
|
+
and exactly one pitch array for a chord;
|
|
614
|
+
- use exactly one MIDI `notes` array, `frequencies` array or `rest: true` per native sequence step, including noise;
|
|
615
|
+
- keep `duration` as the onset interval and `length` as the release onset; both use the sequence time unit;
|
|
616
|
+
- verify `attack < duration`, `release <= duration - attack` and `attack + decay <= duration` for timed tones/chords; sequence ADSR remains in milliseconds and has no interval budget;
|
|
793
617
|
- give `sustain` below 1 a `decay`, give filter fields a `filterType`, and give
|
|
794
618
|
FM a sine carrier and a depth;
|
|
795
619
|
- keep all numbers finite and integer-only fields integral;
|
|
796
620
|
- use `WAVE` constants, not copied numeric waveform values;
|
|
797
|
-
- check Boolean
|
|
621
|
+
- check Boolean acceptance and positive sequence IDs at important control boundaries;
|
|
798
622
|
- size `maxVoices` for the largest real overlap, including release tails;
|
|
799
623
|
- cancel app-owned timers before `stopAll()` or `shutdown()`;
|
|
800
624
|
- audition on the intended phone speaker as well as headphones.
|