@maccesar/titools 5.2.0 → 5.2.2

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 (81) hide show
  1. package/CONTRIBUTING.md +82 -0
  2. package/README.md +83 -72
  3. package/agents/ti-pro.md +1 -1
  4. package/commands/ti-audit.md +2 -2
  5. package/docs/examples.md +91 -0
  6. package/docs/knowledge-index.md +57 -0
  7. package/lib/commands/uninstall.js +19 -16
  8. package/lib/commands/update.js +1 -1
  9. package/lib/downloader.js +1 -1
  10. package/lib/utils.js +2 -0
  11. package/package.json +4 -2
  12. package/skills/purgetss/references/multi-density-images.md +1 -1
  13. package/skills/ti-admob-unified/SKILL.md +21 -7
  14. package/skills/ti-admob-unified/assets/example/app.js +229 -25
  15. package/skills/ti-admob-unified/assets/example/lib/ads.js +30 -2
  16. package/skills/ti-admob-unified/assets/example/library-usage.js +33 -1
  17. package/skills/ti-admob-unified/references/api-reference.md +39 -16
  18. package/skills/ti-admob-unified/references/banners.md +9 -9
  19. package/skills/ti-admob-unified/references/contributing.md +14 -9
  20. package/skills/ti-admob-unified/references/dependencies.md +31 -25
  21. package/skills/ti-admob-unified/references/fullscreen.md +43 -28
  22. package/skills/ti-admob-unified/references/game-monetization.md +138 -7
  23. package/skills/ti-admob-unified/references/getting-started.md +75 -31
  24. package/skills/ti-admob-unified/references/index.md +5 -2
  25. package/skills/ti-admob-unified/references/integration.md +8 -6
  26. package/skills/ti-admob-unified/references/library.md +41 -10
  27. package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
  28. package/skills/ti-admob-unified/references/mediation.md +22 -9
  29. package/skills/ti-admob-unified/references/migration.md +11 -7
  30. package/skills/ti-admob-unified/references/module-changelog.md +41 -11
  31. package/skills/ti-admob-unified/references/privacy.md +35 -8
  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 +28 -31
  35. package/skills/ti-admob-unified/references/source-notes.md +51 -4
  36. package/skills/ti-admob-unified/references/source-snapshot.json +249 -45
  37. package/skills/ti-admob-unified/references/third-party-notices.md +4 -4
  38. package/skills/ti-game/SKILL.md +23 -6
  39. package/skills/ti-game/references/api.md +34 -30
  40. package/skills/ti-game/references/debugging-performance.md +18 -9
  41. package/skills/ti-game/references/examples.md +103 -20
  42. package/skills/ti-game/references/input.md +101 -0
  43. package/skills/ti-game/references/patterns.md +39 -15
  44. package/skills/ti-game/references/physics.md +80 -52
  45. package/skills/ti-game/references/project-setup.md +118 -56
  46. package/skills/ti-game/references/recipes-action.md +107 -8
  47. package/skills/ti-game/references/recipes-adventure-effects.md +41 -9
  48. package/skills/ti-game/references/roadmap.md +10 -3
  49. package/skills/ti-game/references/source-notes.md +81 -0
  50. package/skills/ti-game/references/tilemaps.md +12 -4
  51. package/skills/ti-synthengine/SKILL.md +81 -95
  52. package/skills/ti-synthengine/assets/example/README.md +84 -0
  53. package/skills/ti-synthengine/assets/example/app.js +1178 -0
  54. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
  55. package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
  56. package/skills/ti-synthengine/references/advanced-examples.md +174 -0
  57. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  58. package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
  59. package/skills/ti-synthengine/references/api.md +434 -380
  60. package/skills/ti-synthengine/references/audio-engine.md +157 -230
  61. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  62. package/skills/ti-synthengine/references/examples.md +123 -253
  63. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  64. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  65. package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
  66. package/skills/ti-synthengine/references/guide.md +188 -156
  67. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  68. package/skills/ti-synthengine/references/performance-review.json +1146 -0
  69. package/skills/ti-synthengine/references/physical-validation.json +1457 -0
  70. package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
  71. package/skills/ti-synthengine/references/recipes.md +423 -599
  72. package/skills/ti-synthengine/references/recommendations.md +63 -113
  73. package/skills/ti-synthengine/references/sound-design.md +134 -258
  74. package/skills/ti-synthengine/references/source-notes.md +93 -105
  75. package/skills/ti-synthengine/references/source-snapshot.json +379 -32
  76. package/skills/ti-synthengine/references/stats-performance.md +64 -20
  77. package/skills/ti-synthengine/references/studio-validation.json +6202 -0
  78. package/skills/ti-synthengine/references/troubleshooting.md +109 -121
  79. package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
  80. package/skills/ti-synthengine/references/unified-validation.json +314 -0
  81. package/AGENTS-VERCEL-RESEARCH.md +0 -175
