@maccesar/titools 5.1.1 → 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 (98) hide show
  1. package/CONTRIBUTING.md +69 -0
  2. package/README.md +112 -72
  3. package/agents/ti-pro.md +5 -1
  4. package/commands/ti-audit.md +5 -1
  5. package/docs/examples.md +91 -0
  6. package/docs/knowledge-index.md +57 -0
  7. package/lib/config.js +2 -0
  8. package/lib/downloader.js +1 -1
  9. package/package.json +4 -2
  10. package/skills/purgetss/references/multi-density-images.md +1 -1
  11. package/skills/ti-admob-unified/SKILL.md +58 -0
  12. package/skills/ti-admob-unified/agents/openai.yaml +4 -0
  13. package/skills/ti-admob-unified/assets/example/app.js +1207 -0
  14. package/skills/ti-admob-unified/assets/example/lib/ads.js +646 -0
  15. package/skills/ti-admob-unified/assets/example/library-usage.js +136 -0
  16. package/skills/ti-admob-unified/references/api-reference.md +615 -0
  17. package/skills/ti-admob-unified/references/banners.md +435 -0
  18. package/skills/ti-admob-unified/references/contributing.md +233 -0
  19. package/skills/ti-admob-unified/references/dependencies.md +91 -0
  20. package/skills/ti-admob-unified/references/fullscreen.md +238 -0
  21. package/skills/ti-admob-unified/references/game-monetization.md +148 -0
  22. package/skills/ti-admob-unified/references/getting-started.md +319 -0
  23. package/skills/ti-admob-unified/references/index.md +33 -0
  24. package/skills/ti-admob-unified/references/integration.md +87 -0
  25. package/skills/ti-admob-unified/references/library.md +220 -0
  26. package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
  27. package/skills/ti-admob-unified/references/mediation.md +145 -0
  28. package/skills/ti-admob-unified/references/migration.md +259 -0
  29. package/skills/ti-admob-unified/references/module-changelog.md +84 -0
  30. package/skills/ti-admob-unified/references/module-license.txt +21 -0
  31. package/skills/ti-admob-unified/references/privacy.md +281 -0
  32. package/skills/ti-admob-unified/references/production.md +67 -0
  33. package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
  34. package/skills/ti-admob-unified/references/roadmap.md +143 -0
  35. package/skills/ti-admob-unified/references/source-notes.md +80 -0
  36. package/skills/ti-admob-unified/references/source-snapshot.json +644 -0
  37. package/skills/ti-admob-unified/references/third-party-notices.md +22 -0
  38. package/skills/ti-game/SKILL.md +31 -5
  39. package/skills/ti-game/references/api.md +36 -24
  40. package/skills/ti-game/references/debugging-performance.md +22 -7
  41. package/skills/ti-game/references/examples.md +113 -20
  42. package/skills/ti-game/references/input.md +101 -0
  43. package/skills/ti-game/references/patterns.md +56 -10
  44. package/skills/ti-game/references/physics.md +530 -0
  45. package/skills/ti-game/references/project-setup.md +131 -47
  46. package/skills/ti-game/references/recipes-action.md +149 -18
  47. package/skills/ti-game/references/recipes-adventure-effects.md +47 -4
  48. package/skills/ti-game/references/roadmap.md +11 -4
  49. package/skills/ti-game/references/source-notes.md +81 -0
  50. package/skills/ti-game/references/tilemaps.md +13 -4
  51. package/skills/ti-game-assets/SKILL.md +53 -0
  52. package/skills/ti-game-assets/agents/openai.yaml +4 -0
  53. package/skills/ti-game-assets/references/alpha.md +26 -0
  54. package/skills/ti-game-assets/references/art-direction.md +37 -0
  55. package/skills/ti-game-assets/references/integration.md +49 -0
  56. package/skills/ti-game-assets/references/manifest.example.json +429 -0
  57. package/skills/ti-game-assets/references/manifest.md +39 -0
  58. package/skills/ti-game-assets/references/offline-audio.md +33 -0
  59. package/skills/ti-game-assets/references/prompts.md +33 -0
  60. package/skills/ti-game-assets/references/repeating-textures.md +17 -0
  61. package/skills/ti-game-assets/references/responsible-ads.md +31 -0
  62. package/skills/ti-game-assets/references/sound-assets.md +42 -0
  63. package/skills/ti-game-assets/references/vector-sources.md +21 -0
  64. package/skills/ti-game-assets/scripts/alpha_tools.py +160 -0
  65. package/skills/ti-game-assets/scripts/requirements.txt +1 -0
  66. package/skills/ti-game-assets/scripts/validate_manifest.py +267 -0
  67. package/skills/ti-synthengine/SKILL.md +77 -67
  68. package/skills/ti-synthengine/assets/example/README.md +84 -0
  69. package/skills/ti-synthengine/assets/example/app.js +1178 -0
  70. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
  71. package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
  72. package/skills/ti-synthengine/references/advanced-examples.md +174 -0
  73. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  74. package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
  75. package/skills/ti-synthengine/references/api.md +573 -153
  76. package/skills/ti-synthengine/references/audio-engine.md +272 -124
  77. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  78. package/skills/ti-synthengine/references/examples.md +355 -106
  79. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  80. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  81. package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
  82. package/skills/ti-synthengine/references/guide.md +426 -75
  83. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  84. package/skills/ti-synthengine/references/performance-review.json +1146 -0
  85. package/skills/ti-synthengine/references/physical-validation.json +1457 -0
  86. package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
  87. package/skills/ti-synthengine/references/recipes.md +434 -420
  88. package/skills/ti-synthengine/references/recommendations.md +72 -85
  89. package/skills/ti-synthengine/references/sound-design.md +375 -155
  90. package/skills/ti-synthengine/references/source-notes.md +109 -0
  91. package/skills/ti-synthengine/references/source-snapshot.json +452 -0
  92. package/skills/ti-synthengine/references/stats-performance.json +2992 -0
  93. package/skills/ti-synthengine/references/stats-performance.md +82 -0
  94. package/skills/ti-synthengine/references/studio-validation.json +6202 -0
  95. package/skills/ti-synthengine/references/troubleshooting.md +123 -78
  96. package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
  97. package/skills/ti-synthengine/references/unified-validation.json +314 -0
  98. package/AGENTS-VERCEL-RESEARCH.md +0 -175
