@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
@@ -1,11 +1,10 @@
1
- > Source snapshot: official TiSynthEngine 1.0.0 documentation at commit `6685b81` (2026-08-29).
1
+ > Source snapshot: official TiSynthEngine 1.0.0 documentation at release source commit `e7cbbe8a6f9ab38088fd8029c7b43cef6501101f` (2026-10-24). Upstream `documentation/sound-design.md` SHA-256: `dd543e07c825aaf67a0a5f85619088a8d02721a9d33dbf4579edf760dad3e262`.
2
2
 
3
3
  # Sound design with TiSynthEngine
4
4
 
5
- TiSynthEngine builds sounds from oscillators. There is no recorded sample
6
- underneath a tone: pitch, waveform, envelope, movement and level describe the
7
- whole sound. That small set of controls is enough for interface feedback,
8
- musical cues, alarms, retro effects and simple percussion.
5
+ TiSynthEngine generates sound with oscillators, without recorded samples. Pitch, waveform, envelope, movement and level define each sound, from interface feedback and musical cues to alarms, retro effects and simple percussion.
6
+
7
+ Additional [curated recipes](recipes.md) provide application sound and interaction variants.
9
8
 
10
9
  The examples assume the engine is running and use these constants:
11
10
 
@@ -19,110 +18,123 @@ var WAVE = synth.getDefaults().waveTypes
19
18
 
