@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.
- package/CONTRIBUTING.md +69 -0
- package/README.md +112 -72
- package/agents/ti-pro.md +5 -1
- package/commands/ti-audit.md +5 -1
- package/docs/examples.md +91 -0
- package/docs/knowledge-index.md +57 -0
- package/lib/config.js +2 -0
- package/lib/downloader.js +1 -1
- package/package.json +4 -2
- package/skills/purgetss/references/multi-density-images.md +1 -1
- package/skills/ti-admob-unified/SKILL.md +58 -0
- package/skills/ti-admob-unified/agents/openai.yaml +4 -0
- package/skills/ti-admob-unified/assets/example/app.js +1207 -0
- package/skills/ti-admob-unified/assets/example/lib/ads.js +646 -0
- package/skills/ti-admob-unified/assets/example/library-usage.js +136 -0
- package/skills/ti-admob-unified/references/api-reference.md +615 -0
- package/skills/ti-admob-unified/references/banners.md +435 -0
- package/skills/ti-admob-unified/references/contributing.md +233 -0
- package/skills/ti-admob-unified/references/dependencies.md +91 -0
- package/skills/ti-admob-unified/references/fullscreen.md +238 -0
- package/skills/ti-admob-unified/references/game-monetization.md +148 -0
- package/skills/ti-admob-unified/references/getting-started.md +319 -0
- package/skills/ti-admob-unified/references/index.md +33 -0
- package/skills/ti-admob-unified/references/integration.md +87 -0
- package/skills/ti-admob-unified/references/library.md +220 -0
- package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
- package/skills/ti-admob-unified/references/mediation.md +145 -0
- package/skills/ti-admob-unified/references/migration.md +259 -0
- package/skills/ti-admob-unified/references/module-changelog.md +84 -0
- package/skills/ti-admob-unified/references/module-license.txt +21 -0
- package/skills/ti-admob-unified/references/privacy.md +281 -0
- 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 +143 -0
- package/skills/ti-admob-unified/references/source-notes.md +80 -0
- package/skills/ti-admob-unified/references/source-snapshot.json +644 -0
- package/skills/ti-admob-unified/references/third-party-notices.md +22 -0
- package/skills/ti-game/SKILL.md +31 -5
- package/skills/ti-game/references/api.md +36 -24
- package/skills/ti-game/references/debugging-performance.md +22 -7
- package/skills/ti-game/references/examples.md +113 -20
- package/skills/ti-game/references/input.md +101 -0
- package/skills/ti-game/references/patterns.md +56 -10
- package/skills/ti-game/references/physics.md +530 -0
- package/skills/ti-game/references/project-setup.md +131 -47
- package/skills/ti-game/references/recipes-action.md +149 -18
- package/skills/ti-game/references/recipes-adventure-effects.md +47 -4
- package/skills/ti-game/references/roadmap.md +11 -4
- package/skills/ti-game/references/source-notes.md +81 -0
- package/skills/ti-game/references/tilemaps.md +13 -4
- package/skills/ti-game-assets/SKILL.md +53 -0
- package/skills/ti-game-assets/agents/openai.yaml +4 -0
- package/skills/ti-game-assets/references/alpha.md +26 -0
- package/skills/ti-game-assets/references/art-direction.md +37 -0
- package/skills/ti-game-assets/references/integration.md +49 -0
- package/skills/ti-game-assets/references/manifest.example.json +429 -0
- package/skills/ti-game-assets/references/manifest.md +39 -0
- package/skills/ti-game-assets/references/offline-audio.md +33 -0
- package/skills/ti-game-assets/references/prompts.md +33 -0
- package/skills/ti-game-assets/references/repeating-textures.md +17 -0
- package/skills/ti-game-assets/references/responsible-ads.md +31 -0
- package/skills/ti-game-assets/references/sound-assets.md +42 -0
- package/skills/ti-game-assets/references/vector-sources.md +21 -0
- package/skills/ti-game-assets/scripts/alpha_tools.py +160 -0
- package/skills/ti-game-assets/scripts/requirements.txt +1 -0
- package/skills/ti-game-assets/scripts/validate_manifest.py +267 -0
- package/skills/ti-synthengine/SKILL.md +77 -67
- 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 +573 -153
- package/skills/ti-synthengine/references/audio-engine.md +272 -124
- package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
- package/skills/ti-synthengine/references/examples.md +355 -106
- 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 +426 -75
- 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 +434 -420
- package/skills/ti-synthengine/references/recommendations.md +72 -85
- package/skills/ti-synthengine/references/sound-design.md +375 -155
- package/skills/ti-synthengine/references/source-notes.md +109 -0
- package/skills/ti-synthengine/references/source-snapshot.json +452 -0
- package/skills/ti-synthengine/references/stats-performance.json +2992 -0
- package/skills/ti-synthengine/references/stats-performance.md +82 -0
- package/skills/ti-synthengine/references/studio-validation.json +6202 -0
- package/skills/ti-synthengine/references/troubleshooting.md +123 -78
- 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,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
|
-
|
|
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
|
|
|
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
|
-
|
|
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. `
|
|
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.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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.
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
601
|
-
chord;
|
|
602
|
-
-
|
|
603
|
-
-
|
|
604
|
-
|
|
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
|
|
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.
|