@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,19 +1,44 @@
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/guide.md` SHA-256: `dd03392e1d5f89e520f4be70dab2fbad8e86776bf3a60d6af93f0dc0695c8c69`.
2
2
 
3
3
  # Guide
4
4
 
5
- This guide covers the normal path from installing TiSynthEngine to releasing
6
- the native audio resources when the owning screen closes.
5
+ Install TiSynthEngine, start it after layout settles, and release its native audio resources when the owning screen closes.
6
+
7
+ <!-- TOC-START -->
8
+ ## Contents
9
+
10
+ - [Install the module](#install-the-module)
11
+ - [Load and verify the contract](#load-and-verify-the-contract)
12
+ - [Lifecycle](#lifecycle)
13
+ - [Detect required capabilities](#detect-required-capabilities)
14
+ - [Hold and edit a voice](#hold-and-edit-a-voice)
15
+ - [Choose the musical transport](#choose-the-musical-transport)
16
+ - [Add master effects](#add-master-effects)
17
+ - [Play the first sound](#play-the-first-sound)
18
+ - [Choose a pitch](#choose-a-pitch)
19
+ - [Shape a tone](#shape-a-tone)
20
+ - [Settle, filter and modulate](#settle-filter-and-modulate)
21
+ - [Shape noise and waveforms](#shape-noise-and-waveforms)
22
+ - [Play simultaneous notes](#play-simultaneous-notes)
23
+ - [Play a sequence](#play-a-sequence)
24
+ - [Know when a sequence ends](#know-when-a-sequence-ends)
25
+ - [Master level and stopping](#master-level-and-stopping)
26
+ - [Output route changes](#output-route-changes)
27
+ - [Integrate binaural in a 2D app](#integrate-binaural-in-a-2d-app)
28
+ - [Read engine statistics](#read-engine-statistics)
29
+ - [Next steps](#next-steps)
30
+ - [Diagnose mix headroom](#diagnose-mix-headroom)
31
+
32
+ <!-- TOC-END -->
7
33
 
8
34
  ## Install the module
9
35
 
10
- Use the archive for each target platform:
36
+ Download both official packages from [Android 1.0.0](https://github.com/macCesar/TiSynthEngine/releases/tag/android-1.0.0) and [iOS 1.0.0](https://github.com/macCesar/TiSynthEngine/releases/tag/ios-1.0.0). For source builds, the matching archives are produced under `android/dist` and `ios/dist`:
11
37
 
12
38
  - `ti.synthengine-android-1.0.0.zip`
13
39
  - `ti.synthengine-iphone-1.0.0.zip`
14
40
 
15
- Put the archives in the Titanium project root or install them through the
16
- Titanium CLI. Pin the same version for both platforms:
41
+ Put the archives in the Titanium project root or install them through the Titanium CLI. Pin the same version for both platforms:
17
42
 
18
43
  ```xml
19
44
  <modules>
@@ -22,13 +47,11 @@ Titanium CLI. Pin the same version for both platforms:
22
47
  </modules>
23
48
  ```
24
49
 
25
- TiSynthEngine requires Titanium SDK 13.3.1.GA or newer. The iOS renderer
26
- requires iOS 13 or newer.
50
+ TiSynthEngine requires Titanium SDK 13.2.0.GA or newer. Android projects must compile against API 35 or newer. The iOS module requires iOS 15 or newer.
27
51
 
28
52
  ## Load and verify the contract
29
53
 
30
- Require the module once in the code that owns audio. Check `apiVersion` before
31
- starting the engine so a stale native archive fails with a useful message.
54
+ Require the module once in the code that owns audio. Check `apiVersion` before starting. Also check the [spatial limits](#detect-the-installed-feature) and [statistics methods](#read-engine-statistics) if the app needs those features. Check [required capabilities](#detect-required-capabilities) for held voices, musical transport and master processing.
32
55
 
33
56
  ```javascript
34
57
  var synth = require('ti.synthengine')
@@ -39,15 +62,11 @@ if (contract.apiVersion !== '1.0.0') {
39
62
  }
40
63
  ```
41
64
 
42
- `getDefaults()` works before the stream starts. Call it again after startup if
43
- you need the frequency ceiling for input validation, because that ceiling
44
- depends on the output sample rate.
65
+ `getDefaults()` works before the stream starts. Call it again after startup if you need the frequency ceiling for input validation, because that ceiling depends on the output sample rate.
45
66
 
46
67
  ## Lifecycle
47
68
 
48
- Opening a low-latency stream while Titanium lays out the first window can cause
49
- an underrun on some Android devices. Register `postlayout` before `open()`, wait
50
- for a short quiet period, and cancel the timer if the window closes first.
69
+ Opening a low-latency stream while Titanium lays out the first window can cause an underrun on some Android devices. Register `postlayout` before `open()`, wait for a short quiet period, and cancel the timer if the window closes first.
51
70
 
52
71
  ```javascript