@@ -1,9 +1,8 @@
1
- > Source snapshot: official TiSynthEngine 1.0.0 beta documentation from the reviewed working tree based on `95901c98af5353b86d9dc7c60b29ab9186b038cd` (2026-10-14), including uncommitted statistics changes. Upstream `documentation/guide.md` SHA-256: `c5fb4ff34ec425487221b80d6793bda24aa328281ab9ccb02e65b0065e54842e`.
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.
7
6
 
8
7
  <!-- TOC-START -->
9
8
  ## Contents
@@ -11,30 +10,35 @@ the native audio resources when the owning screen closes.
11
10
  - [Install the module](#install-the-module)
12
11
  - [Load and verify the contract](#load-and-verify-the-contract)
13
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)
14
17
  - [Play the first sound](#play-the-first-sound)
15
18
  - [Choose a pitch](#choose-a-pitch)
16
19
  - [Shape a tone](#shape-a-tone)
17
20
  - [Settle, filter and modulate](#settle-filter-and-modulate)
18
21
  - [Shape noise and waveforms](#shape-noise-and-waveforms)
19
22
  - [Play simultaneous notes](#play-simultaneous-notes)
20
- - [Play a pattern](#play-a-pattern)
21
- - [Know when a pattern ends](#know-when-a-pattern-ends)
23
+ - [Play a sequence](#play-a-sequence)
24
+ - [Know when a sequence ends](#know-when-a-sequence-ends)
22
25
  - [Master level and stopping](#master-level-and-stopping)
23
26
  - [Output route changes](#output-route-changes)
24
27
  - [Integrate binaural in a 2D app](#integrate-binaural-in-a-2d-app)
28
+ - [Read engine statistics](#read-engine-statistics)
25
29
  - [Next steps](#next-steps)
30
+ - [Diagnose mix headroom](#diagnose-mix-headroom)
26
31
 
27
32
  <!-- TOC-END -->
28
33
 
29
34
  ## Install the module
30
35
 
31
- For the unreleased beta, use the packages built from the current module source in `android/dist` and `ios/dist`. The [latest release](https://github.com/macCesar/TiSynthEngine/releases/latest) is a separate package snapshot; it does not update when source files change. The archive names are:
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`:
32
37
 
33
38
  - `ti.synthengine-android-1.0.0.zip`
34
39
  - `ti.synthengine-iphone-1.0.0.zip`
35
40
 
36
- Put the archives in the Titanium project root or install them through the
37
- 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:
38
42
 
39
43
  ```xml
40
44
  <modules>
@@ -43,13 +47,11 @@ Titanium CLI. Pin the same version for both platforms:
43
47
  </modules>
44
48
  ```
45
49
 
46
- TiSynthEngine requires Titanium SDK 13.2.0.GA or newer. Android projects must
47
- compile against API 35 or newer. The iOS module requires iOS 15 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.
48
51
 
49
52
  ## Load and verify the contract
50
53
 
51
- Require the module once in the code that owns audio. Check `apiVersion` before
52
- 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.
53
55
 
54
56
  ```javascript
55
57
  var synth = require('ti.synthengine')
@@ -60,15 +62,11 @@ if (contract.apiVersion !== '1.0.0') {
60
62
  }
61
63
  ```
62
64
 
63
- `getDefaults()` works before the stream starts. Call it again after startup if
64
- you need the frequency ceiling for input validation, because that ceiling
65
- 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.
66
66
 
67
67
  ## Lifecycle
68
68
 
69
- Opening a low-latency stream while Titanium lays out the first window can cause
70
- an underrun on some Android devices. Register `postlayout` before `open()`, wait
71
- 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.
72
70
 
73
71
  ```javascript
74
72
  var running = false
@@ -77,10 +75,12 @@ var startTimer = null
77
75
  function startAudio () {
78
76
  startTimer = null
79
77
  win.removeEventListener('postlayout', onPostLayout)
80
- running = synth.startEngine({
78
+ running = synth.start({
81
79
  maxVoices: 16,
82
- mixingProfile: 'conservative'
80
+ mixingProfile: 'conservative',
81
+ audioSession: 'ambient'
83
82
  })
83
+ if (!running) Ti.API.warn('TiSynthEngine could not start; keep playback controls disabled')
84
84
  }
85
85
 
86
86
  function onPostLayout () {
@@ -108,9 +108,61 @@ win.addEventListener('close', closeAudio)
108
108
  win.open()
109
109
  ```
110
110
 
111
- Use one owner for the engine. Start it once, pass a small audio service to the
112
- screens that need it, and call `shutdown()` from the owner. A second
113
- `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).
114
166
 
115
167
  ## Play the first sound
116
168
 
@@ -127,9 +179,7 @@ var accepted = synth.playTone({
127
179
  if (!accepted) Ti.API.warn('The tone was rejected')
128
180
  ```
129
181
 
130
- Calls that create or change sound return `true` when accepted and `false` when
131
- rejected. During development, handle that result and inspect logcat or the
132
- 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.
133
183
 
134
184
  ## Choose a pitch
135
185
 
@@ -140,13 +190,11 @@ synth.playTone({ frequency: 440 })
140
190
  synth.playTone({ note: 'A4' })
141
191
  ```
142
192
 
143
- Do not send both. Notes accept English names (`C#4`, `Bb3`) and solfege names
144
- (`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.
145
194
 
146
195
  ## Shape a tone
147
196
 
148
- The oscillator runs for `duration` milliseconds. `attack` controls the fade in
149
- 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.
150
198
 
151
199
  ```javascript
152
200
  synth.playTone({
@@ -158,9 +206,7 @@ synth.playTone({
158
206
  })
159
207
  ```
160
208
 
161
- Explicit envelope values must fit inside `duration`. An attack equal to or
162
- longer than the duration is rejected, as is a release longer than the time left
163
- 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.
164
210
 
165
211
  Tone options can also move during the event:
166
212
 
@@ -178,15 +224,11 @@ synth.playTone({
178
224
  })
179
225
  ```
180
226
 
181
- `frequencyEnd` creates a linear pitch sweep. `panEnd` moves the sound between
182
- channels. `lfoFreq` is the vibrato rate in hertz and `lfoDepth` is its depth in
183
- 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.
184
228
 
185
229
  ## Settle, filter and modulate
186
230
 
187
- A flat envelope suits beeps. For plucks, keys and bells, add `decay` and
188
- `sustain`: the level falls from full scale to `sustain` over `decay`
189
- milliseconds and holds there until the release.
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.
190
232
 
191
233
  ```javascript
192
234
  synth.playTone({
@@ -200,13 +242,9 @@ synth.playTone({
200
242
  })
201
243
  ```
202
244
 
203
- `sustain` below 1 needs a nonzero `decay`, and `attack` plus `decay` must fit in
204
- `duration`. The 50 ms default attack counts, so set a short `attack` when the
205
- decay is long.
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.
206
246
 
207
- `filterType` adds a resonant filter: `lowpass`, `highpass` or `bandpass`.
208
- `filterHz` is the cutoff, `filterQ` the resonance, and `filterHzEnd` sweeps the
209
- cutoff over the note.
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.
210
248
 
211
249
  ```javascript
212
250
  synth.playTone({
@@ -221,12 +259,9 @@ synth.playTone({
221
259
  })
222
260
  ```
223
261
 
224
- Any `filterHz`, `filterHzEnd` or `filterQ` without a `filterType` other than
225
- `none` is rejected with `filter_type_required`.
262
+ Any `filterHz`, `filterHzEnd` or `filterQ` without a `filterType` other than `none` is rejected with `filter_type_required`.
226
263
 
227
- `fmDepth` makes a `SINE` voice an FM voice. `fmRatio` is the modulator
228
- frequency as a multiple of the carrier, and `fmDepthEnd` moves the modulation
229
- index over the note. Omit `fmDepthEnd` for no sweep; 0 is a real target.
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.
230
265
 
231
266
  ```javascript
232
267
  synth.playTone({
@@ -241,8 +276,7 @@ synth.playTone({
241
276
  })
242
277
  ```
243
278
 
244
- `detune` shifts a pitch in cents. In a chord, `detunes` gives one offset per
245
- pitch, so repeating a note with small offsets thickens it:
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:
246
280
 
247
281
  ```javascript
248
282
  synth.playChord({
@@ -255,9 +289,7 @@ synth.playChord({
255
289
 
256
290
  ## Shape noise and waveforms
257
291
 
258
- Use `noiseRate` to hold each pseudo-random noise value instead of generating a
259
- new one every sample. A low rate sounds sparse and grainy; a high rate moves
260
- toward white noise. `noiseRateEnd` sweeps the update rate.
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.
261
293
 
262
294
  ```javascript
263
295
  synth.playTone({
@@ -273,8 +305,7 @@ synth.playTone({
273
305
  })
274
306
  ```
275
307
 
276
- `lowPassHz` is a per-voice one-pole filter. `bitDepth` applies amplitude
277
- quantization from 2 to 16 bits. Zero disables either control.
308
+ `lowPassHz` is a per-voice one-pole filter. `bitDepth` applies amplitude quantization from 2 to 16 bits. Zero disables either control.
278
309
 
279
310
  Square waves accept a duty cycle and a duty-cycle sweep:
280
311
 
@@ -289,8 +320,7 @@ synth.playTone({
289
320
  })
290
321
  ```
291
322
 
292
- For a caller-defined repeating shape, select `WAVETABLE` and provide 2 to 64
293
- samples from -1 to 1. The table is stepped rather than interpolated.
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.
294
324
 
295
325
  ```javascript
296
326
  synth.playTone({
@@ -317,11 +347,9 @@ synth.playChord({
317
347
  })
318
348
  ```
319
349
 
320
- The chord is placed on the audio queue atomically. Its voices also share a
321
- constant-power event gain. Several separate `playTone` calls are independent,
322
- 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.
323
351
 
324
- Use `pans` when notes need deliberate positions. 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.
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.
325
353
 
326
354
  ```javascript
327
355
  synth.playChord({
@@ -331,65 +359,34 @@ synth.playChord({
331
359
  })
332
360
  ```
333
361
 
334
- ## Play a pattern
335
-
336
- A pattern contains 1 to 512 object steps. Each step must have exactly one of
337
- `note`, `frequency` or `rest: true`.
362
+ ## Play a sequence
338
363
 
339
364
  ```javascript
340
- synth.playPattern({
341
- bpm: 126,
342
- swing: 0.1,
343
- noteValue: '8n',
344
- waveType: contract.waveTypes.SQUARE,
345
- steps: [
346
- { note: 'C4' },
347
- { note: 'E4' },
348
- { rest: true },
349
- { note: 'G4', noteValue: '4n', pan: 0.5 }
350
- ]
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 }]
351
371
  })
372
+ if (sequenceId === 0) throw new Error('Sequence rejected')
373
+ // Stop this owner without affecting unrelated voices:
374
+ synth.stopSequence({ sequenceId: sequenceId })
352
375
  ```
353
376
 
354
- Step timing is chosen in this order: step `duration`, step `noteValue`, pattern
355
- `noteValue`, pattern `stepDuration`, then the 100 ms default. A step also
356
- inherits the pattern's envelope, decay, sustain, volume, waveform, pan, sweep,
357
- vibrato, noise, filter, resonant filter, FM, detune, pulse-width, wavetable and
358
- quantization options. Values set on the step override the inherited value.
377
+ ## Know when a sequence ends
359
378
 
360
- Only one pattern scheduler is active. Starting another pattern cancels the
361
- pending steps of the previous pattern. It does not stop voices that have
362
- already started.
363
-
364
- ## Know when a pattern ends
365
-
366
- Each accepted pattern produces one `patternend` event. `completed` is `true`
367
- when the pattern reaches its natural end and `false` when a newer pattern
368
- replaces it. Use it to chain jingles or to release state that follows a
369
- pattern.
370
-
371
- ```javascript
372
- function onPatternEnd (event) {
373
- if (event.completed) playNextJingle()
374
- }
375
-
376
- synth.addEventListener('patternend', onPatternEnd)
377
- ```
378
-
379
- The listener belongs to the module, not to the pattern, so it outlives the
380
- screen unless you remove it. Add it to the same cleanup that calls
381
- `shutdown()`:
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.
382
380
 
383
381
  ```javascript
384
- function closeAudio () {
385
- synth.removeEventListener('patternend', onPatternEnd)
386
- if (running) synth.shutdown()
382
+ function onSequenceEnd (event) {
383
+ if (event.sequenceId === sequenceId) Ti.API.info(event.reason)
387
384
  }
385
+ synth.addEventListener('sequenceend', onSequenceEnd)
386
+ // During owner cleanup:
387
+ synth.removeEventListener('sequenceend', onSequenceEnd)
388
388
  ```
389
389
 
390
- `shutdown()` discards a pending pattern without an event, so a closing owner
391
- never receives a callback after cleanup.
392
-
393
390
  ## Master level and stopping
394
391
 
395
392
  ```javascript
@@ -397,32 +394,23 @@ synth.setVolume({ volume: 0.7 })
397
394
  synth.stopAll()
398
395
  ```
399
396
 
400
- Master volume is ramped to avoid a step in the output. `stopAll()` fades active
401
- voices, but it does not cancel future steps in the current pattern. Start a new
402
- 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.
403
398
 
404
399
  ## Output route changes
405
400
 
406
- The native ports rebuild or resume their stream after headphones, Bluetooth or
407
- other output routes change. The note crossing the transition may end early;
408
- new notes can be played normally after the system settles. Do not restart the
409
- 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.
410
402
 
411
- On iOS, `startEngine()` activates a `Playback` audio session by default. That
412
- ignores the silent switch and normally interrupts other audio. Pass
413
- `audioSession: 'ambient'` to respect the silent switch and mix with other apps;
414
- read [coexisting with other audio](recommendations.md#coexisting-with-other-audio)
415
- and test the final app on a device.
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.
416
404
 
417
405
  ## Integrate binaural in a 2D app
418
406
 
419
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.
420
408
 
421
- Keep the synthesis recipe (pitch, waveform, duration, envelope, filters, FM and volume). Replace its placement fields when submitting the event. This is per-event processing inside the native ports using the shared KEMAR FIR, rather than a global platform spatial-audio switch.
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.
422
410
 
423
411
  ### Detect the installed feature
424
412
 
425
- During the unreleased 1.0.0 beta, the version string alone does not prove that an installed binary has binaural support. Check both coordinate limits before enabling the controls:
413
+ Check both coordinate limits before enabling binaural controls to verify that the app loaded the expected native module:
426
414
 
427
415
  ```javascript
428
416
  var synth = require('ti.synthengine')
@@ -437,28 +425,28 @@ If the limits are absent, disable the binaural switch and keep stereo playback.
437
425
 
438
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.
439
427
 
440
- | App control | Effect |
441
- | --- | --- |
442
- | Binaural switch | Send spatial coordinates when on; omit both when off. |
428
+ | App control | Effect |
429
+ | ------------------------------- | ------------------------------------------------------------------------------------------- |
430
+ | Binaural switch | Send spatial coordinates when on; omit both when off. |
443
431
  | Horizontal amplitude, 0 to 100% | Scale each source's left/right coordinate; also controls stereo `pan` when binaural is off. |
444
- | Height amplitude, 0 to 100% | Scale each source's below/above coordinate; inactive in stereo mode. |
445
- | Master volume | Use `setVolume`; independent of direction and amplitude. |
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. |
446
434
 
447
- These are app preferences, not options for `startEngine` or additional 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.
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.
448
436
 
449
437
  With both amplitude controls at 100%, screen positions map as follows:
450
438
 
451
- | Position | `spatialX` | `spatialY` |
452
- | --- | ---: | ---: |
453
- | Top left | -1 | 1 |
454
- | Top center | 0 | 1 |
455
- | Top right | 1 | 1 |
456
- | Middle left | -1 | 0 |
457
- | Center | 0 | 0 |
458
- | Middle right | 1 | 0 |
459
- | Bottom left | -1 | -1 |
460
- | Bottom center | 0 | -1 |
461
- | Bottom right | 1 | -1 |
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 |
462
450
 
463
451
  ### Convert screen positions when playing a tone
464
452
 
@@ -521,7 +509,7 @@ if (!playPositionedTone(explosion, 900, 100, 1000, 800)) {
521
509
 
522
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`.
523
511
 
524
- Supply the current position whenever an effect fires. A projectile launch cue can use the launch position; an impact uses the collision position. Repeated short motor events can use successive positions. A single active tone cannot move after submission because this API has no per-voice handle or position-update method. Stereo `panEnd` movement cannot be combined with binaural; do not continually retrigger long sounds every frame to imitate movement.
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.
525
513
 
526
514
  ### Chords and patterns
527
515
 
@@ -537,21 +525,18 @@ if (!synth.playChord({
537
525
  })) Ti.API.warn('Spatial chord rejected')
538
526
  ```
539
527
 
540
- Pattern steps inherit coordinates from the root and can override either one. Remove conflicting stereo fields from both the root and every step; 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 on its root for a mix of spatial and stereo steps.
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.
541
529
 
542
530
  ```javascript
543
- if (!synth.playPattern({
531
+ if (!synth.playSequence({
532
+ timeUnit: 'ms',
544
533
  stepDuration: 300,
545
- attack: 5,
546
- release: 80,
547
- volume: 0.3,
548
- spatialX: 0,
549
- spatialY: 1,
534
+ voice: { attack: 5, release: 80, volume: 0.3, spatialX: 0, spatialY: 1 },
550
535
  steps: [
551
- { note: 'C4' },
552
- { note: 'E4', spatialY: -1 },
536
+ { notes: [60] },
537
+ { notes: [64], overrides: { spatialY: -1 } },
553
538
  { rest: true },
554
- { note: 'G4', spatialX: 1, spatialY: 0 }
539
+ { notes: [67], overrides: { spatialX: 1, spatialY: 0 } }
555
540
  ]
556
541
  })) Ti.API.warn('Spatial pattern rejected')
557
542
  ```
@@ -560,12 +545,59 @@ if (!synth.playPattern({
560
545
 
561
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.
562
547
 
563
- CPU cost depends on concurrent spatial voices and their short tails, not the total number of effects over a game. Measure the intended device and realistic event overlap. Host replay is a comparison tool, not device CPU, battery, heat or underrun evidence. See [the renderer](audio-engine.md#binaural-front-plane-renderer) and [diagnostic checks](troubleshooting.md#binaural-does-not-change-the-position).
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
551
+
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).
564
591
 
565
592
  ## Next steps
566
593
 
567
594
  - [API reference](api.md) for every accepted key and limit.
568
595
  - [Sound design](sound-design.md) for waveform, envelope, filter and FM recipes.
569
596
  - [Examples](examples.md) for copy-ready sounds and patterns.
570
- - [Troubleshooting](troubleshooting.md) when a call returns `false` or audio is
571
- 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.