@@ -2,21 +2,27 @@
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
 
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.
25
+
20
26
  <!-- TOC-START -->
21
27
  ## Contents
22
28
 
@@ -24,6 +30,7 @@ overlap.
24
30
  - [Playable xylophone](#playable-xylophone)
25
31
  - [Four-pad memory cues](#four-pad-memory-cues)
26
32
  - [Polyphony and layered chords](#polyphony-and-layered-chords)
33
+ - [Expressive envelopes, filters and FM](#expressive-envelopes-filters-and-fm)
27
34
  - [Preset architecture for strict contracts](#preset-architecture-for-strict-contracts)
28
35
  - [Recipe verification checklist](#recipe-verification-checklist)
29
36
 
@@ -38,7 +45,7 @@ and acceleration. The short release removes the edge without blurring the
38
45
  motion.
39
46
 
40
47
  ```javascript
41
- var accepted = synth.playTone({
48
+ const accepted = synth.playTone({
42
49
  frequency: 196,
43
50
  frequencyEnd: 523,
44
51
  duration: 350,
@@ -105,69 +112,293 @@ were chosen for it.
105
112
 
106
113
  ### Three-note bonus cue
107
114
 
108
- Short ascending square notes read as collection or reward. `stepDuration`
109
- controls spacing; each step's `duration` controls its gate.
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.
110
116
 
111
117
  ```javascript
112
- synth.playPattern({
113
- stepDuration: 110,
114
- attack: 2,
115
- release: 30,
116
- volume: 0.55,
117
- waveType: WAVE.SQUARE,
118
- steps: [
119
- { frequency: 523.25, duration: 95 },
120
- { frequency: 587.33, duration: 95 },
121
- { 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
+ }
122
161
  ]
123
162
  })
163
+ if (!sequenceId) throw new Error('Sequence rejected')
124
164
  ```
125
165
 
126
166
  ### Fast four-note arpeggio
127
167
 
128
168
  Very short square notes with a small attack retain the hard cartridge-era
129
- edge while reducing uncontrolled clicks.
169
+ edge while reducing uncontrolled clicks. The 10 ms rests keep the first three
170
+ 40 ms notes on a 50 ms grid; the final note rings for 80 ms.
130
171
 
131
172
  ```javascript
132
- synth.playPattern({
133
- stepDuration: 50,
134
- attack: 5,
135
- release: 15,
136
- volume: 0.5,
137
- waveType: WAVE.SQUARE,
138
- steps: [
139
- { frequency: 261.63, duration: 40 },
140
- { frequency: 329.63, duration: 40 },
141
- { frequency: 392, duration: 40 },
142
- { 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
+ }
143
228
  ]
144
229
  })
230
+ if (!sequenceId) throw new Error('Sequence rejected')
145
231
  ```
146
232
 
147
233
  ### Rapid score ticks
148
234
 
149
235
  Use one native pattern instead of ten JavaScript timers. Explicit attack and
150
- release values make every 60 ms step valid without relying on shortened
151
- defaults.
236
+ release values make every 60 ms tone valid without relying on shortened
237
+ defaults. A 10 ms rest after each tone places the ticks 70 ms apart.
152
238
 
153
239
  ```javascript
154
- var scoreSteps = []
155
-
156
- for (var index = 0; index < 10; index++) {
157
- scoreSteps.push({
158
- frequency: 1046.5,
159
- duration: 60,
160
- attack: 2,
161
- release: 20
162
- })
163
- }
164
-
165
- synth.playPattern({
166
- stepDuration: 70,
167
- volume: 0.42,
168
- waveType: WAVE.SQUARE,
169
- 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
+ ]
170
400
  })
401
+ if (!sequenceId) throw new Error('Sequence rejected')
171
402
  ```
172
403
 
173
404
  ### Hovering craft
@@ -194,25 +425,144 @@ synth.playTone({
194
425
  ### Alternating warning alarm
195
426
 
196
427
  An octave-like high/low alternation is easy to identify even through a small
197
- speaker. The envelope fits exactly inside every 120 ms tone:
198
- `10 + 100 <= 120`.
428
+ speaker. Each 120 ms tone has a fitted envelope (`10 + 100 <= 120`), followed
429
+ by a 10 ms rest, so the pitch changes every 130 ms.
199
430
 
200
431
  ```javascript
201
- var warningSteps = []
202
-
203
- for (var pulse = 0; pulse < 5; pulse++) {
204
- warningSteps.push({ frequency: 800, duration: 120 })
205
- warningSteps.push({ frequency: 400, duration: 120 })
206
- }
207
-
208
- synth.playPattern({
209
- stepDuration: 130,
210
- attack: 10,
211
- release: 100,
212
- volume: 0.45,
213
- waveType: WAVE.SQUARE,
214
- 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
+ ]
215
564
  })
565
+ if (!sequenceId) throw new Error('Sequence rejected')
216
566
  ```
217
567
 
218
568
  ### Low heartbeat pair
@@ -223,388 +573,52 @@ phone speaker, try 180 and 160 Hz while preserving the rhythm.
223
573
 
224
574
  ```javascript
225
575
  function playHeartbeatPair () {
226
- synth.playTone({
227
- frequency: 90,
228
- duration: 150,
229
- attack: 10,
230
- release: 120,
231
- volume: 0.55,
232
- 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
+ ]
233
582
  })
234
-
235
- setTimeout(function () {
236
- synth.playTone({
237
- frequency: 80,
238
- duration: 150,
239
- attack: 10,
240
- release: 120,
241
- volume: 0.48,
242
- waveType: WAVE.TRIANGLE
243
- })
244
- }, 220)
245
583
  }
246
584
  ```
247
585
 
248
- If this becomes a repeating effect, own and cancel its JavaScript timer; see
249
- [Cancellable repetition](#cancellable-repetition).
586
+ The native clock owns both attacks. Keep the returned ID to stop only this cue.
250
587
 
251
588
  ## Playable xylophone
252
589
 
253
- Triangle gives a mallet-like balance of a defined pitch and soft upper
254
- harmonics. A 2 ms attack makes the strike immediate, while a 180 ms release
255
- lets adjacent bars overlap musically.
256
-
257
- ```javascript
258
- var XYLOPHONE = [
259
- { label: 'C5', frequency: 523.25 },
260
- { label: 'D5', frequency: 587.33 },
261
- { label: 'E5', frequency: 659.25 },
262
- { label: 'F5', frequency: 698.46 },
263
- { label: 'G5', frequency: 783.99 },
264
- { label: 'A5', frequency: 880 },
265
- { label: 'B5', frequency: 987.77 },
266
- { label: 'C6', frequency: 1046.5 }
267
- ]
268
-
269
- function playXylophoneBar (index, pan) {
270
- var bar = XYLOPHONE[index]
271
- if (!bar || !isFinite(pan) || pan < -1 || pan > 1) return false
272
-
273
- return synth.playTone({
274
- frequency: bar.frequency,
275
- duration: 420,
276
- attack: 2,
277
- release: 180,
278
- volume: 0.65,
279
- waveType: WAVE.TRIANGLE,
280
- pan: pan
281
- })
282
- }
283
- ```
284
-
285
- Map the bars across the stereo field with
286
- `pan = -0.8 + (1.6 * index / (barCount - 1))`. Do not make pan carry essential
287
- meaning because the device may have a mono output.
288
-
289
- ### Drag interaction without note storms
290
-
291
- A `touchmove` event may fire many times while the finger remains on one bar.
292
- Track the last entered bar and strike only when the index changes:
293
-
294
- ```javascript
295
- function createBarGesture (playIndex) {
296
- var tracking = false
297
- var activeIndex = -1
298
-
299
- function enter (index) {
300
- if (!tracking || index < 0 || index === activeIndex) return
301
- activeIndex = index
302
- playIndex(index)
303
- }
304
-
305
- return {
306
- start: function (index) {
307
- tracking = true
308
- activeIndex = -1
309
- enter(index)
310
- },
311
- move: enter,
312
- finish: function () {
313
- tracking = false
314
- activeIndex = -1
315
- }
316
- }
317
- }
318
- ```
319
-
320
- In a Titanium view:
321
-
322
- - compute each bar's hit rectangle after layout with `convertPointToView()`;
323
- - convert move coordinates into one shared container before hit testing;
324
- - call `gesture.start(index)` on `touchstart` and `gesture.move(index)` only
325
- after resolving a valid bar;
326
- - call `gesture.finish()` on both `touchend` and `touchcancel`;
327
- - keep visual pressed-state counts separate if multiple touches are supported.
328
-
329
- The audio release may continue after the finger leaves a bar. That is desired
330
- for a xylophone; call `stopAll()` only for an explicit all-notes-off action.
590
+ Continue in [the interaction recipes](recipes-interaction.md#playable-xylophone).
331
591
 
332
592
  ## Four-pad memory cues
333
593
 
334
- Four separated square-wave frequencies remain easy to distinguish at short
335
- durations. Keep each bank as app metadata, then build a fresh closed options
336
- object for the module.
337
-
338
- ```javascript
339
- var PAD_BANKS = {
340
- low: [415, 310, 252, 209],
341
- equalTempered: [391.995, 329.628, 261.626, 195.998]
342
- }
343
-
344
- function playMemoryPad (bankName, index) {
345
- var bank = PAD_BANKS[bankName]
346
- var frequency = bank && bank[index]
347
- if (!isFinite(frequency)) return false
348
-
349
- return synth.playTone({
350
- frequency: frequency,
351
- duration: 420,
352
- attack: 6,
353
- release: 100,
354
- volume: 0.62,
355
- waveType: WAVE.SQUARE
356
- })
357
- }
358
- ```
359
-
360
- To demonstrate the whole pitch bank as one event, use a chord rather than four
361
- independent full-gain calls:
362
-
363
- ```javascript
364
- function playMemoryBank (bankName) {
365
- var bank = PAD_BANKS[bankName]
366
- if (!bank) return false
367
-
368
- return synth.playChord({
369
- frequencies: bank.slice(),
370
- duration: 480,
371
- attack: 8,
372
- release: 120,
373
- volume: 0.56,
374
- waveType: WAVE.SQUARE,
375
- pans: [-0.75, -0.25, 0.25, 0.75]
376
- })
377
- }
378
- ```
379
-
380
- ### Controlled timbre comparison
381
-
382
- For an honest A/B test, hold pitches, duration, envelope and pan constant.
383
- Change only waveform and its comparison level. Never pass a profile object
384
- containing labels or other app metadata directly to `playChord()` because its
385
- options dictionary is closed.
386
-
387
- ```javascript
388
- var TIMBRE_PROFILES = {
389
- square: { waveType: WAVE.SQUARE, volume: 0.52 },
390
- sine: { waveType: WAVE.SINE, volume: 0.68 },
391
- triangle: { waveType: WAVE.TRIANGLE, volume: 0.64 },
392
- sawtooth: { waveType: WAVE.SAWTOOTH, volume: 0.46 }
393
- }
394
-
395
- function auditionTimbre (profileName) {
396
- var profile = TIMBRE_PROFILES[profileName]
397
- if (!profile) return false
398
-
399
- return synth.playChord({
400
- frequencies: PAD_BANKS.low.slice(),
401
- duration: 420,
402
- attack: 8,
403
- release: 100,
404
- volume: profile.volume,
405
- waveType: profile.waveType,
406
- pans: [-0.75, -0.25, 0.25, 0.75]
407
- })
408
- }
409
- ```
594
+ Continue in [the interaction recipes](recipes-interaction.md#four-pad-memory-cues).
410
595
 
411
596
  ## Polyphony and layered chords
412
597
 
413
- ### Convert MIDI pitch to hertz
414
-
415
- The standard A4 = 440 Hz formula makes it easy to build interval recipes and
416
- small detunings without embedding a long frequency table.
417
-
418
- ```javascript
419
- function midiToHz (midi) {
420
- return 440 * Math.pow(2, (midi - 69) / 12)
421
- }
422
- ```
423
-
424
- A cent is one hundredth of a semitone, so add `cents / 100` to the MIDI value
425
- before conversion.
426
-
427
- ### Build a wide layered chord
428
-
429
- The interval cycle `[0, 4, 7, ...]` stacks major-triad chord tones across
430
- octaves. Later layers repeat those tones with small cent offsets, which creates
431
- width and beating without requiring an unsupported detune option.
432
-
433
- ```javascript
434
- var CHORD_INTERVALS = [0, 4, 7, 12, 16, 19, 24, 28, 31, 36]
435
- var LAYER_DETUNE_CENTS = [0, -8, 8, -12]
436
-
437
- function normalizeVoiceCount (value) {
438
- var count = Math.round(Number(value))
439
- if (!isFinite(count)) return 1
440
- return Math.max(1, Math.min(32, count))
441
- }
442
-
443
- function buildLayeredFrequencies (voiceCount) {
444
- var count = normalizeVoiceCount(voiceCount)
445
- var frequencies = []
598
+ Continue in [the interaction recipes](recipes-interaction.md#polyphony-and-layered-chords).
446
599
 
447
- for (var index = 0; index < count; index++) {
448
- var intervalIndex = index % CHORD_INTERVALS.length
449
- var layer = Math.floor(index / CHORD_INTERVALS.length)
450
- var cents = LAYER_DETUNE_CENTS[layer % LAYER_DETUNE_CENTS.length]
451
- var midi = 60.12 + CHORD_INTERVALS[intervalIndex] + (cents / 100)
452
- frequencies.push(midiToHz(midi))
453
- }
454
-
455
- return frequencies
456
- }
600
+ ## Expressive envelopes, filters and FM
457
601
 
458
- function buildSymmetricPans (voiceCount, spread) {
459
- var count = normalizeVoiceCount(voiceCount)
460
- var numericSpread = Number(spread)
461
- var safeSpread = isFinite(numericSpread)
462
- ? Math.max(0, Math.min(1, numericSpread))
463
- : 0
464
- var pans = []
465
-
466
- if (count === 1) return [0]
467
-
468
- for (var index = 0; index < count; index++) {
469
- pans.push(-safeSpread + (2 * safeSpread * index / (count - 1)))
470
- }
471
-
472
- return pans
473
- }
474
-
475
- function playLayeredChord (voiceCount) {
476
- var count = normalizeVoiceCount(voiceCount)
477
-
478
- return synth.playChord({
479
- frequencies: buildLayeredFrequencies(count),
480
- duration: 800,
481
- attack: 35,
482
- release: 220,
483
- volume: 0.48,
484
- waveType: WAVE.SINE,
485
- pans: buildSymmetricPans(count, 0.8)
486
- })
487
- }
488
- ```
489
-
490
- The engine must have `maxVoices` at least as large as the intended simultaneous
491
- chord. Four to twelve voices are a practical musical starting point. Counts
492
- near 32 are useful for deliberate capacity tests, not as a default.
493
-
494
- ### Clean reference versus overlap stress
495
-
496
- A single sustained chord is the clean reference for judging waveform quality.
497
- If a new 220 ms chord starts every 120 ms, release tails overlap and the summed
498
- level rises and falls. Limiter pumping is expected in that stress condition;
499
- it is not proof that a single chord is defective.
500
-
501
- For a musical pulse, keep the event shorter than its period:
502
-
503
- ```javascript
504
- var pulseTimer = null
505
- var pulseGeneration = 0
506
-
507
- function stopChordPulse () {
508
- pulseGeneration++
509
- if (pulseTimer !== null) {
510
- clearTimeout(pulseTimer)
511
- pulseTimer = null
512
- }
513
- synth.stopAll()
514
- }
515
-
516
- function startChordPulse () {
517
- stopChordPulse()
518
- var generation = pulseGeneration
519
-
520
- function tick () {
521
- if (generation !== pulseGeneration) return
522
-
523
- synth.playChord({
524
- frequencies: buildLayeredFrequencies(4),
525
- duration: 180,
526
- attack: 10,
527
- release: 60,
528
- volume: 0.42,
529
- waveType: WAVE.SINE,
530
- pans: buildSymmetricPans(4, 0.7)
531
- })
532
-
533
- pulseTimer = setTimeout(tick, 260)
534
- }
535
-
536
- tick()
537
- }
538
- ```
539
-
540
- ### Cancellable repetition
541
-
542
- `stopAll()` silences native voices, but it cannot cancel JavaScript timers that
543
- will launch new calls later. Every repeating UI or game effect therefore needs
544
- an owner-controlled cancellation mechanism. The generation token above guards
545
- against a callback that was already queued when `clearTimeout()` ran.
546
-
547
- When the owning window closes:
548
-
549
- ```javascript
550
- function cleanupAudio () {
551
- stopChordPulse()
552
- synth.shutdown()
553
- }
554
-
555
- win.addEventListener('close', cleanupAudio)
556
- ```
557
-
558
- Do not call `shutdown()` from a child control if another visible screen owns
559
- the shared engine.
602
+ Continue in [the interaction recipes](recipes-interaction.md#expressive-envelopes-filters-and-fm).
560
603
 
561
604
  ## Preset architecture for strict contracts
562
605
 
563
- Keep descriptive metadata outside the native options object. Build a new
564
- closed object at the call boundary so keys such as `label`, `category`, or
565
- `description` never reach the module.
566
-
567
- ```javascript
568
- var PRESETS = {
569
- success: {
570
- label: 'Success',
571
- frequency: 760,
572
- frequencyEnd: 1040,
573
- waveType: WAVE.TRIANGLE
574
- }
575
- }
576
-
577
- function playPreset (name) {
578
- var preset = PRESETS[name]
579
- if (!preset) return false
580
-
581
- return synth.playTone({
582
- frequency: preset.frequency,
583
- frequencyEnd: preset.frequencyEnd,
584
- duration: 150,
585
- attack: 3,
586
- release: 90,
587
- volume: 0.58,
588
- waveType: preset.waveType
589
- })
590
- }
591
- ```
592
-
593
- Avoid `synth.playTone(PRESETS[name])`: the preset contains `label`, which makes
594
- the native call reject the entire dictionary.
606
+ Continue in [the interaction recipes](recipes-interaction.md#preset-architecture-for-strict-contracts).
595
607
 
596
608
  ## Recipe verification checklist
597
609
 
598
610
  Before returning or shipping a variation:
599
611
 
600
- - use exactly one pitch source for a tone and exactly one pitch array for a
601
- chord;
602
- - include a pitch field on every sounding pattern step, including noise;
603
- - verify `attack < duration` and `release <= duration - attack` for every
604
- explicit event or step;
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;
617
+ - give `sustain` below 1 a `decay`, give filter fields a `filterType`, and give
618
+ FM a sine carrier and a depth;
605
619
  - keep all numbers finite and integer-only fields integral;
606
620
  - use `WAVE` constants, not copied numeric waveform values;
607
- - check Boolean results at important control boundaries;
621
+ - check Boolean acceptance and positive sequence IDs at important control boundaries;
608
622
  - size `maxVoices` for the largest real overlap, including release tails;
609
623
  - cancel app-owned timers before `stopAll()` or `shutdown()`;
610
624
  - audition on the intended phone speaker as well as headphones.