53
72
  var running = false
@@ -56,10 +75,12 @@ var startTimer = null
56
75
  function startAudio () {
57
76
  startTimer = null
58
77
  win.removeEventListener('postlayout', onPostLayout)
59
- running = synth.startEngine({
78
+ running = synth.start({
60
79
  maxVoices: 16,
61
- mixingProfile: 'conservative'
80
+ mixingProfile: 'conservative',
81
+ audioSession: 'ambient'
62
82
  })
83
+ if (!running) Ti.API.warn('TiSynthEngine could not start; keep playback controls disabled')
63
84
  }
64
85
 
65
86
  function onPostLayout () {
@@ -87,9 +108,61 @@ win.addEventListener('close', closeAudio)
87
108
  win.open()
88
109
  ```
89
110
 
90
- Use one owner for the engine. Start it once, pass a small audio service to the
91
- screens that need it, and call `shutdown()` from the owner. A second
92
- `startEngine()` call is harmless, but it makes ownership and cleanup unclear.
111
+ Use one owner for the engine. Start it once, pass a small audio service to the screens that need it, and call `shutdown()` from the owner. While the engine is running, another valid `start()` call returns `true` and leaves the existing options in effect. Changing voice capacity, profile or session policy requires `shutdown()` and a new start. This example selects `ambient` for game/UI audio; the default remains `playback`. Android validates the session policy and leaves output unchanged.
112
+
113
+ ## Detect required capabilities
114
+
115
+ Release 1.0.0 includes held voices, sample-clock sequences, stereo effects and two optional LFOs. Require the methods used by each feature before enabling its controls:
116
+
117
+ ```javascript
118
+ function hasMethods (names) {
119
+ return names.every(function (name) { return typeof synth[name] === 'function' })
120
+ }
121
+ var supportsHeld = hasMethods(['noteOn', 'noteOff', 'updateVoice'])
122
+ var supportsSequence = hasMethods(['playSequence', 'updateSequence', 'stopSequence', 'getSequenceState'])
123
+ var supportsProcessing = hasMethods(['setMasterProcessing'])
124
+ ```
125
+
126
+ Start the engine successfully before publishing held notes, sequence edits or processing. `getDefaults` and feature checks do not open the stream. Install matching packages for Android/iOS and rebuild the application when replacing a native module. Additional LFOs require the documented modulation contract; check acceptance of the complete configuration. Treat rejected configurations as unavailable rather than silently dropping their keys.
127
+
128
+ ## Hold and edit a voice
129
+
130
+ Use `noteOn({ voiceId, ... })` when duration follows interaction: a piano key, motor, beam, pad or the last chord of a ceremony. Allocate a unique positive integer ID per attack, and release only that ID with `noteOff`. No duration or sweep-end fields are accepted. A chord held this way consists of separate IDs; unlike one `playChord` or sequence chord, separate `noteOn` calls have independent gain and publication.
131
+
132
+ `updateVoice` changes compatible pitch, volume, cutoff/Q, vibrato, PWM/FM, LFO targets, noise clock, one-pole cutoff, effect send and stereo/spatial position with 10 ms ramps, without restarting oscillator/envelope phase or clearing spatial history. Source, table, filter type and ADSR are fixed at attack. Spatial edits require a voice started spatially; stereo pan edits require a stereo voice. See [complete fields](api.md#updatevoiceoptions). A note that has ended or been stolen accepts updates as a no-op; there is no note-finished event or reserved slot. A `true` return means accepted publication, not proof that the sound is still active.
133
+
134
+ Release on touch end and cancellation as well as ordinary key up. Retry a rejected release twice at 16 ms outside the audio callback, then shut down and report failure if publication remains blocked. Cancel retries during teardown. The [held motor example](advanced-examples.md#sustained-game-motor) implements this ownership policy. For pause/owner close, invalidate app scheduling and shut down; require a new start before future notes.
135
+
136
+ ## Choose the musical transport
137
+
138
+ Use `playSequence` for all successive native sounds. `playTone` and `playChord` handle independent timed events; `noteOn`/`noteOff` hold a sound for interaction. Start with `maxSequences: 1…4` to preallocate independent sequence capacity. All use the same pool of up to 32 voices.
139
+
140
+ Store the positive identifier returned by `playSequence`, and include it in each update, stop or state query. 0 rejects publication. Three banks per sequence publish complete edits atomically. Structural edits enter at the next step; swing at the next pair. `setTempo({ bpm })` applies at the next block, preserving beat position. Beat sequences follow it; ms sequences keep fixed times. `stopSequence({ sequenceId })` releases only that owner's voices and leaves effect tails. A retired identifier returns null from `getSequenceState`.
141
+
142
+ ```javascript
143
+ synth.setTempo({ bpm: 112 })
144
+ var sequenceId = synth.playSequence({
145
+ stepDuration: 0.5, swing: 0.12, bus: 'music',
146
+ voice: { waveType: WAVE.SQUARE, volume: 0.55 },
147
+ steps: [{ notes: [60] }, { notes: [64], overrides: { pan: -0.4, panEnd: 0.4 } },
148
+ { rest: true }, { notes: [67], duration: 1, length: 0.8 }]
149
+ })
150
+ if (sequenceId === 0) throw new Error('Sequence rejected')
151
+ // Stop this owner without affecting unrelated voices:
152
+ synth.stopSequence({ sequenceId: sequenceId })
153
+ ```
154
+
155
+ See [the full sequence contract](api.md#native-sequence-transport) for length, gate, variable durations, arpeggios, revisions and capacity rejection.
156
+
157
+ ## Add master effects
158
+
159
+ The native chain is voices, fixed input gain, chorus, ping-pong delay, stereo Schroeder reverb, master volume, optional saturation, then the final limiter. The initial `inputGain` is 1 and chorus/delay/reverb/saturation are disabled. New notes have no extra LFOs unless requested. This preserves default render behavior without making effects exclusive to instruments or Studio.
160
+
161
+ Chorus adds stereo movement, delay adds free or synchronized echoes, and reverb adds a damped tail. Saturation is optional creative drive. Select processed categories with `applyTo: ['music', 'ambience']`; a voice's explicit `effectSend` overrides that selection. One chain processes the selected share and one final limiter protects the recombined output.
162
+
163
+ `setMasterProcessing` merges partial updates, validates the entire result and returns publication acceptance. Continuous controls ramp over 10 ms. Disabling an effect closes input gradually and keeps its tail, bounded to 30 seconds with a final fade, then restores exact bypass. It does not undo a non-unity input gain. `stopAll` invalidates histories and stops the native sequence; `shutdown` stops the stream and frees its buffers. See the [processing example](advanced-examples.md#chorus-delay-reverb-and-creative-saturation) and [field ranges](api.md#setmasterprocessingoptions).
164
+
165
+ Delay and LFO sync follow global `setTempo` (initially 120); free settings retain milliseconds or hertz. The two extra voice LFOs target filter cutoff, square PWM or sine FM, with sine/triangle waves. They are independent of `lfoFreq`/`lfoDepth` pitch vibrato. See [two LFOs](advanced-examples.md#two-voice-lfos).
93
166
 
94
167
  ## Play the first sound
95
168
 
@@ -106,9 +179,7 @@ var accepted = synth.playTone({
106
179
  if (!accepted) Ti.API.warn('The tone was rejected')
107
180
  ```
108
181
 
109
- Calls that create or change sound return `true` when accepted and `false` when
110
- rejected. During development, handle that result and inspect logcat or the
111
- Xcode console for the structured validation warning.
182
+ Timed and held sound calls and accepted edits return `true` when accepted and `false` when rejected. `playSequence` instead returns a positive sequence ID or `0`; state queries return their documented state or `null`, and lifecycle/cleanup methods follow their own return contracts. During development, handle the relevant result and inspect logcat or the Xcode console for the structured validation warning.
112
183
 
113
184
  ## Choose a pitch
114
185
 
@@ -119,13 +190,11 @@ synth.playTone({ frequency: 440 })
119
190
  synth.playTone({ note: 'A4' })
120
191
  ```
121
192
 
122
- Do not send both. Notes accept English names (`C#4`, `Bb3`) and solfege names
123
- (`DO4`, `FA#5`, `Sib3`). When the octave is omitted, octave 4 is used.
193
+ Do not send both. Notes accept English names (`C#4`, `Bb3`) and solfege names (`DO4`, `FA#5`, `Sib3`). When the octave is omitted, octave 4 is used.
124
194
 
125
195
  ## Shape a tone
126
196
 
127
- The oscillator runs for `duration` milliseconds. `attack` controls the fade in
128
- and `release` controls the fade out; the time between them stays at full level.
197
+ The oscillator runs for `duration` milliseconds. `attack` controls the fade in and `release` controls the fade out; the time between them stays at full level.
129
198
 
130
199
  ```javascript
131
200
  synth.playTone({
@@ -137,9 +206,7 @@ synth.playTone({
137
206
  })
138
207
  ```
139
208
 
140
- Explicit envelope values must fit inside `duration`. An attack equal to or
141
- longer than the duration is rejected, as is a release longer than the time left
142
- after attack.
209
+ Explicit envelope values must fit inside `duration`. An attack equal to or longer than the duration is rejected, as is a release longer than the time left after attack.
143
210
 
144
211
  Tone options can also move during the event:
145
212
 
@@ -157,9 +224,113 @@ synth.playTone({
157
224
  })
158
225
  ```
159
226
 
160
- `frequencyEnd` creates a linear pitch sweep. `panEnd` moves the sound between
161
- channels. `lfoFreq` is the vibrato rate in hertz and `lfoDepth` is its depth in
162
- hertz.
227
+ `frequencyEnd` creates a linear pitch sweep. `panEnd` moves the sound between channels. `lfoFreq` is the vibrato rate in hertz and `lfoDepth` is its depth in hertz.
228
+
229
+ ## Settle, filter and modulate
230
+
231
+ A flat envelope suits beeps. For plucks, keys and bells, add `decay` and `sustain`: the level falls from full scale to `sustain` over `decay` milliseconds and holds there until the release.
232
+
233
+ ```javascript
234
+ synth.playTone({
235
+ note: 'E4',
236
+ duration: 900,
237
+ attack: 2,
238
+ decay: 260,
239
+ sustain: 0.18,
240
+ release: 380,
241
+ waveType: contract.waveTypes.TRIANGLE
242
+ })
243
+ ```
244
+
245
+ `sustain` below 1 needs a nonzero `decay`, and `attack` plus `decay` must fit in `duration`. The 50 ms default attack counts, so set a short `attack` when the decay is long.
246
+
247
+ `filterType` adds a resonant filter: `lowpass`, `highpass` or `bandpass`. `filterHz` is the cutoff, `filterQ` the resonance, and `filterHzEnd` sweeps the cutoff over the note.
248
+
249
+ ```javascript
250
+ synth.playTone({
251
+ note: 'A2',
252
+ duration: 1000,
253
+ release: 200,
254
+ waveType: contract.waveTypes.SAWTOOTH,
255
+ filterType: 'bandpass',
256
+ filterHz: 350,
257
+ filterHzEnd: 2400,
258
+ filterQ: 5
259
+ })
260
+ ```
261
+
262
+ Any `filterHz`, `filterHzEnd` or `filterQ` without a `filterType` other than `none` is rejected with `filter_type_required`.
263
+
264
+ `fmDepth` makes a `SINE` voice an FM voice. `fmRatio` is the modulator frequency as a multiple of the carrier, and `fmDepthEnd` moves the modulation index over the note. Omit `fmDepthEnd` for no sweep; 0 is a real target.
265
+
266
+ ```javascript
267
+ synth.playTone({
268
+ note: 'C5',
269
+ duration: 2600,
270
+ attack: 1,
271
+ decay: 2000,
272
+ sustain: 0.02,
273
+ fmRatio: 3.5,
274
+ fmDepth: 4,
275
+ fmDepthEnd: 0
276
+ })
277
+ ```
278
+
279
+ `detune` shifts a pitch in cents. In a chord, `detunes` gives one offset per pitch, so repeating a note with small offsets thickens it:
280
+
281
+ ```javascript
282
+ synth.playChord({
283
+ notes: ['A3', 'A3', 'E4', 'E4'],
284
+ detunes: [-8, 8, -8, 8],
285
+ duration: 2000,
286
+ waveType: contract.waveTypes.SAWTOOTH
287
+ })
288
+ ```
289
+
290
+ ## Shape noise and waveforms
291
+
292
+ Use `noiseRate` to hold each pseudo-random noise value instead of generating a new one every sample. A low rate sounds sparse and grainy; a high rate moves toward white noise. `noiseRateEnd` sweeps the update rate.
293
+
294
+ ```javascript
295
+ synth.playTone({
296
+ frequency: 120,
297
+ duration: 700,
298
+ attack: 1,
299
+ release: 520,
300
+ waveType: contract.waveTypes.NOISE,
301
+ noiseRate: 11000,
302
+ noiseRateEnd: 650,
303
+ lowPassHz: 2600,
304
+ bitDepth: 8
305
+ })
306
+ ```
307
+
308
+ `lowPassHz` is a per-voice one-pole filter. `bitDepth` applies amplitude quantization from 2 to 16 bits. Zero disables either control.
309
+
310
+ Square waves accept a duty cycle and a duty-cycle sweep:
311
+
312
+ ```javascript
313
+ synth.playTone({
314
+ note: 'A3',
315
+ duration: 650,
316
+ release: 180,
317
+ waveType: contract.waveTypes.SQUARE,
318
+ pulseWidth: 0.12,
319
+ pulseWidthEnd: 0.82
320
+ })
321
+ ```
322
+
323
+ For a caller-defined repeating shape, select `WAVETABLE` and provide 2 to 64 samples from -1 to 1. The table is stepped rather than interpolated.
324
+
325
+ ```javascript
326
+ synth.playTone({
327
+ note: 'D4',
328
+ duration: 500,
329
+ waveType: contract.waveTypes.WAVETABLE,
330
+ waveTable: [-1, -0.8, -0.1, 0.9, 0.4, 0.1, -0.2, -0.6],
331
+ bitDepth: 8
332
+ })
333
+ ```
163
334
 
164
335
  ## Play simultaneous notes
165
336
 
@@ -176,12 +347,9 @@ synth.playChord({
176
347
  })
177
348
  ```
178
349
 
179
- The chord is placed on the audio queue atomically. Its voices also share a
180
- constant-power event gain. Several separate `playTone` calls are independent,
181
- can begin in different callbacks, and are hotter when summed.
350
+ The chord is placed on the audio queue atomically. Its voices also share a constant-power event gain. Several separate `playTone` calls are independent, can begin in different callbacks, and are hotter when summed.
182
351
 
183
- Use `pans` when every note needs a deliberate position. Missing entries remain
184
- centered, and an array longer than the note array is rejected.
352
+ Use `pans` to choose a position for each note. Supplied entries override `panSpread`; missing entries use the spread position, or remain centered when spread is 0. An array longer than the note array is rejected.
185
353
 
186
354
  ```javascript
187
355
  synth.playChord({
@@ -191,34 +359,33 @@ synth.playChord({
191
359
  })
192
360
  ```
193
361
 
194
- ## Play a pattern
195
-
196
- A pattern contains 1 to 512 object steps. Each step must have exactly one of
197
- `note`, `frequency` or `rest: true`.
362
+ ## Play a sequence
198
363
 
199
364
  ```javascript
200
- synth.playPattern({
201
- bpm: 126,
202
- swing: 0.1,
203
- noteValue: '8n',
204
- waveType: contract.waveTypes.SQUARE,
205
- steps: [
206
- { note: 'C4' },
207
- { note: 'E4' },
208
- { rest: true },
209
- { note: 'G4', noteValue: '4n', pan: 0.5 }
210
- ]
365
+ synth.setTempo({ bpm: 112 })
366
+ var sequenceId = synth.playSequence({
367
+ stepDuration: 0.5, swing: 0.12, bus: 'music',
368
+ voice: { waveType: WAVE.SQUARE, volume: 0.55 },
369
+ steps: [{ notes: [60] }, { notes: [64], overrides: { pan: -0.4, panEnd: 0.4 } },
370
+ { rest: true }, { notes: [67], duration: 1, length: 0.8 }]
211
371
  })
372
+ if (sequenceId === 0) throw new Error('Sequence rejected')
373
+ // Stop this owner without affecting unrelated voices:
374
+ synth.stopSequence({ sequenceId: sequenceId })
212
375
  ```
213
376
 
214
- Step timing is chosen in this order: step `duration`, step `noteValue`, pattern
215
- `noteValue`, pattern `stepDuration`, then the 100 ms default. A step also
216
- inherits the pattern's envelope, volume, waveform, pan, sweep and vibrato
217
- options. Values set on the step override the inherited value.
377
+ ## Know when a sequence ends
218
378
 
219
- Only one pattern scheduler is active. Starting another pattern cancels the
220
- pending steps of the previous pattern. It does not stop voices that have
221
- already started.
379
+ Listen for `sequenceend`, whose payload is `{ sequenceId, reason: 'completed' | 'stopped' }`. Filter by the owner ID before changing UI. The event is asynchronous and may arrive while a release or effect tail remains audible. For a precise next attack, place it in the same native sequence; event chaining crosses JavaScript delivery latency. Remove the listener during owner cleanup. Identifiers survive internal slot reuse without referring to the new sequence.
380
+
381
+ ```javascript
382
+ function onSequenceEnd (event) {
383
+ if (event.sequenceId === sequenceId) Ti.API.info(event.reason)
384
+ }
385
+ synth.addEventListener('sequenceend', onSequenceEnd)
386
+ // During owner cleanup:
387
+ synth.removeEventListener('sequenceend', onSequenceEnd)
388
+ ```
222
389
 
223
390
  ## Master level and stopping
224
391
 
@@ -227,26 +394,210 @@ synth.setVolume({ volume: 0.7 })
227
394
  synth.stopAll()
228
395
  ```
229
396
 
230
- Master volume is ramped to avoid a step in the output. `stopAll()` fades active
231
- voices, but it does not cancel future steps in the current pattern. Start a new
232
- pattern or call `shutdown()` when future scheduling must stop as well.
397
+ Master volume is ramped to avoid a step in the output. `stopAll()` stops all native sequences, fades active voices and invalidates effect histories while leaving the engine available. Use `stopSequence({ sequenceId })` to stop only one owner and preserve unrelated sounds. `shutdown()` also stops the stream and releases its resources.
233
398
 
234
399
  ## Output route changes
235
400
 
236
- The native ports rebuild or resume their stream after headphones, Bluetooth or
237
- other output routes change. The note crossing the transition may end early;
238
- new notes can be played normally after the system settles. Do not restart the
239
- whole app for a route change.
401
+ The native ports rebuild or resume their stream after headphones, Bluetooth or other output routes change. The note crossing the transition may end early; new notes can be played normally after the system settles. Do not restart the whole app for a route change.
402
+
403
+ On iOS, `start()` activates a `Playback` audio session by default. That ignores the silent switch and normally interrupts other audio. Pass `audioSession: 'ambient'` to respect the silent switch and mix with other apps; read [coexisting with other audio](recommendations.md#coexisting-with-other-audio) and test the final app on a device.
404
+
405
+ ## Integrate binaural in a 2D app
406
+
407
+ The JavaScript app chooses a position for each sound; the native module generates and filters that voice. Installing the module or enabling an app switch does not convert existing `pan` calls into binaural calls. Update every playback path that should use positioning, including explosions, impacts, shots, lasers and repeated motor/thrust events. A central audio service can do this once for all effects that supply their current source position. Calls bypassing that service keep their existing behavior.
408
+
409
+ Keep the synthesis recipe (pitch, waveform, duration, envelope, filters, FM and volume), and replace its placement fields when submitting the event. The native ports process each event with the shared KEMAR FIR; there is no global platform spatial-audio switch.
410
+
411
+ ### Detect the installed feature
412
+
413
+ Check both coordinate limits before enabling binaural controls to verify that the app loaded the expected native module:
414
+
415
+ ```javascript
416
+ var synth = require('ti.synthengine')
417
+ var contract = synth.getDefaults()
418
+ var supportsBinaural = !!(contract.limits &&
419
+ contract.limits.spatialX && contract.limits.spatialY)
420
+ ```
421
+
422
+ If the limits are absent, disable the binaural switch and keep stereo playback. Replace the installed module package and rebuild and reinstall the app to use a new native binary. Updating `tiapp.xml` or copying module files does not change an app already installed on a device. Follow the [lifecycle](#lifecycle) above to start the engine before playing.
423
+
424
+ ### Direction and amplitude are different controls
425
+
426
+ `spatialX`/`spatialY` are signed directions from -1 to 1. A 0 to 100% amplitude slider scales the position derived from the object; it does not select a direction. At height 100%, an upper object remains above and a lower object remains below. At height 0%, both are at the front-center height, while binaural remains enabled. Direct direction sliders would instead need -100 to +100%, divided by 100.
427
+
428
+ | App control | Effect |
429
+ | ------------------------------- | ------------------------------------------------------------------------------------------- |
430
+ | Binaural switch | Send spatial coordinates when on; omit both when off. |
431
+ | Horizontal amplitude, 0 to 100% | Scale each source's left/right coordinate; also controls stereo `pan` when binaural is off. |
432
+ | Height amplitude, 0 to 100% | Scale each source's below/above coordinate; inactive in stereo mode. |
433
+ | Master volume | Use `setVolume`; independent of direction and amplitude. |
434
+
435
+ Store these settings as app preferences; `start` does not accept them, and they have no separate native methods. Keep the app's chosen defaults and saved preferences; the module does not impose a 100% slider default. Persist them in the app if they should survive restart. A switch change applies to newly submitted events, without restarting the engine or repositioning voices already playing.
436
+
437
+ With both amplitude controls at 100%, screen positions map as follows:
438
+
439
+ | Position | `spatialX` | `spatialY` |
440
+ | ------------- | ---------: | ---------: |
441
+ | Top left | -1 | 1 |
442
+ | Top center | 0 | 1 |
443
+ | Top right | 1 | 1 |
444
+ | Middle left | -1 | 0 |
445
+ | Center | 0 | 0 |
446
+ | Middle right | 1 | 0 |
447
+ | Bottom left | -1 | -1 |
448
+ | Bottom center | 0 | -1 |
449
+ | Bottom right | 1 | -1 |
450
+
451
+ ### Convert screen positions when playing a tone
452
+
453
+ This example preserves a recipe, removes conflicting placement fields, and computes new coordinates for each call. Coordinates use the same local arena space: X grows right, Y grows down, and width/height are the arena dimensions. Convert world or camera coordinates into that space first. The example percentages are app choices; retain your own defaults.
454
+
455
+ ```javascript
456
+ var audioSettings = {
457
+ binaural: supportsBinaural,
458
+ horizontalPercent: 50,
459
+ heightPercent: 100
460
+ }
461
+
462
+ function clamp (value, min, max) {
463
+ return Math.max(min, Math.min(max, value))
464
+ }
465
+
466
+ function playPositionedTone (recipe, x, y, width, height) {
467
+ var values = [x, y, width, height]
468
+ if (!values.every(function (value) {
469
+ return typeof value === 'number' && isFinite(value)
470
+ }) || width <= 0 || height <= 0) {
471
+ throw new Error('Sound position needs finite coordinates and positive dimensions')
472
+ }
473
+
474
+ var options = Object.assign({}, recipe)
475
+ delete options.pan
476
+ delete options.panEnd
477
+ delete options.pans
478
+ delete options.panSpread
479
+ delete options.spatialX
480
+ delete options.spatialY
481
+
482
+ var horizontal = clamp(audioSettings.horizontalPercent, 0, 100) / 100
483
+ var vertical = clamp(audioSettings.heightPercent, 0, 100) / 100
484
+ var nx = clamp(2 * x / width - 1, -1, 1)
485
+ var ny = clamp(1 - 2 * y / height, -1, 1)
486
+
487
+ if (audioSettings.binaural && supportsBinaural) {
488
+ options.spatialX = nx * horizontal
489
+ options.spatialY = ny * vertical
490
+ } else {
491
+ options.pan = nx * horizontal
492
+ options.panEnd = options.pan
493
+ }
494
+ return synth.playTone(options)
495
+ }
496
+
497
+ var explosion = {
498
+ frequency: 90,
499
+ waveType: contract.waveTypes.NOISE,
500
+ duration: 500,
501
+ attack: 2,
502
+ release: 200,
503
+ volume: 0.3
504
+ }
505
+ if (!playPositionedTone(explosion, 900, 100, 1000, 800)) {
506
+ Ti.API.warn('Positioned explosion rejected')
507
+ }
508
+ ```
509
+
510
+ Connect a Titanium switch's `change` event to `audioSettings.binaural = event.value`; connect the two sliders' `change` events to `horizontalPercent` and `heightPercent`. Configure slider minimum 0 and maximum 100, and disable the height slider while the switch is off. Validate restored preference values as finite numbers within those ranges before using them. Do not pass these preference names to `playTone`: that would return `false` with `unsupported_option`.
511
+
512
+ Supply the current position whenever an effect fires. A projectile launch cue can use the launch position; an impact uses the collision position. Timed event positions remain fixed after submission. An identified sustained voice started spatially can instead change `spatialX`/`spatialY` through `updateVoice`, with 10 ms ramps and retained spatial history. A stereo voice cannot become spatial through an update. Stereo `panEnd` movement cannot be combined with binaural; do not continually retrigger long sounds every frame to imitate movement.
513
+
514
+ ### Chords and patterns
515
+
516
+ A chord shares one binaural position across all its voices. Remove `pans` and `panSpread` when sending coordinates. For different simultaneous source positions, use independent tone events and account for their different gain/overlap behavior:
517
+
518
+ ```javascript
519
+ if (!synth.playChord({
520
+ notes: ['C4', 'E4', 'G4'],
521
+ duration: 500,
522
+ volume: 0.3,
523
+ spatialX: -0.5,
524
+ spatialY: 0.7
525
+ })) Ti.API.warn('Spatial chord rejected')
526
+ ```
527
+
528
+ Sequence steps inherit coordinates from root `voice` and can override either one. Remove conflicting stereo fields from both root `voice` and every step override; even `pan: 0` conflicts with a spatial coordinate. A step cannot opt out of inherited binaural by passing zero coordinates; use a pattern without spatial coordinates in its root voice for a mix of spatial and stereo steps.
529
+
530
+ ```javascript
531
+ if (!synth.playSequence({
532
+ timeUnit: 'ms',
533
+ stepDuration: 300,
534
+ voice: { attack: 5, release: 80, volume: 0.3, spatialX: 0, spatialY: 1 },
535
+ steps: [
536
+ { notes: [60] },
537
+ { notes: [64], overrides: { spatialY: -1 } },
538
+ { rest: true },
539
+ { notes: [67], overrides: { spatialX: 1, spatialY: 0 } }
540
+ ]
541
+ })) Ti.API.warn('Spatial pattern rejected')
542
+ ```
543
+
544
+ ### Listen and measure
545
+
546
+ Use stereo headphones and compare isolated broadband noise above/below before testing many collisions. Perceived elevation varies with the listener, headphones and spectrum; low sine tones are weak tests. The output is still stereo, with a front plane and no rear sources, distance attenuation, room reflections or head tracking. A 100% slider uses the modeled directional range; it does not guarantee strong localization for every person.
547
+
548
+ Measure CPU cost on the intended device with realistic event overlap. The cost depends on concurrent spatial voices and their short tails; the total number of effects over a game does not determine it. Host replay supports comparisons but does not measure device CPU, battery, heat or underruns. See [the renderer](audio-engine.md#binaural-front-plane-renderer) and [diagnostic checks](troubleshooting.md#binaural-does-not-change-the-position).
549
+
550
+ ## Read engine statistics
240
551
 
241
- On iOS, `startEngine()` activates a `Playback` audio session. That ignores the
242
- silent switch and normally interrupts other audio. If the app needs different
243
- session behavior, read [coexisting with other audio](recommendations.md#coexisting-with-other-audio)
244
- and test the final app on a device.
552
+ Basic counters are available without enabling optional diagnostics. Reads do not start audio or reset counters. Detect the methods if supporting older development packages that also report 1.0.0:
553
+
554
+ ```javascript
555
+ var statsTimer = null
556
+ var statsSupported = typeof synth.getStats === 'function' &&
557
+ typeof synth.setStatsEnabled === 'function' && typeof synth.resetStats === 'function'
558
+
559
+ function readStats () {
560
+ var stats = synth.getStats()
561
+ Ti.API.info('Voices: ' + stats.activeVoices + '/' + stats.maxVoices)
562
+ Ti.API.info('Render load: ' + (stats.renderLoad === null
563
+ ? 'unmeasured' : (stats.renderLoad * 100).toFixed(1) + '%'))
564
+ }
565
+
566
+ function startDiagnostics () {
567
+ if (!statsSupported || statsTimer !== null) return false
568
+ if (!synth.setStatsEnabled(true)) return false
569
+ statsTimer = setInterval(readStats, 200)
570
+ return true
571
+ }
572
+
573
+ function stopDiagnostics () {
574
+ if (statsTimer !== null) clearInterval(statsTimer)
575
+ statsTimer = null
576
+ if (statsSupported) synth.setStatsEnabled(false)
577
+ }
578
+
579
+ // Read basic counters without enabling timing or signal meters:
580
+ if (statsSupported) readStats()
581
+ // When the user requests diagnostics: startDiagnostics()
582
+ // When the diagnostic view hides, the app backgrounds or the owner closes:
583
+ // stopDiagnostics()
584
+ // To reset accumulated counters deliberately, without stopping playback:
585
+ // if (statsSupported) synth.resetStats()
586
+ ```
587
+
588
+ Integrate `stopDiagnostics()` into the existing owner's hide/background/close handlers, before shutdown. Resume polling only while the diagnostic view is active; do not add a second engine owner. Timing and output meters stay `null` until enabled and measured. Basic counters remain available when diagnostics are off. Android backend underrun/buffer/latency fields are best-effort and independent of that switch; iOS returns `null` for those backend fields. A zero underrun count does not prove clean sound.
589
+
590
+ See the [complete field contract](api.md#getstats), [measurement limits](stats-performance.md) and [interactive statistics controls](../assets/example/README.md#statistics).
245
591
 
246
592
  ## Next steps
247
593
 
248
594
  - [API reference](api.md) for every accepted key and limit.
249
- - [Sound design](sound-design.md) for waveform and envelope recipes.
595
+ - [Sound design](sound-design.md) for waveform, envelope, filter and FM recipes.
250
596
  - [Examples](examples.md) for copy-ready sounds and patterns.
251
- - [Troubleshooting](troubleshooting.md) when a call returns `false` or audio is
252
- silent.
597
+ - [Troubleshooting](troubleshooting.md) when a call returns `false` or audio is silent.
598
+
599
+ ## Diagnose mix headroom
600
+
601
+ The current diagnostics add `preLimiterPeakDb` (maximum signal before final limiting, in dBFS) and `maxLimiterReductionDb` (greatest limiter attenuation in dB) accumulated since reset. Unlike the approximately 200 ms window reduction field, brief peaks remain visible between UI polls. Enable measurements, reset before the scenario and keep `null`/absent fields distinct from measured zero. The [measurement example](advanced-examples.md#measure-peaks-and-limiter-reduction) shows opt-in polling and cleanup.
602
+
603
+ Fixed event/input gain does not normalize continuously by active voice count. Coherent overlapping notes, effects feedback or resonance can still exceed the ceiling; the limiter then acts on the whole stereo sum. Saturation is a separate creative processor, disabled initially. Physical loudness and small-speaker bass response are not measured by these digital meters.