@maccesar/titools 4.16.3 → 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.
@@ -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.