@maccesar/titools 4.16.2 → 4.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS-VERCEL-RESEARCH.md +10 -6
- package/README.md +58 -31
- package/agents/ti-pro.md +16 -3
- package/commands/ti-audit.md +21 -6
- package/commands/ti-check.md +2 -1
- package/lib/config.js +1 -0
- package/lib/downloader.js +1 -0
- package/package.json +6 -3
- package/skills/ti-synthengine/SKILL.md +92 -0
- package/skills/ti-synthengine/references/api.md +236 -0
- package/skills/ti-synthengine/references/audio-engine.md +217 -0
- package/skills/ti-synthengine/references/examples.md +348 -0
- package/skills/ti-synthengine/references/guide.md +252 -0
- package/skills/ti-synthengine/references/recipes.md +610 -0
- package/skills/ti-synthengine/references/recommendations.md +150 -0
- package/skills/ti-synthengine/references/sound-design.md +428 -0
- package/skills/ti-synthengine/references/troubleshooting.md +162 -0
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
# Curated sound and interaction recipes
|
|
2
|
+
|
|
3
|
+
These recipes are tuned starting points for `ti.synthengine` 1.0.0. They use
|
|
4
|
+
only public option keys, keep every explicit envelope valid, and obtain
|
|
5
|
+
waveforms from the runtime contract instead of hardcoding integers.
|
|
6
|
+
|
|
7
|
+
Unless a section says otherwise, the snippets assume one owner has already
|
|
8
|
+
started the engine:
|
|
9
|
+
|
|
10
|
+
```javascript
|
|
11
|
+
var synth = require('ti.synthengine')
|
|
12
|
+
var contract = synth.getDefaults()
|
|
13
|
+
var WAVE = contract.waveTypes
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Treat levels as starting points, not mastering targets. Test on the actual
|
|
17
|
+
phone speaker and leave headroom when release tails or independent events can
|
|
18
|
+
overlap.
|
|
19
|
+
|
|
20
|
+
<!-- TOC-START -->
|
|
21
|
+
## Contents
|
|
22
|
+
|
|
23
|
+
- [Retro and arcade effects](#retro-and-arcade-effects)
|
|
24
|
+
- [Playable xylophone](#playable-xylophone)
|
|
25
|
+
- [Four-pad memory cues](#four-pad-memory-cues)
|
|
26
|
+
- [Polyphony and layered chords](#polyphony-and-layered-chords)
|
|
27
|
+
- [Preset architecture for strict contracts](#preset-architecture-for-strict-contracts)
|
|
28
|
+
- [Recipe verification checklist](#recipe-verification-checklist)
|
|
29
|
+
|
|
30
|
+
<!-- TOC-END -->
|
|
31
|
+
|
|
32
|
+
## Retro and arcade effects
|
|
33
|
+
|
|
34
|
+
### Rising missile launch
|
|
35
|
+
|
|
36
|
+
A bright square wave and a fast upward sweep create the impression of thrust
|
|
37
|
+
and acceleration. The short release removes the edge without blurring the
|
|
38
|
+
motion.
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
var accepted = synth.playTone({
|
|
42
|
+
frequency: 196,
|
|
43
|
+
frequencyEnd: 523,
|
|
44
|
+
duration: 350,
|
|
45
|
+
attack: 0,
|
|
46
|
+
release: 50,
|
|
47
|
+
volume: 0.55,
|
|
48
|
+
waveType: WAVE.SQUARE
|
|
49
|
+
})
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Downward projectile
|
|
53
|
+
|
|
54
|
+
Reverse the pitch direction for a compact shot, fall, or power-down effect.
|
|
55
|
+
|
|
56
|
+
```javascript
|
|
57
|
+
synth.playTone({
|
|
58
|
+
frequency: 900,
|
|
59
|
+
frequencyEnd: 200,
|
|
60
|
+
duration: 150,
|
|
61
|
+
attack: 0,
|
|
62
|
+
release: 50,
|
|
63
|
+
volume: 0.48,
|
|
64
|
+
waveType: WAVE.SQUARE
|
|
65
|
+
})
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Power-up sweep
|
|
69
|
+
|
|
70
|
+
Sawtooth carries more upper harmonics than square or triangle, so this sweep
|
|
71
|
+
cuts through small speakers at a moderate event level.
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
synth.playTone({
|
|
75
|
+
frequency: 220,
|
|
76
|
+
frequencyEnd: 880,
|
|
77
|
+
duration: 600,
|
|
78
|
+
attack: 10,
|
|
79
|
+
release: 100,
|
|
80
|
+
volume: 0.5,
|
|
81
|
+
waveType: WAVE.SAWTOOTH
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Noise explosion
|
|
86
|
+
|
|
87
|
+
White noise supplies an impact spectrum without implying a musical pitch. The
|
|
88
|
+
long release creates the debris tail. A pitch field is still included because
|
|
89
|
+
every sounding event follows the same contract.
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
synth.playTone({
|
|
93
|
+
frequency: 200,
|
|
94
|
+
duration: 600,
|
|
95
|
+
attack: 0,
|
|
96
|
+
release: 500,
|
|
97
|
+
volume: 0.72,
|
|
98
|
+
waveType: WAVE.NOISE
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For a shorter blast, use `duration: 500` and `release: 400`. Do not stack
|
|
103
|
+
several full-level explosions unless the engine profile and overlap budget
|
|
104
|
+
were chosen for it.
|
|
105
|
+
|
|
106
|
+
### Three-note bonus cue
|
|
107
|
+
|
|
108
|
+
Short ascending square notes read as collection or reward. `stepDuration`
|
|
109
|
+
controls spacing; each step's `duration` controls its gate.
|
|
110
|
+
|
|
111
|
+
```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 }
|
|
122
|
+
]
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Fast four-note arpeggio
|
|
127
|
+
|
|
128
|
+
Very short square notes with a small attack retain the hard cartridge-era
|
|
129
|
+
edge while reducing uncontrolled clicks.
|
|
130
|
+
|
|
131
|
+
```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 }
|
|
143
|
+
]
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Rapid score ticks
|
|
148
|
+
|
|
149
|
+
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.
|
|
152
|
+
|
|
153
|
+
```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
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Hovering craft
|
|
174
|
+
|
|
175
|
+
A large triangle-wave LFO is no longer subtle vibrato: it becomes a cyclical
|
|
176
|
+
engine wobble. Slow stereo travel adds movement on headphones while pitch
|
|
177
|
+
remains the primary cue on mono speakers.
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
synth.playTone({
|
|
181
|
+
frequency: 800,
|
|
182
|
+
duration: 2500,
|
|
183
|
+
attack: 100,
|
|
184
|
+
release: 400,
|
|
185
|
+
volume: 0.42,
|
|
186
|
+
waveType: WAVE.TRIANGLE,
|
|
187
|
+
lfoFreq: 12,
|
|
188
|
+
lfoDepth: 200,
|
|
189
|
+
pan: -0.35,
|
|
190
|
+
panEnd: 0.35
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Alternating warning alarm
|
|
195
|
+
|
|
196
|
+
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`.
|
|
199
|
+
|
|
200
|
+
```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
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Low heartbeat pair
|
|
219
|
+
|
|
220
|
+
Two separated triangle pulses suggest a double heartbeat without the buzz of a
|
|
221
|
+
square wave. These sub-bass pitches suit headphones or a larger speaker; for a
|
|
222
|
+
phone speaker, try 180 and 160 Hz while preserving the rhythm.
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
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
|
|
233
|
+
})
|
|
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
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
If this becomes a repeating effect, own and cancel its JavaScript timer; see
|
|
249
|
+
[Cancellable repetition](#cancellable-repetition).
|
|
250
|
+
|
|
251
|
+
## Playable xylophone
|
|
252
|
+
|
|
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.
|
|
331
|
+
|
|
332
|
+
## Four-pad memory cues
|
|
333
|
+
|
|
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
|
+
```
|
|
410
|
+
|
|
411
|
+
## Polyphony and layered chords
|
|
412
|
+
|
|
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 = []
|
|
446
|
+
|
|
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
|
+
}
|
|
457
|
+
|
|
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.
|
|
560
|
+
|
|
561
|
+
## Preset architecture for strict contracts
|
|
562
|
+
|
|
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.
|
|
595
|
+
|
|
596
|
+
## Recipe verification checklist
|
|
597
|
+
|
|
598
|
+
Before returning or shipping a variation:
|
|
599
|
+
|
|
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;
|
|
605
|
+
- keep all numbers finite and integer-only fields integral;
|
|
606
|
+
- use `WAVE` constants, not copied numeric waveform values;
|
|
607
|
+
- check Boolean results at important control boundaries;
|
|
608
|
+
- size `maxVoices` for the largest real overlap, including release tails;
|
|
609
|
+
- cancel app-owned timers before `stopAll()` or `shutdown()`;
|
|
610
|
+
- audition on the intended phone speaker as well as headphones.
|