20
19
  - [Notes, frequencies and octaves](#notes-frequencies-and-octaves)
21
20
  - [Waveform and timbre](#waveform-and-timbre)
21
+ - [Filter, pulse width and quantization](#filter-pulse-width-and-quantization)
22
22
  - [Envelope and total duration](#envelope-and-total-duration)
23
+ - [Decay and sustain](#decay-and-sustain)
23
24
  - [Pan and movement](#pan-and-movement)
24
25
  - [Vibrato and the LFO](#vibrato-and-the-lfo)
26
+ - [Resonant filter](#resonant-filter)
27
+ - [Frequency modulation](#frequency-modulation)
28
+ - [Detune](#detune)
25
29
  - [Chords, polyphony and gain](#chords-polyphony-and-gain)
26
30
  - [Mixing profiles and limiter](#mixing-profiles-and-limiter)
27
31
  - [Speakers, headphones and devices](#speakers-headphones-and-devices)
28
32
  - [Recipes](#recipes)
33
+ - [Holding a sound and adding movement](#holding-a-sound-and-adding-movement)
34
+ - [Native space and creative drive](#native-space-and-creative-drive)
29
35
 
30
36
  <!-- TOC-END -->
31
37
 
32
38
  ## Notes, frequencies and octaves
33
39
 
34
- Frequency is the number of waveform cycles per second, measured in hertz. A
35
- 440 Hz oscillator repeats 440 times each second. TiSynthEngine accepts a
36
- frequency directly or converts a note name for you:
40
+ Frequency is the number of waveform cycles per second, measured in hertz. A 440 Hz oscillator repeats 440 times each second. TiSynthEngine accepts a frequency directly or converts a note name for you:
37
41
 
38
42
  ```javascript
39
43
  synth.playTone({ frequency: 440 })
40
44
  synth.playTone({ note: 'A4' })
41
45
  ```
42
46
 
43
- Both calls have the same pitch. Note names use an octave number: `C4` is middle
44
- C, `C5` is one octave higher, and `C3` is one octave lower. Moving up one
45
- octave doubles frequency; moving down halves it.
47
+ Both calls have the same pitch. Note names use an octave number: `C4` is middle C, `C5` is one octave higher, and `C3` is one octave lower. Moving up one octave doubles frequency; moving down halves it.
46
48
 
47
- The note parser accepts English letters and solfege. Sharps use `#`; flats use
48
- a lowercase `b`.
49
+ The note parser accepts English letters and solfege. Sharps use `#`; flats use a lowercase `b`.
49
50
 
50
51
  ```text
51
52
  C4 C#4 Db4 D4
52
53
  DO4 DO#4 REb4 RE4
53
54
  ```
54
55
 
55
- When no octave is present, octave 4 is used. The supported octave digit is 0
56
- through 9, subject to the current frequency ceiling.
56
+ When no octave is present, octave 4 is used. The supported octave digit is 0 through 9, subject to the current frequency ceiling.
57
57
 
58
- The tuning system is twelve-tone equal temperament with A4 at 440 Hz. Each
59
- semitone multiplies frequency by the twelfth root of two:
58
+ The tuning system is twelve-tone equal temperament with A4 at 440 Hz. Each semitone multiplies frequency by the twelfth root of two:
60
59
 
61
60
  ```text
62
61
  frequency = 440 * 2 ^ ((midiNote - 69) / 12)
63
62
  ```
64
63
 
65
- Equal temperament keeps intervals consistent in every key. It also means a
66
- major third is the practical keyboard interval, not a pure 5:4 frequency
67
- ratio. That distinction rarely matters for game or interface sounds.
64
+ Equal temperament keeps intervals consistent in every key. It also means a major third is the practical keyboard interval, not a pure 5:4 frequency ratio. That distinction rarely matters for game or interface sounds.
68
65
 
69
66
  ## Waveform and timbre
70
67
 
71
- Pitch says how fast the oscillator repeats. Waveform says how it moves within
72
- each cycle, which determines the harmonic content and perceived timbre.
68
+ Pitch says how fast the oscillator repeats. Waveform says how it moves within each cycle, which determines the harmonic content and perceived timbre.
73
69
 
74
70
  ### Sine
75
71
 
76
- `SINE` contains the fundamental and no intentional overtones. It works for
77
- clean beeps, soft bass notes and sounds that should stay out of the way. On a
78
- small speaker, very low sines may be quiet or may excite the speaker's own
79
- distortion because there are no upper harmonics to carry the pitch.
72
+ `SINE` contains the fundamental and no intentional overtones. Use it for clean beeps, soft bass notes and unobtrusive sounds. On a small speaker, very low sines may be quiet or may excite the speaker's own distortion because there are no upper harmonics to carry the pitch.
80
73
 
81
74
  ### Square
82
75
 
83
- `SQUARE` contains strong odd harmonics. It sounds hollow, bright and electronic.
84
- It is a good starting point for retro game cues and clear melodic prompts. The
85
- extra high-frequency energy makes it more assertive than a sine at the same
86
- pitch.
76
+ `SQUARE` contains strong odd harmonics. It sounds hollow, bright and electronic. Try it for retro game cues and clear melodic prompts. The extra high-frequency energy makes it more assertive than a sine at the same pitch.
87
77
 
88
78
  ### Sawtooth
89
79
 
90
- `SAWTOOTH` contains both even and odd harmonics. It is the densest pitched
91
- waveform in the module and reads as buzzy or brassy. Use it for alarms, arcade
92
- leads and short sweeps. Square and sawtooth are band-limited with PolyBLEP to
93
- reduce aliasing at discontinuities.
80
+ `SAWTOOTH` contains both even and odd harmonics. It is the densest pitched waveform in the module and reads as buzzy or brassy. Use it for alarms, arcade leads and short sweeps. Square and sawtooth are band-limited with PolyBLEP to reduce aliasing at discontinuities.
94
81
 
95
82
  ### Triangle
96
83
 
97
- `TRIANGLE` contains odd harmonics that fall off faster than a square wave. It
98
- has more body than a sine without the hard edge of a square. It suits gentle
99
- melodies, pads and xylophone-like sketches.
84
+ `TRIANGLE` contains odd harmonics that fall off faster than a square wave. It has more body than a sine without the hard edge of a square. It suits gentle melodies, pads and xylophone-like sketches.
100
85
 
101
86
  ### Noise
102
87
 
103
- `NOISE` produces white noise rather than a repeating pitched cycle. Its
104
- `frequency` is accepted because every sound event uses the same contract, but
105
- changing that value does not tune the noise. A short envelope turns it into a
106
- click, hat or impact layer; a long envelope creates wind or static.
88
+ `NOISE` produces white noise rather than a repeating pitched cycle. Its `frequency` is accepted because every sound event uses the same contract, but changing that value does not tune the noise. A short envelope turns it into a click, hat or impact layer; a long envelope creates wind or static.
89
+
90
+ `noiseRate` changes the source into variable-rate sample-and-hold noise. Zero keeps the white-noise default. Lower rates hold each pseudo-random value longer and sound grainier. `noiseRateEnd` moves the rate over the event.
91
+
92
+ ### Wavetable
107
93
 
108
- Try every waveform at a moderate level before choosing one:
94
+ `WAVETABLE` repeats 2 to 64 values supplied in `waveTable`. Values range from -1 to 1 and are read as steps, without interpolation. Short asymmetric tables are useful for coarse DAC-like voices, buzzy radio tones and original retro effects that do not fit the four built-in oscillator shapes.
109
95
 
110
96
  ```javascript
111
- ;[WAVE.SINE, WAVE.SQUARE, WAVE.SAWTOOTH, WAVE.TRIANGLE, WAVE.NOISE]
112
- .forEach(function (waveType, index) {
113
- setTimeout(function () {
114
- synth.playTone({
115
- note: 'A4',
116
- waveType: waveType,
117
- duration: 450,
118
- attack: 8,
119
- release: 120,
120
- volume: 0.65
121
- })
122
- }, index * 550)
123
- })
97
+ synth.playTone({
98
+ note: 'D4',
99
+ duration: 600,
100
+ waveType: WAVE.WAVETABLE,
101
+ waveTable: [-1, -0.8, -0.1, 0.9, 0.45, 0.1, -0.25, -0.6],
102
+ bitDepth: 8
103
+ })
104
+ ```
105
+
106
+ Try the built-in sources at a moderate level before choosing one:
107
+
108
+ ```javascript
109
+ synth.playSequence({ timeUnit: 'ms', stepDuration: 550,
110
+ voice: { attack: 8, release: 120, volume: 0.65 },
111
+ steps: [WAVE.SINE, WAVE.SQUARE, WAVE.SAWTOOTH, WAVE.TRIANGLE, WAVE.NOISE]
112
+ .map(function (waveType) {
113
+ return { notes: [69], length: 330, overrides: { waveType: waveType } }
114
+ })
115
+ })
116
+ ```
117
+
118
+ ## Filter, pulse width and quantization
119
+
120
+ `lowPassHz` enables a per-voice one-pole low-pass filter. It is most obvious on noise, square and sawtooth sources. Zero bypasses the filter. For resonance, high-pass and band-pass shapes or a moving cutoff, use the [resonant filter](#resonant-filter) below.
121
+
122
+ `pulseWidth` controls how much of each square-wave cycle stays high. The 0.5 default is a normal square. Values closer to 0.05 or 0.95 become thinner and brighter. `pulseWidthEnd` creates a linear duty-cycle sweep.
123
+
124
+ ```javascript
125
+ synth.playTone({
126
+ note: 'A3',
127
+ duration: 700,
128
+ release: 180,
129
+ waveType: WAVE.SQUARE,
130
+ pulseWidth: 0.1,
131
+ pulseWidthEnd: 0.86,
132
+ lowPassHz: 4200
133
+ })
124
134
  ```
125
135
 
136
+ `bitDepth` rounds the oscillator or noise amplitude to a fixed number of levels before filtering. Values from 2 to 16 are accepted. Zero preserves the floating-point signal. Quantization adds harmonic grit; it does not change the audio device's output format.
137
+
126
138
  ## Envelope and total duration
127
139
 
128
140
  Every event uses an attack, an implicit sustain section and a release:
@@ -132,16 +144,11 @@ silence -> attack -> full level -> release -> silence
132
144
  |<---------- duration ---------->|
133
145
  ```
134
146
 
135
- `duration` is the total time, not the sustain time. If duration is 500 ms,
136
- attack is 50 ms and release is 100 ms, the full-level middle lasts 350 ms.
147
+ `duration` is the total time, not the sustain time. If duration is 500 ms, attack is 50 ms and release is 100 ms, the full-level middle lasts 350 ms.
137
148
 
138
- Short attacks feel immediate. A 0 or 1 ms attack can be useful for percussion,
139
- but an abrupt start at an unlucky phase can sound like a click. Values between
140
- 5 and 15 ms keep most game feedback crisp without a hard edge.
149
+ Short attacks feel immediate. A 0 or 1 ms attack can be useful for percussion, but an abrupt start at an unlucky phase can sound like a click. Values between 5 and 15 ms keep most game feedback crisp without a hard edge.
141
150
 
142
- Long attacks soften the onset. Use 100 to 500 ms for pads and slow warnings.
143
- Release controls how the sound leaves: 20 to 80 ms feels tight, while several
144
- hundred milliseconds leaves a tail.
151
+ Long attacks soften the onset. Use 100 to 500 ms for pads and slow warnings. Release controls how the sound leaves: 20 to 80 ms feels tight, while several hundred milliseconds leaves a tail.
145
152
 
146
153
  Explicit attack and release values must fit. These relationships are required:
147
154
 
@@ -150,13 +157,31 @@ attack < duration
150
157
  release <= duration - attack
151
158
  ```
152
159
 
153
- Defaults are adjusted internally if a very short duration leaves no room, but
154
- values supplied by the caller are never reshaped silently.
160
+ Defaults are adjusted internally if a very short duration leaves no room, but values supplied by the caller are never reshaped silently.
161
+
162
+ ## Decay and sustain
163
+
164
+ Plucked strings, struck keys and mallets start loud and settle. A flat envelope cannot do that, so `decay` and `sustain` add a stage after the attack: the level falls from full scale to `sustain` over `decay` milliseconds, then holds until the release.
165
+
166
+ ```text
167
+ decay: 0 flat tone, the default
168
+ decay: 260, sustain: 0.18 pluck: loud start, settles low
169
+ decay: 2000, sustain: 0.02 bell or mallet: fades by itself
170
+ ```
171
+
172
+ Practical starting regions:
173
+
174
+ - plucks and keys: attack 2 to 10 ms, decay 100 to 400 ms, sustain 0.1 to 0.4;
175
+ - struck percussion with a pitch drop: decay about half the duration, sustain near 0.3;
176
+ - bells and mallets: decay over most of the duration, sustain near 0.
177
+
178
+ `sustain` below 1 needs a nonzero `decay`. Without one the level would jump instantly, which clicks, so the call is rejected with `sustain_without_decay`.
179
+
180
+ The release scales the level the note has reached. If the release begins while the decay is still running, the note leaves smoothly from that point. The decay still has to fit: `attack` plus `decay` cannot exceed `duration`, and the 50 ms default attack counts. Set a short `attack` explicitly for a tight pluck.
155
181
 
156
182
  ## Pan and movement
157
183
 
158
- `pan` ranges from -1 on the left to 1 on the right. Zero is centered. Stereo
159
- movement is a straight interpolation from `pan` to `panEnd` over the event.
184
+ `pan` ranges from -1 on the left to 1 on the right. Zero is centered. Stereo movement is a straight interpolation from `pan` to `panEnd` over the event.
160
185
 
161
186
  ```javascript
162
187
  synth.playTone({
@@ -170,11 +195,9 @@ synth.playTone({
170
195
  })
171
196
  ```
172
197
 
173
- Phone speakers are often mono or close together, so pan may be subtle. Treat it
174
- as extra detail rather than the only cue that distinguishes two sounds.
198
+ Phone speakers are often mono or close together, so pan may be subtle. Treat it as extra detail rather than the only cue that distinguishes two sounds.
175
199
 
176
- `frequencyEnd` makes a linear pitch sweep. Use an upward sweep for activation
177
- or reward and a downward sweep for dismissal, power-down or impact.
200
+ `frequencyEnd` makes a linear pitch sweep. Use an upward sweep for activation or reward and a downward sweep for dismissal, power-down or impact.
178
201
 
179
202
  ```javascript
180
203
  synth.playTone({
@@ -190,8 +213,7 @@ synth.playTone({
190
213
 
191
214
  ## Vibrato and the LFO
192
215
 
193
- The low-frequency oscillator modulates pitch. `lfoFreq` is the modulation rate
194
- in cycles per second, and `lfoDepth` is the maximum pitch movement in hertz.
216
+ The low-frequency oscillator modulates pitch. `lfoFreq` is the modulation rate in cycles per second, and `lfoDepth` is the maximum pitch movement in hertz.
195
217
 
196
218
  ```javascript
197
219
  synth.playTone({
@@ -205,16 +227,70 @@ synth.playTone({
205
227
  })
206
228
  ```
207
229
 
208
- A rate around 4 to 7 Hz with a small depth sounds like musical vibrato. A fast
209
- rate or large depth becomes an effect. Because depth is measured in hertz, 8 Hz
210
- is a larger musical interval on a low note than on a high note.
230
+ A rate around 4 to 7 Hz with a small depth sounds like musical vibrato. A fast rate or large depth becomes an effect. Because depth is measured in hertz, 8 Hz is a larger musical interval on a low note than on a high note.
231
+
232
+ ## Resonant filter
233
+
234
+ `filterType` adds a filter that removes or emphasizes part of the spectrum. It has 12 dB per octave of slope, twice as steep as the one-pole `lowPassHz` filter, and can be combined with it (`lowPassHz` runs first).
235
+
236
+ - `lowpass` darkens the sound. A sawtooth through a closing low-pass reads as a zap or a sound moving away; an opening one reads as a swell.
237
+ - `highpass` thins the sound by removing body. It makes hats, hiss and tinny effects out of noise.
238
+ - `bandpass` keeps one band around `filterHz`, which gives vocal, nasal and wah-like colors.
239
+
240
+ Filters shape harmonics, so give them a source that has some: `SAWTOOTH`, `SQUARE` and `NOISE` respond strongly. A sine has only its fundamental, so a filter mostly changes its level.
241
+
242
+ `filterQ` is the resonance. The default 0.7071 is flat. Values from 2 to 4 add character, and above about 6 the filter whistles around the cutoff. A low-pass or high-pass boosts the cutoff region by about `filterQ`, so Q 10 is roughly 20 dB. Lower the event `volume` as Q goes up. The limiter catches peaks, but a resonance that keeps hitting it makes the whole mix pump. The band-pass keeps unity gain at its center for every Q and only narrows.
243
+
244
+ `filterHzEnd` moves the cutoff over the whole event. The motion is exponential, so each second covers the same musical distance. Set it above `filterHz` to open the filter and below to close it. Zero disables the sweep.
245
+
246
+ ```javascript
247
+ synth.playTone({
248
+ note: 'A2',
249
+ duration: 1000,
250
+ attack: 10,
251
+ release: 200,
252
+ volume: 0.5,
253
+ waveType: WAVE.SAWTOOTH,
254
+ filterType: 'bandpass',
255
+ filterHz: 350,
256
+ filterHzEnd: 2400,
257
+ filterQ: 5
258
+ })
259
+ ```
260
+
261
+ ## Frequency modulation
262
+
263
+ FM makes rich, bright and metallic sounds from a plain sine. A second sine, the modulator, bends the pitch of the first one, the carrier. Turn it on with `fmDepth` or `fmDepthEnd`, on a `SINE` voice.
264
+
265
+ - `fmRatio` is the modulator frequency as a multiple of the carrier's. Whole numbers (1, 2, 3) keep the added overtones on the harmonic series, so the pitch stays clear and the sound reads as organ- or piano-like. Fractional ratios such as 1.4142 or 3.5 put the overtones between harmonics, which reads as bells, glass and metal.
266
+ - `fmDepth` is the modulation index and works like a brightness control. Around 0.5 to 1.5 it warms a sine, 2 to 5 makes it bright and rich, and values near 10 get harsh.
267
+ - `fmDepthEnd` moves the index over the event. Starting high and ending low gives a bright attack that mellows, which is the signature of electric pianos, bells and mallets. Pair it with `decay` so the level falls with the brightness.
268
+
269
+ FM does not change the sine's amplitude, so gain staging is the same as for a plain sine. There is no band-limiting: a high carrier with a large index and ratio puts sidebands above the Nyquist frequency, where they alias into inharmonic noise. Keep bell carriers modest, around 500 Hz, or lower the index.
270
+
271
+ ## Detune
272
+
273
+ `detune` shifts a pitch by cents, hundredths of a semitone. Two tones a few cents apart drift in and out of phase and beat at a rate close to `frequency * (2 ^ (cents / 1200) - 1)`. A 12-cent gap at 440 Hz beats about 3 times a second. Slow beating (0.5 to 4 Hz) sounds warm and wide, like a chorus; past about 20 cents it starts to sound out of tune.
274
+
275
+ Use `detune` on a single tone to vary the pitch slightly between repetitions. The effect comes from stacking detuned copies. In a chord, give `detunes` one entry per pitch and repeat a pitch to thicken it:
276
+
277
+ ```javascript
278
+ synth.playChord({
279
+ frequencies: [440, 440],
280
+ detunes: [-6, 6],
281
+ duration: 2000,
282
+ attack: 200,
283
+ release: 600,
284
+ volume: 0.55,
285
+ waveType: WAVE.SINE
286
+ })
287
+ ```
288
+
289
+ Each repeated pitch is a voice, so size `maxVoices` for the full stack. Protected profiles divide per-voice source gain by the square root of the event voice count. This reduces buildup; it does not guarantee equal loudness. Correlated voices can still reinforce each other, and detuned voices can produce peaks as their phases align. `raw` does not apply this event normalization.
211
290
 
212
291
  ## Chords, polyphony and gain
213
292
 
214
- `playChord` is for notes that should start together. The call is placed in the
215
- native queue as one event, so the audio callback sees the whole chord or none
216
- of it. Its source gain is divided by the square root of the chord's voice count
217
- to keep power stable.
293
+ `playChord` is for notes that should start together. The call is placed in the native queue as one event, so the audio callback sees the whole chord or none of it. In protected profiles, per-voice source gain is divided by the square root of the chord's voice count. This approximately stabilizes power for uncorrelated voices, not coherent copies; it does not guarantee constant peaks or perceived loudness. `raw` bypasses this normalization.
218
294
 
219
295
  ```javascript
220
296
  synth.playChord({
@@ -228,42 +304,28 @@ synth.playChord({
228
304
  })
229
305
  ```
230
306
 
231
- Four separate `playTone` calls are four independent full-gain events. They are
232
- not atomic and may start in different audio callbacks. Their sum is also hotter
233
- and can push the limiter. Separate calls are correct for unrelated taps or
234
- effects; use `playChord` when the app already knows the pitches are one chord.
307
+ Four separate `playTone` calls are four independent full-gain events. They are not atomic and may start in different audio callbacks. Their sum is also hotter and can push the limiter. Separate calls are correct for unrelated taps or effects; use `playChord` when the app already knows the pitches are one chord.
235
308
 
236
- `maxVoices` is capacity, not a loudness control. When the pool is full, the
237
- voice with the least time remaining is faded and reused. Leave a small margin for release tails, but do
238
- not set 32 by habit when the app can only make four sounds at once.
309
+ `maxVoices` sets voice capacity without changing loudness. Timed-only pools replace the least remaining duration; held/transport pools prefer a releasing voice, then the oldest attack. Leave a small margin for release tails, but do not set 32 by habit when the app can only make four sounds at once.
239
310
 
240
311
  ## Mixing profiles and limiter
241
312
 
242
- Choose the profile once in `startEngine()`:
313
+ Choose the profile once in `start()`:
243
314
 
244
- | Profile | Practical use |
245
- | --- | --- |
246
- | `conservative` | Default for mixed effects, chords and unpredictable overlap. |
247
- | `balanced` | More level for mostly monophonic sounds. |
248
- | `speakerSafe` | Lower source gain and a 0.70 ceiling for small speakers. |
249
- | `raw` | Full source gain; useful only when the app owns all gain staging. |
315
+ | Profile | Practical use |
316
+ | -------------- | ----------------------------------------------------------------- |
317
+ | `conservative` | Default for mixed effects, chords and unpredictable overlap. |
318
+ | `balanced` | More level for mostly monophonic sounds. |
319
+ | `speakerSafe` | Lower source gain and a 0.70 ceiling for small speakers. |
320
+ | `raw` | Full source gain; useful only when the app owns all gain staging. |
250
321
 
251
- Protected profiles calibrate source gain by waveform and event voice count.
252
- The final stereo-linked limiter catches exceptional peaks with 10 ms of
253
- lookahead and a 100 ms release. Its ceiling is 0.85, or 0.70 for `speakerSafe`.
322
+ Protected profiles calibrate source gain by waveform and event voice count. The final stereo-linked limiter catches exceptional peaks with 10 ms of lookahead and a 100 ms release. Its ceiling is 0.85, or 0.70 for `speakerSafe`.
254
323
 
255
- The limiter is protection, not a loudness effect. If it works on every note,
256
- lower event volume or choose a more conservative profile. Repeatedly starting
257
- the same long sound before its previous release ends raises and lowers the sum,
258
- which can produce audible pumping. That overlap is useful as a stress test, but
259
- it is a poor reference for the tone's normal quality.
324
+ The limiter protects against peaks. If it engages on every note, lower event volume or choose a more conservative profile instead of using it to raise loudness. Repeatedly starting the same long sound before its previous release ends raises and lowers the sum, which can produce audible pumping. That overlap is useful as a stress test, but it is a poor reference for the tone's normal quality.
260
325
 
261
326
  ## Speakers, headphones and devices
262
327
 
263
- Headphones usually reproduce bass and stereo movement more cleanly than a phone
264
- speaker. A sound that is smooth in headphones can buzz on a small driver even
265
- when the digital signal never clips. Low pitches and square or sawtooth waves
266
- are the most demanding.
328
+ Headphones usually reproduce bass and stereo movement more cleanly than a phone speaker. A sound that is smooth in headphones can buzz on a small driver even when the digital signal never clips. Low pitches and square or sawtooth waves are the most demanding.
267
329
 
268
330
  For speaker-facing sounds:
269
331
 
@@ -273,15 +335,11 @@ For speaker-facing sounds:
273
335
  - test at several system volume settings on the target device;
274
336
  - keep critical meaning in rhythm and pitch, not pan alone.
275
337
 
276
- Different phones have different sample rates, speaker protection and acoustic
277
- resonances. Test the real device family. A desktop browser or simulator cannot
278
- answer whether a small speaker will rattle.
338
+ Different phones have different sample rates, speaker protection and acoustic resonances. Test the real device family. A desktop browser or simulator cannot answer whether a small speaker will rattle.
279
339
 
280
340
  ## Recipes
281
341
 
282
- These recipes use only the 1.0.0 contract and keep explicit envelopes valid.
283
- Tune pitch and volume for the app's other audio rather than treating the values
284
- as mastering targets.
342
+ These recipes use only the 1.0.0 contract and keep explicit envelopes valid. Adjust pitch and volume to fit the app's other audio; the recipe values are starting points, not mastering targets.
285
343
 
286
344
  ### Interface click
287
345
 
@@ -296,8 +354,7 @@ synth.playTone({
296
354
  })
297
355
  ```
298
356
 
299
- The short sine is cleaner than a full-band noise click. Raise the frequency for
300
- a lighter control or lower it for a heavier one.
357
+ The short sine is cleaner than a full-band noise click. Raise the frequency for a lighter control or lower it for a heavier one.
301
358
 
302
359
  ### Confirmation hit
303
360
 
@@ -330,18 +387,16 @@ synth.playChord({
330
387
  ### Two-tone alarm
331
388
 
332
389
  ```javascript
333
- synth.playPattern({
334
- bpm: 150,
335
- noteValue: '8n',
336
- attack: 8,
337
- release: 70,
338
- volume: 0.55,
339
- waveType: WAVE.SAWTOOTH,
390
+ synth.setTempo({ bpm: 150 })
391
+ synth.playSequence({
392
+ timeUnit: 'beats',
393
+ stepDuration: 0.5,
394
+ voice: { attack: 8, release: 70, volume: 0.55, waveType: WAVE.SAWTOOTH },
340
395
  steps: [
341
- { note: 'A4' },
342
- { note: 'E5' },
343
- { note: 'A4' },
344
- { note: 'E5' }
396
+ { notes: [69] },
397
+ { notes: [76] },
398
+ { notes: [69] },
399
+ { notes: [76] }
345
400
  ]
346
401
  })
347
402
  ```
@@ -349,50 +404,198 @@ synth.playPattern({
349
404
  ### Retro pickup
350
405
 
351
406
  ```javascript
352
- synth.playPattern({
353
- bpm: 180,
407
+ synth.setTempo({ bpm: 180 })
408
+ synth.playSequence({
409
+ timeUnit: 'beats',
410
+ stepDuration: 0.25,
354
411
  swing: 0.06,
355
- noteValue: '16n',
356
- attack: 2,
357
- release: 45,
358
- volume: 0.5,
359
- waveType: WAVE.SQUARE,
412
+ voice: { attack: 2, release: 45, volume: 0.5, waveType: WAVE.SQUARE },
360
413
  steps: [
361
- { note: 'C5', duration: 80 },
362
- { note: 'E5', duration: 80 },
363
- { note: 'G5', duration: 80 },
364
- { note: 'C6', duration: 160, noteValue: '8n' }
414
+ { notes: [72] },
415
+ { notes: [76] },
416
+ { notes: [79] },
417
+ { notes: [84], duration: 0.5 }
365
418
  ]
366
419
  })
367
420
  ```
368
421
 
369
- The pattern root does not accept `duration`. Put it on each sounding step when
370
- you want a gate shorter than its rhythmic step.
422
+ The first three intervals are sixteenth notes and the last is an eighth note. Gate sets when release begins; `length` overrides that note duration independently of the next attack.
371
423
 
372
424
  ### Noise percussion
373
425
 
374
426
  ```javascript
375
- synth.playPattern({
376
- bpm: 112,
377
- noteValue: '8n',
378
- waveType: WAVE.NOISE,
379
- volume: 0.34,
427
+ synth.setTempo({ bpm: 112 })
428
+ synth.playSequence({
429
+ timeUnit: 'beats',
430
+ stepDuration: 0.5,
431
+ voice: { waveType: WAVE.NOISE, volume: 0.34 },
380
432
  steps: [
381
- { frequency: 220, duration: 65, attack: 1, release: 55 },
433
+ { frequencies: [220], overrides: { attack: 1, decay: 65, sustain: 0, release: 55 } },
382
434
  { rest: true },
383
- { frequency: 220, duration: 35, attack: 1, release: 28 },
384
- { frequency: 220, duration: 35, attack: 1, release: 28 }
435
+ { frequencies: [220], overrides: { attack: 1, decay: 35, sustain: 0, release: 28 } },
436
+ { frequencies: [220], overrides: { attack: 1, decay: 35, sustain: 0, release: 28 } }
385
437
  ]
386
438
  })
387
439
  ```
388
440
 
389
- Noise steps still need `frequency` or `note` because every sounding pattern
390
- step follows the same shape. The pitch value is not used by the noise source.
441
+ The steps keep the eighth-note grid while short decay and `sustain: 0` let each hit settle to silence inside its step.
442
+
443
+ Noise steps still need `frequencies` or `notes` because every sounding pattern step follows the same shape. The pitch value is not used by the noise source.
444
+
445
+ ### Filtered noise burst
446
+
447
+ ```javascript
448
+ synth.playTone({
449
+ frequency: 120,
450
+ duration: 760,
451
+ attack: 1,
452
+ release: 580,
453
+ volume: 0.62,
454
+ waveType: WAVE.NOISE,
455
+ noiseRate: 12000,
456
+ noiseRateEnd: 650,
457
+ lowPassHz: 2500,
458
+ bitDepth: 8
459
+ })
460
+ ```
461
+
462
+ The decreasing update rate makes the texture break into slower chunks while the envelope removes energy. Reverse the two rates for an effect that becomes more frantic.
463
+
464
+ ### Pluck
465
+
466
+ ```javascript
467
+ synth.playTone({
468
+ note: 'E4',
469
+ duration: 900,
470
+ attack: 2,
471
+ decay: 260,
472
+ sustain: 0.18,
473
+ release: 380,
474
+ volume: 0.6,
475
+ waveType: WAVE.TRIANGLE
476
+ })
477
+ ```
478
+
479
+ ### Electric piano
480
+
481
+ ```javascript
482
+ synth.playTone({
483
+ note: 'E3',
484
+ duration: 1800,
485
+ attack: 3,
486
+ decay: 700,
487
+ sustain: 0.3,
488
+ release: 600,
489
+ volume: 0.55,
490
+ waveType: WAVE.SINE,
491
+ fmRatio: 1,
492
+ fmDepth: 2.2,
493
+ fmDepthEnd: 0.25
494
+ })
495
+ ```
496
+
497
+ The index falls from 2.2 to 0.25 while the level settles, so the attack is bright and the sustain is mellow.
498
+
499
+ ### Bell
500
+
501
+ ```javascript
502
+ synth.playTone({
503
+ note: 'C5',
504
+ duration: 2600,
505
+ attack: 1,
506
+ decay: 2000,
507
+ sustain: 0.02,
508
+ release: 500,
509
+ volume: 0.5,
510
+ waveType: WAVE.SINE,
511
+ fmRatio: 3.5,
512
+ fmDepth: 4,
513
+ fmDepthEnd: 0
514
+ })
515
+ ```
516
+
517
+ The fractional ratio makes the overtones inharmonic. Try 1.4142 or 2.76 for a different metal.
518
+
519
+ ### Wah
520
+
521
+ The wah in [Resonant filter](#resonant-filter) is also a recipe. Reverse the two cutoffs for a sound that closes instead of opening.
522
+
523
+ ### Filtered laser
524
+
525
+ ```javascript
526
+ synth.playTone({
527
+ frequency: 1600,
528
+ frequencyEnd: 160,
529
+ duration: 340,
530
+ attack: 2,
531
+ release: 90,
532
+ volume: 0.4,
533
+ waveType: WAVE.SAWTOOTH,
534
+ filterType: 'lowpass',
535
+ filterHz: 7000,
536
+ filterHzEnd: 500,
537
+ filterQ: 4
538
+ })
539
+ ```
540
+
541
+ ### Hi-hat
542
+
543
+ ```javascript
544
+ synth.playTone({
545
+ duration: 80,
546
+ attack: 1,
547
+ decay: 30,
548
+ sustain: 0.25,
549
+ release: 45,
550
+ volume: 0.45,
551
+ waveType: WAVE.NOISE,
552
+ filterType: 'highpass',
553
+ filterHz: 7000
554
+ })
555
+ ```
556
+
557
+ For an open hat, use `duration: 320`, `decay: 70` and `release: 220`.
558
+
559
+ ### Thick pad
560
+
561
+ ```javascript
562
+ synth.playChord({
563
+ notes: ['A3', 'E4', 'A3', 'E4', 'A3', 'E4'],
564
+ detunes: [-9, -9, 0, 0, 9, 9],
565
+ duration: 3200,
566
+ attack: 700,
567
+ release: 1300,
568
+ volume: 0.5,
569
+ waveType: WAVE.SAWTOOTH,
570
+ filterType: 'lowpass',
571
+ filterHz: 1500,
572
+ panSpread: 0.8
573
+ })
574
+ ```
575
+
576
+ Six voices play two pitches, each in three slightly different tunings. The low-pass takes the harshness off the sawtooth stack. Start the engine with `maxVoices` of at least 6.
577
+
578
+ ### Kick
579
+
580
+ ```javascript
581
+ synth.playTone({
582
+ frequency: 150,
583
+ frequencyEnd: 42,
584
+ duration: 320,
585
+ attack: 1,
586
+ decay: 150,
587
+ sustain: 0.3,
588
+ release: 140,
589
+ volume: 0.85,
590
+ waveType: WAVE.SINE
591
+ })
592
+ ```
593
+
594
+ The pitch drop is the punch and the decay is the thump. A phone speaker cannot reproduce 42 Hz, so raise `frequencyEnd` toward 70 for speaker playback.
391
595
 
392
596
  ### Simon-style cues
393
597
 
394
- Use short, distinct square-wave pitches. Play one tone for the sequence and
395
- one atomic chord only when demonstrating all pads together.
598
+ Use short, distinct square-wave pitches. Play one tone for the sequence and one atomic chord only when demonstrating all pads together.
396
599
 
397
600
  ```javascript
398
601
  var SIMON = [329.63, 440, 554.37, 659.25]
@@ -421,8 +624,25 @@ function playAllSimonPads () {
421
624
  }
422
625
  ```
423
626
 
424
- For more complete code, continue with [Examples](examples.md) and
425
- [Curated sound and interaction recipes](recipes.md). The latter includes retro
426
- effects, a playable xylophone, memory-pad timbres, layered chords and timer
427
- ownership. For gain and device-testing advice, see
428
- [Recommendations](recommendations.md).
627
+ See [Examples](examples.md) for complete code and [Recommendations](recommendations.md) for gain and device-testing advice.
628
+
629
+ ## Holding a sound and adding movement
630
+
631
+ Choose a held voice when the ending follows a player action instead of a fixed duration: motor, charge beam, pad or the last chord of a ceremony. `noteOn` starts a positive app-owned ID; `noteOff` begins release from the current level, and compatible `updateVoice` edits ramp over 10 ms. Distinct IDs remain independent events with fixed gain. Reserve enough capacity for their releases and unrelated effects, and release on cancellation as well as ordinary key up. See the [held motor recipe](advanced-examples.md#sustained-game-motor) for retry/cleanup handling.
632
+
633
+ A fanfare can use beat or millisecond sequences with native chords, rests, gate, independent length, swing and pitch-ordered arpeggios. A long release can ring over later steps. Use held voices when release follows presentation or interaction. See [transport usage](guide.md#choose-the-musical-transport).
634
+
635
+ The two additional voice LFOs move filter cutoff, square PWM or sine FM index using sine/triangle shapes. Cutoff movement creates slow breathing or rhythmic filtering; PWM adds changing harmonic color; a small FM-index modulation varies brightness. Free rates reach 20 Hz, and synced rates follow native transport tempo. These slots are separate from pitch vibrato. Avoid deep/high-rate modulation with strong resonance or high-frequency FM; safe parameter clamping does not guarantee a pleasant or alias-free sound. The [two-LFO example](advanced-examples.md#two-voice-lfos) shows a filtered PWM pad.
636
+
637
+ ## Native space and creative drive
638
+
639
+ | Processor | What changes | Useful starting region |
640
+ | --- | --- | --- |
641
+ | Stereo chorus | A short interpolated delay with offset LFO phases adds width and movement. | Rate 0.3 to 0.8 Hz, depth 0.15 to 0.35, mix 0.08 to 0.2. |
642
+ | Ping-pong delay | Alternating echoes create rhythmic or spatial repeats. | 180 to 350 ms or synced eighths, feedback 0.15 to 0.35, mix 0.08 to 0.18. |
643
+ | Stereo reverb | Damped comb/allpass reflections create a diffuse decay. | Decay 0.8 to 1.8 seconds, damping 0.5 to 0.7, mix 0.08 to 0.18. |
644
+ | Optional saturation | Soft waveshaping adds harmonics and changes attack/timbre. | Drive 1.2 to 1.6 with a small blend; compare at matched levels. |
645
+
646
+ All processors are off initially, with input gain 1. Enable one shared chain through `setMasterProcessing`; select `applyTo` categories such as `music` and `ambience` to keep explosions dry. Per-voice `effectSend` overrides that category selection. [Configuration](advanced-examples.md#chorus-delay-reverb-and-creative-saturation) and [exact ranges](api.md#setmasterprocessingoptions) explain chorus, delay, reverb and optional saturation.
647
+
648
+ Disabling an effect closes its input gradually and drains the tail before exact bypass; it does not immediately erase its history. Panic invalidates histories. Saturation is a creative choice rather than automatic gain protection; the final limiter stays in place. Match dry/wet comparison levels and watch accumulated limiter reduction so a louder version does not win an A/B comparison merely by level. None of these effects makes a small speaker reproduce missing bass.