@maccesar/titools 5.2.0 → 5.2.1

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.
Files changed (78) hide show
  1. package/CONTRIBUTING.md +69 -0
  2. package/README.md +80 -72
  3. package/agents/ti-pro.md +1 -1
  4. package/commands/ti-audit.md +2 -2
  5. package/docs/examples.md +91 -0
  6. package/docs/knowledge-index.md +57 -0
  7. package/lib/downloader.js +1 -1
  8. package/package.json +4 -2
  9. package/skills/purgetss/references/multi-density-images.md +1 -1
  10. package/skills/ti-admob-unified/SKILL.md +21 -7
  11. package/skills/ti-admob-unified/assets/example/app.js +229 -25
  12. package/skills/ti-admob-unified/assets/example/lib/ads.js +30 -2
  13. package/skills/ti-admob-unified/assets/example/library-usage.js +33 -1
  14. package/skills/ti-admob-unified/references/api-reference.md +39 -16
  15. package/skills/ti-admob-unified/references/banners.md +9 -9
  16. package/skills/ti-admob-unified/references/contributing.md +14 -9
  17. package/skills/ti-admob-unified/references/dependencies.md +31 -25
  18. package/skills/ti-admob-unified/references/fullscreen.md +43 -28
  19. package/skills/ti-admob-unified/references/game-monetization.md +138 -7
  20. package/skills/ti-admob-unified/references/getting-started.md +75 -31
  21. package/skills/ti-admob-unified/references/index.md +5 -2
  22. package/skills/ti-admob-unified/references/integration.md +8 -6
  23. package/skills/ti-admob-unified/references/library.md +41 -10
  24. package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
  25. package/skills/ti-admob-unified/references/mediation.md +22 -9
  26. package/skills/ti-admob-unified/references/migration.md +11 -7
  27. package/skills/ti-admob-unified/references/module-changelog.md +41 -11
  28. package/skills/ti-admob-unified/references/privacy.md +35 -8
  29. package/skills/ti-admob-unified/references/production.md +67 -0
  30. package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
  31. package/skills/ti-admob-unified/references/roadmap.md +28 -31
  32. package/skills/ti-admob-unified/references/source-notes.md +51 -4
  33. package/skills/ti-admob-unified/references/source-snapshot.json +249 -45
  34. package/skills/ti-admob-unified/references/third-party-notices.md +4 -4
  35. package/skills/ti-game/SKILL.md +23 -6
  36. package/skills/ti-game/references/api.md +34 -30
  37. package/skills/ti-game/references/debugging-performance.md +18 -9
  38. package/skills/ti-game/references/examples.md +103 -20
  39. package/skills/ti-game/references/input.md +101 -0
  40. package/skills/ti-game/references/patterns.md +39 -15
  41. package/skills/ti-game/references/physics.md +80 -52
  42. package/skills/ti-game/references/project-setup.md +118 -56
  43. package/skills/ti-game/references/recipes-action.md +107 -8
  44. package/skills/ti-game/references/recipes-adventure-effects.md +41 -9
  45. package/skills/ti-game/references/roadmap.md +10 -3
  46. package/skills/ti-game/references/source-notes.md +81 -0
  47. package/skills/ti-game/references/tilemaps.md +12 -4
  48. package/skills/ti-synthengine/SKILL.md +81 -95
  49. package/skills/ti-synthengine/assets/example/README.md +84 -0
  50. package/skills/ti-synthengine/assets/example/app.js +1178 -0
  51. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
  52. package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
  53. package/skills/ti-synthengine/references/advanced-examples.md +174 -0
  54. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  55. package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
  56. package/skills/ti-synthengine/references/api.md +434 -380
  57. package/skills/ti-synthengine/references/audio-engine.md +157 -230
  58. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  59. package/skills/ti-synthengine/references/examples.md +123 -253
  60. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  61. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  62. package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
  63. package/skills/ti-synthengine/references/guide.md +188 -156
  64. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  65. package/skills/ti-synthengine/references/performance-review.json +1146 -0
  66. package/skills/ti-synthengine/references/physical-validation.json +1457 -0
  67. package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
  68. package/skills/ti-synthengine/references/recipes.md +423 -599
  69. package/skills/ti-synthengine/references/recommendations.md +63 -113
  70. package/skills/ti-synthengine/references/sound-design.md +134 -258
  71. package/skills/ti-synthengine/references/source-notes.md +93 -105
  72. package/skills/ti-synthengine/references/source-snapshot.json +379 -32
  73. package/skills/ti-synthengine/references/stats-performance.md +64 -20
  74. package/skills/ti-synthengine/references/studio-validation.json +6202 -0
  75. package/skills/ti-synthengine/references/troubleshooting.md +109 -121
  76. package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
  77. package/skills/ti-synthengine/references/unified-validation.json +314 -0
  78. 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
- var synth = require('ti.synthengine')
12
- var contract = synth.getDefaults()
13
- var WAVE = contract.waveTypes
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
- The recipes under "Expressive envelopes, filters and FM" pass the real
21
- contract, but they come from standard synthesis ranges and have not been
22
- auditioned on a device. Listen before shipping them.
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
- var accepted = synth.playTone({
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. A step's `duration`
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.playPattern({
119
- attack: 2,
120
- release: 30,
121
- volume: 0.55,
122
- waveType: WAVE.SQUARE,
123
- steps: [
124
- { frequency: 523.25, duration: 95 },
125
- { rest: true, duration: 15 },
126
- { frequency: 587.33, duration: 95 },
127
- { rest: true, duration: 15 },
128
- { frequency: 659.25, duration: 130 }
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.playPattern({
141
- attack: 5,
142
- release: 15,
143
- volume: 0.5,
144
- waveType: WAVE.SQUARE,
145
- steps: [
146
- { frequency: 261.63, duration: 40 },
147
- { rest: true, duration: 10 },
148
- { frequency: 329.63, duration: 40 },
149
- { rest: true, duration: 10 },
150
- { frequency: 392, duration: 40 },
151
- { rest: true, duration: 10 },
152
- { frequency: 523.25, duration: 80 }
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
- var scoreSteps = []
165
-
166
- for (var index = 0; index < 10; index++) {
167
- scoreSteps.push({
168
- frequency: 1046.5,
169
- duration: 60,
170
- attack: 2,
171
- release: 20
172
- })
173
- scoreSteps.push({ rest: true, duration: 10 })
174
- }
175
-
176
- synth.playPattern({
177
- volume: 0.42,
178
- waveType: WAVE.SQUARE,
179
- steps: scoreSteps
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
- var warningSteps = []
212
-
213
- for (var pulse = 0; pulse < 5; pulse++) {
214
- warningSteps.push({ frequency: 800, duration: 120 })
215
- warningSteps.push({ rest: true, duration: 10 })
216
- warningSteps.push({ frequency: 400, duration: 120 })
217
- warningSteps.push({ rest: true, duration: 10 })
218
- }
219
-
220
- synth.playPattern({
221
- attack: 10,
222
- release: 100,
223
- volume: 0.45,
224
- waveType: WAVE.SQUARE,
225
- steps: warningSteps
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.playTone({
238
- frequency: 90,
239
- duration: 150,
240
- attack: 10,
241
- release: 120,
242
- volume: 0.55,
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
- If this becomes a repeating effect, own and cancel its JavaScript timer; see
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
- Triangle gives a mallet-like balance of a defined pitch and soft upper
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
- Four separated square-wave frequencies remain easy to distinguish at short
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
- ### Convert MIDI pitch to hertz
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
- ### Plucked marimba note
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
- Keep descriptive metadata outside the native options object. Build a new
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 exactly one pitch source for a tone and exactly one pitch array for a
787
- chord;
788
- - include a pitch field on every sounding pattern step, including noise;
789
- - treat each resolved step duration as both its voice length and timeline
790
- advance; insert explicit rests when a shorter note needs a longer interval;
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 results at important control boundaries;
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.