@maccesar/titools 5.2.4 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +27 -32
  2. package/agents/ti-pro.md +1 -1
  3. package/commands/ti-audit.md +1 -1
  4. package/docs/examples.md +15 -8
  5. package/package.json +1 -1
  6. package/skills/ti-synthengine/SKILL.md +62 -92
  7. package/skills/ti-synthengine/assets/example/README.md +51 -54
  8. package/skills/ti-synthengine/assets/example/app.js +96 -57
  9. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +39 -40
  10. package/skills/ti-synthengine/assets/example/lib/soundPreset.js +34 -0
  11. package/skills/ti-synthengine/assets/index.d.ts +226 -0
  12. package/skills/ti-synthengine/references/advanced-examples.md +12 -12
  13. package/skills/ti-synthengine/references/api.md +125 -33
  14. package/skills/ti-synthengine/references/audio-engine.md +24 -59
  15. package/skills/ti-synthengine/references/examples.md +64 -7
  16. package/skills/ti-synthengine/references/guide.md +69 -63
  17. package/skills/ti-synthengine/references/instruments-and-effects.md +34 -27
  18. package/skills/ti-synthengine/references/native-variation.md +92 -51
  19. package/skills/ti-synthengine/references/recipes-interaction.md +26 -19
  20. package/skills/ti-synthengine/references/recipes.md +19 -15
  21. package/skills/ti-synthengine/references/recommendations.md +14 -14
  22. package/skills/ti-synthengine/references/sound-api.md +197 -0
  23. package/skills/ti-synthengine/references/sound-design.md +11 -11
  24. package/skills/ti-synthengine/references/source-notes.md +62 -147
  25. package/skills/ti-synthengine/references/troubleshooting.md +22 -24
  26. package/skills/ti-synthengine/references/upgrade-2.0.0.md +40 -0
  27. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  28. package/skills/ti-synthengine/references/android-sim80-correction.json +0 -162
  29. package/skills/ti-synthengine/references/audio-pipeline-validation.md +0 -19
  30. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  31. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  32. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  33. package/skills/ti-synthengine/references/frontier-physical-validation.json +0 -1918
  34. package/skills/ti-synthengine/references/native-variation-proposal.md +0 -182
  35. package/skills/ti-synthengine/references/native-variation-validation.md +0 -63
  36. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  37. package/skills/ti-synthengine/references/performance-review.json +0 -1146
  38. package/skills/ti-synthengine/references/physical-validation.json +0 -1457
  39. package/skills/ti-synthengine/references/source-snapshot.json +0 -463
  40. package/skills/ti-synthengine/references/stats-performance.json +0 -2992
  41. package/skills/ti-synthengine/references/stats-performance.md +0 -82
  42. package/skills/ti-synthengine/references/studio-validation.json +0 -6202
  43. package/skills/ti-synthengine/references/unified-bypass-performance.json +0 -22425
  44. package/skills/ti-synthengine/references/unified-validation.json +0 -314
@@ -1,25 +1,29 @@
1
- > Source snapshot: official TiSynthEngine documentation at commit `b07c97c762be5905f132cecf33e9d60ea0c21316` (2026-11-02). Published 1.0.2; local 1.1.0 candidates; public API 1.0.0. Upstream `documentation/api.md` SHA-256: `fed350aedb50da41b303e8bb8129c6381285e1aa708c6a0a70d3a9b5f2994e44`.
1
+ > Source snapshot: official TiSynthEngine documentation at commit `9d74aa45f84d3f2822d4aab76874efe37d81257f` (2026-11-03). Published module 2.0.0. Upstream `documentation/api.md` SHA-256: `ca5b7f1a44ef79543399fd1a40fa83024c190c086b6412fb79dc54de91a2a329`.
2
2
 
3
3
  # API reference
4
4
 
5
5
  Module id: `ti.synthengine`
6
6
 
7
- Public API version: `1.0.0`
7
+ Module version: `2.0.0`. Read the [Sound object reference](sound-api.md) for reusable definitions, playback controls and property units. This page documents the global controls and direct native parameters behind those interpretations.
8
8
 
9
- Android and iOS expose the same nineteen methods, one event and waveform values. Option objects are strict: an unknown key is rejected with `unsupported_option`. Reported metadata and rejection diagnostics have platform differences; this reference does not promise sample-identical rendering between Android and iOS.
9
+ Android and iOS expose the same public methods and waveform values. Pattern controls emit `end`; direct native sequences emit `sequenceend`. Option objects are strict: an unknown key is rejected with `unsupported_option`. Reported metadata and rejection diagnostics have platform differences; this reference does not promise sample-identical rendering between Android and iOS.
10
10
 
11
11
  <!-- TOC-START -->
12
12
  ## Contents
13
13
 
14
- - [Native attack variation (local package 1.1.0)](#native-attack-variation-local-package-110)
14
+ - [Sound interface](#sound-interface)
15
+ - [Native attack variation](#native-attack-variation)
15
16
  - [Return values](#return-values)
16
17
  - [`getDefaults()`](#getdefaults)
17
18
  - [Limits](#limits)
18
19
  - [`start(options)`](#startoptions)
19
20
  - [`noteOn(options)`](#noteonoptions)
20
21
  - [`noteOff(options)`](#noteoffoptions)
21
- - [`updateVoice(options)`](#updatevoiceoptions)
22
+ - [`updateNote(options)`](#updatenoteoptions)
23
+ - [`updateVoice(options)` (deprecated)](#updatevoiceoptions-deprecated)
22
24
  - [`playTone(options)`](#playtoneoptions)
25
+ - [`repeatTone(options)`](#repeattoneoptions)
26
+ - [Finite-tone repetition](#finite-tone-repetition)
23
27
  - [Native sequence transport](#native-sequence-transport)
24
28
  - [`setMasterProcessing(options)`](#setmasterprocessingoptions)
25
29
  - [Additional voice modulation](#additional-voice-modulation)
@@ -32,21 +36,43 @@ Android and iOS expose the same nineteen methods, one event and waveform values.
32
36
  - [Strict validation](#strict-validation)
33
37
  - [Binaural front-plane positioning](#binaural-front-plane-positioning)
34
38
  - [`getStats()`](#getstats)
35
- - [`setStatsEnabled(enabled)`](#setstatsenabledenabled)
39
+ - [`enableStats()` and `disableStats()`](#enablestats-and-disablestats)
40
+ - [`setStatsEnabled(enabled)` (deprecated)](#setstatsenabledenabled-deprecated)
36
41
  - [`resetStats()`](#resetstats)
37
42
  - [Routing and voice priority](#routing-and-voice-priority)
38
43
 
39
44
  <!-- TOC-END -->
40
45
 
41
- ## Native attack variation (local package 1.1.0)
46
+ ## Sound interface
42
47
 
43
- The optional `variation` dictionary is accepted by `playTone`, `playChord`, `noteOn`, sequence `voice` and step `overrides`. Public API compatibility remains `1.0.0`. Both platforms advertise `capabilities.variation: true` and `variationSchemaVersion: 1`; consumers must require both before sending the dictionary. The published 1.0.2 packages do not support it. See [native variation](native-variation.md) for fields, source restrictions, seeds and examples. Native `amount` is linear; Studio divides its displayed percentage by 100. `updateVoice` rejects the dictionary. An override replaces the whole inherited dictionary, and an amount-zero dictionary still validates its ranges.
48
+ | Method | Result |
49
+ | --- | --- |
50
+ | `getContract()` | Installed module version, capabilities, units and structural property allowlists |
51
+ | `createSound({ name?, sound?, playback? })` | A reusable validated Sound; no audio starts |
52
+ | `Sound.play(changes?)` | Finite acceptance or an independent note/repetition/pattern control |
53
+ | `Sound.noteOn(changes?)` | One held-note control, or `false` on operational rejection |
54
+ | `Sound.set({ sound?, playback? })` | Atomic stored revision for future executions |
55
+ | `Sound.getPreset()` | Detached authored definition |
56
+ | Note `update(changes)`, `noteOff()` | Edit or release that held note |
57
+ | Repetition `update(changes)`, `stop()` | Edit future finite attacks or cancel repetition |
58
+ | Pattern `update(changes)`, `stop()`, `getState()` | Edit, stop or inspect that pattern |
59
+ | Pattern `addEventListener('end', fn)`, `removeEventListener('end', fn)` | Subscribe to that pattern's completion or stopping |
60
+ | `setProcessing(options)` | Shared effects with delay `time` and reverb `decay` in milliseconds |
61
+ | `setVolume(number)`, `setTempo(number)` | Global gain and beats per minute |
62
+
63
+ Sound definitions split synthesis into `sound` and execution defaults into `playback`. `play()` and `noteOn()` accept flat temporary overrides. Invalid configurations throw before publication; operational playback rejection returns `false`. Finite events end naturally without individual controls. Shutdown invalidates controls while definitions remain reusable.
64
+
65
+ The [Sound reference](sound-api.md) supplies all properties, defaults, units and interpretation restrictions. The direct native methods below remain supported and document the validators and rendering parameters behind each interpretation. Use the [upgrade guide](upgrade-2.0.0.md) for method mappings.
66
+
67
+ ## Native attack variation
68
+
69
+ The optional `variation` dictionary is accepted by `playTone`, `repeatTone`, `playChord`, `noteOn`, sequence `voice` and step `overrides`. Both platforms advertise `capabilities.variation: true` and `variationSchemaVersion: 1`; consumers must require both before sending the dictionary. See [native variation](native-variation.md) for fields, source restrictions, seeds and examples. Native `amount` is linear; Studio divides its displayed percentage by 100. `updateNote` rejects the dictionary. An override replaces the whole inherited dictionary, and an amount-zero dictionary still validates its ranges.
44
70
 
45
71
  ## Return values
46
72
 
47
- `start`, `playTone`, `playChord`, `noteOn`, `noteOff`, `updateVoice`, `setVolume`, `setTempo`, `updateSequence`, `stopSequence` and `setMasterProcessing` return a Boolean. `playSequence` returns a positive sequence identifier, or 0 when rejected. `true` means the request was accepted. `false` means validation failed, the engine was unavailable, or the native command queue was full.
73
+ `start`, `playTone`, `playChord`, `noteOn`, `noteOff`, `updateNote`, `updateVoice`, `updateTone`, `stopTone`, `setVolume`, `setTempo`, `updateSequence`, `stopSequence` and `setMasterProcessing` return a Boolean. `repeatTone` returns a positive tone identifier, or 0 when rejected. Its required `intervalMs` is an integer from 1 to 60,000 ms. `playSequence` returns a positive sequence identifier, or 0 when rejected. `true` means the request was accepted. `false` means validation failed, the engine was unavailable, or the native command queue was full.
48
74
 
49
- `setStatsEnabled` also returns Boolean acceptance. `stopAll`, `resetStats` and `shutdown` do not return a value. `getDefaults` returns the contract object described below. `getStats` returns a new statistics snapshot. `getSequenceState` returns the applied sequence state or `null`.
75
+ `enableStats`, `disableStats` and the deprecated `setStatsEnabled` also return Boolean acceptance. `stopAll`, `resetStats` and `shutdown` do not return a value. `getDefaults` returns the contract object described below. `getStats` returns a new statistics snapshot. `getSequenceState` returns the applied sequence state or `null`.
50
76
 
51
77
  ## `getDefaults()`
52
78
 
@@ -56,12 +82,12 @@ Returns the installed contract without starting the audio stream.
56
82
  var contract = synth.getDefaults()
57
83
  ```
58
84
 
59
- The shared result in the local 1.1.0 candidates has this shape; Android adds the platform metadata described below. Published 1.0.2 packages retain the core contract but omit the variation capability/schema fields:
85
+ The shared result in TiSynth 2.0.0 has this shape; Android adds the platform metadata described below. `version` identifies the installed module, with no independent API version:
60
86
 
61
87
  ```javascript
62
88
  {
63
- apiVersion: '1.0.0',
64
- capabilities: { variation: true },
89
+ version: '2.0.0',
90
+ capabilities: { variation: true, toneRepetition: true },
65
91
  variationSchemaVersion: 1,
66
92
  defaults: {
67
93
  maxVoices: 8,
@@ -123,7 +149,7 @@ The shared result in the local 1.1.0 candidates has this shape; Android adds the
123
149
 
124
150
  `fmDepthEnd`, `detunes`, `spatialX` and `spatialY` have no reported default: their presence changes how the call is interpreted. Supplying a spatial coordinate enables binaural; the other coordinate then defaults to 0 internally. The maximum frequency is `sampleRate * 0.45`. Before the stream opens, the reported fallback is 19,845 Hz. Read the object again after `start()` if the precise device limit matters.
125
151
 
126
- In the local 1.1.0 candidates, both platforms report `capabilities.variation: true` and `variationSchemaVersion: 1`. Android additionally reports `nativeSequence`, `masterProcessing` and `modulations` capability flags and `limits.stepDurationMs`. iOS omits those three older flags and `limits.stepDurationMs` while implementing the corresponding methods and millisecond sequences. Published 1.0.2 packages do not advertise variation; their iOS metadata has no `capabilities` map. Detect a required core method with `typeof synth.playSequence === 'function'` (and the other methods your feature needs); require the capability/schema pair specifically for variation. Both platforms report `limits.maxSequences`, `limits.priority` and `limits.effectSend`. `limits.stepDuration` is in beats on both platforms; millisecond sequences accept `4000 / sampleRate` through 60,000 even when `limits.stepDurationMs` is absent.
152
+ Both platforms report `capabilities.variation: true` and `variationSchemaVersion: 1`. Android also reports `nativeSequence`, `masterProcessing` and `modulations` capability flags and `limits.stepDurationMs`. iOS omits those three flags and `limits.stepDurationMs` while implementing the corresponding methods and millisecond sequences. Detect a required core method with `typeof synth.playSequence === 'function'` (and the other methods your feature needs); require the capability/schema pair specifically for variation. Both platforms report `limits.maxSequences`, `limits.priority` and `limits.effectSend`. `limits.stepDuration` is in beats on both platforms; millisecond sequences accept `4000 / sampleRate` through 60,000 even when `limits.stepDurationMs` is absent.
127
153
 
128
154
  ## Limits
129
155
 
@@ -131,6 +157,8 @@ All ranges include their endpoints. `getDefaults().limits` uses `{ min, max }` o
131
157
 
132
158
  | Field | Minimum | Maximum | Unit or rule |
133
159
  | --------------------------- | ------------------: | ------------------: | ------------------------------------------------------------------------- |
160
+ | `maxToneRepetitions` | 1 | 32 | fixed independent repetition capacity |
161
+ | `intervalMs` | 1 | 60,000 | integer milliseconds between finite-tone attacks |
134
162
  | `maxSequences` | 1 | 4 | preallocated sequence capacity |
135
163
  | `priority` | 0 | 3 | integer admission priority |
136
164
  | `effectSend` | 0 | 1 | explicit processed share |
@@ -186,7 +214,7 @@ On iOS, `playback` ignores the silent switch and interrupts other apps' audio. `
186
214
 
187
215
  ## `noteOn(options)`
188
216
 
189
- Starts a sustained native voice. Detect `typeof synth.noteOn === 'function'`, `noteOff` and `updateVoice` before enabling their controls; `apiVersion` is `1.0.0`. There is no duration limit and no JavaScript timer holds the note.
217
+ Starts a sustained native voice. Check `typeof synth.noteOn === 'function'`, `noteOff` and `updateNote` before enabling their controls. Read the installed module version from `getDefaults().version`. The note has no duration limit and needs no JavaScript timer to remain held.
190
218
 
191
219
  | Field | Rule |
192
220
  | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
@@ -228,7 +256,7 @@ Repeating a public sustained note can inherit the oscillator phase and pre-envel
228
256
 
229
257
  Begins release from the current level, including during attack or decay. Repeated release requests never restart or lengthen a release. An ended or stolen ID is a no-op. `true` reports validation and queue publication, not proof of an audible voice. On `false` from a full queue, an instrument should retry twice at 16 ms intervals, then shut down and report failure if release still cannot be published.
230
258
 
231
- ## `updateVoice(options)`
259
+ ## `updateNote(options)`
232
260
 
233
261
  | Field | Rule |
234
262
  | ------------------------ | -------------------------------------------------------------------------------- |
@@ -245,26 +273,32 @@ Begins release from the current level, including during attack or decay. Repeate
245
273
  | `spatialX`, `spatialY` | Target position for a spatial held voice; omitted coordinate stays unchanged. |
246
274
  | `effectSend` | Explicit processed share, 0-1, overriding category selection. |
247
275
 
248
- At least one change is required. An incompatible change rejects the entire request when the ID is known; unknown/ended IDs accept publication as a no-op. The callback also checks source compatibility before applying a command. Changed continuous parameters ramp linearly from their current values over 10 ms. New targets retarget that ramp without restarting oscillator phase or the envelope. Unchanged parameters retain their current ramps. This also works during release.
276
+ Supply at least one change. An incompatible change rejects the entire request when the ID is known; unknown/ended IDs accept publication as a no-op. The callback also checks source compatibility before applying a command. Changed continuous parameters ramp linearly from their current values over 10 ms. A new target redirects the ramp without restarting oscillator phase or the envelope. Unchanged parameters keep their current ramps. These edits also work during release.
249
277
 
250
278
  Pitch updates are absolute: an initial `detune` is already included in the note's starting frequency. To edit tuning later, compute the desired effective frequency in the app and send `frequency`. There is no `detune` update field. Filter type, ADSR, quantization and wavetable require a new attack. Noise clock, low-pass cutoff, send and compatible stereo/spatial position support live 10 ms updates.
251
279
 
252
280
  ```javascript
253
281
  if (typeof synth.noteOn !== 'function' || typeof synth.noteOff !== 'function' ||
254
- typeof synth.updateVoice !== 'function') throw new Error('Sustained voices unavailable')
282
+ typeof synth.updateNote !== 'function') throw new Error('Sustained voices unavailable')
255
283
  if (!synth.noteOn({ voiceId: 1, note: 'C4', attack: 8, decay: 180, sustain: 0.6,
256
284
  release: 300, filterType: 'lowpass', filterHz: 1200, volume: 0.25 })) {
257
285
  throw new Error('Note rejected')
258
286
  }
259
- synth.updateVoice({ voiceId: 1, filterHz: 3600 })
287
+ synth.updateNote({ voiceId: 1, filterHz: 3600 })
260
288
  synth.noteOff({ voiceId: 1 })
261
289
  ```
262
290
 
263
- Use `stopAll()` for panic: it stops every sequence, fades sounding voices and invalidates effect histories. Owner close should call `shutdown()` after cancelling app-owned UI/retry timers.
291
+ Use `stopAll()` for panic: it stops every sequence, cancels finite-tone repetitions, fades sounding voices and invalidates effect histories. Owner close should call `shutdown()` after cancelling app-owned UI/retry timers.
264
292
 
265
293
 
294
+ ## `updateVoice(options)` (deprecated)
295
+
296
+ Compatibility alias of [`updateNote(options)`](#updatenoteoptions), with identical validation, partial updates, 10 ms transitions, `voiceId` ownership and ended/stolen-ID behavior. Use `updateNote` in new code. The alias remains available without repeated runtime warnings or a scheduled removal date.
297
+
266
298
  ## `playTone(options)`
267
299
 
300
+ `playTone` always plays one finite pitch and returns Boolean acceptance. Its closed options reject `intervalMs` with `false`. Use `repeatTone` for native repetition.
301
+
268
302
  Plays one pitch. It uses 440 Hz when neither pitch field is present. Note names accept C through B or DO, RE, MI, FA, SOL, LA, SI and TI, followed by optional `#` or lowercase `b` and one octave digit from 0 to 9. Letters are case-insensitive, the omitted octave is 4, and A4 is 440 Hz. A valid named pitch is limited to the active frequency ceiling when rendered; an out-of-range numeric `frequency` is rejected.
269
303
 
270
304
  | Key | Type | Default | Description |
@@ -307,7 +341,7 @@ Plays one pitch. It uses 440 Hz when neither pitch field is present. Note names
307
341
 
308
342
  Omitted attack/release values are shortened to fit an event smaller than the default envelope. Explicit values must satisfy the documented envelope rules. `getDefaults()` reports the baseline defaults, not these per-event adjustments.
309
343
 
310
- In the current 1.0.2/1.1.0 implementations, the iOS timed tone/chord command builder
344
+ In the native implementation, the iOS timed tone/chord command builder
311
345
  initially rounds accepted zero attack/release values to one sample, before
312
346
  trimming them to the event duration budget. Android retains zero samples.
313
347
  Validator acceptance does not imply identical zero-edge rendering. Held notes
@@ -315,6 +349,60 @@ have the separately documented 3 ms minimum.
315
349
 
316
350
  The controls from `decay` to `detune` are described in [Expressive controls](#expressive-controls), including the rules that tie them together.
317
351
 
352
+ ## `repeatTone(options)`
353
+
354
+ Returns a positive `toneId`, or 0 on rejection. Requires integer `intervalMs` from 1 to 60,000 ms; missing or invalid intervals return 0. All remaining options use the same finite-tone contract as `playTone`, with the same admission, envelope, sweeps, gain and routes. Repetition uses neither sustained nor sequence voices.
355
+
356
+ Always supply exact integer milliseconds. The current Apple validator tolerates extremely near-integer values: for example, `1.0000000001` is accepted and converted to `1`, while Android rejects it. This is a native validation discrepancy, not an additional supported interval format; do not rely on that tolerance.
357
+
358
+ | Key | Type | Rule |
359
+ | ----------------- | -------- | ------------------------------------------------- |
360
+ | `intervalMs` | integer | Required; 1-60,000 ms |
361
+ | Other tone fields | as above | Same fields and validation as `playTone(options)` |
362
+
363
+ ## Finite-tone repetition
364
+
365
+ Both platforms advertise `capabilities.toneRepetition: true`, `limits.maxToneRepetitions: { min: 1, max: 32 }` and `limits.intervalMs: { min: 1, max: 60000 }`. These 32 repetition slots are independent of the four sequence slots; attacks still share the configured voice pool. Reserve for overlaps, active voices and release tails if the application promises no stealing. Native priority policy remains in force. A positive `toneId` accepts the repeating recipe; it does not guarantee admission of every future attack.
366
+
367
+ The first attack occurs at the first render opportunity. Later attacks use the audio sample clock, retain fractional samples across intervals and blocks, and do not follow `setTempo`. If voice capacity rejects an attack, it is dropped without catch-up and without consuming a variation selection. Every admitted attack finishes its original duration even after `stopTone`.
368
+
369
+ ```javascript
370
+ var toneId = synth.repeatTone({ frequency: 1380, frequencyEnd: 340,
371
+ duration: 105, attack: 2, release: 40, waveType: synth.getDefaults().waveTypes.SQUARE,
372
+ volume: 0.1, intervalMs: 150 })
373
+ if (!toneId) throw new Error('Repeating tone rejected')
374
+ // Edits enter at the next scheduled attack, without adding an immediate attack.
375
+ if (!synth.updateTone({ toneId: toneId, intervalMs: 120, volume: 0.08 })) {
376
+ throw new Error('Tone edit rejected')
377
+ }
378
+ // On release/cancel/preset change/owner close:
379
+ if (!synth.stopTone({ toneId: toneId })) throw new Error('Tone stop rejected')
380
+ ```
381
+
382
+ Independent JavaScript calls remain supported and always keep their Boolean return through `playTone`. A game can fire in response to events or deliberately schedule a fixed count of standalone calls, for example:
383
+
384
+ ```javascript
385
+ var recipe = { frequency: 880, duration: 100, attack: 2, release: 30, volume: 0.1 }
386
+ function fireStandalone() {
387
+ if (!synth.playTone(recipe)) Ti.API.warn('Independent tone rejected')
388
+ }
389
+ fireStandalone()
390
+ setTimeout(fireStandalone, 200)
391
+ setTimeout(fireStandalone, 400)
392
+ ```
393
+
394
+ Those three calls use JavaScript timing and independent attack selection. For an ongoing native repetition, call `repeatTone` with `intervalMs` once and use its tone ID. Owner teardown must cancel any application timers it creates.
395
+
396
+ ### `updateTone({ toneId, ...changes })`
397
+
398
+ Returns Boolean acceptance. Requires a known positive repetition identifier and at least one change. Accepts partial finite-tone options, including `intervalMs`, and merges them with the latest accepted recipe before validating the entire result. Supplying `note` replaces an earlier `frequency`; supplying `frequency` replaces an earlier `note`. Supplying both is invalid. Invalid edits leave the last accepted recipe intact.
399
+
400
+ An accepted edit affects the next scheduled attack without moving its deadline. The new interval sets the gap after that attack. Active attacks keep their original configuration. A supplied `variation` dictionary replaces the whole previous dictionary; `{ amount: 0 }` disables future variation. Each repetition keeps its own seed and admitted-attack counter. Edits preserve the stream unless the seed changes, which restarts it. Unknown or retired IDs reject edits.
401
+
402
+ ### `stopTone({ toneId })`
403
+
404
+ Returns Boolean acceptance. Accepts only a known positive repetition identifier. Cancels future attacks at the next render block, including before the first attack, while admitted finite voices finish completely. Stop publication is independent of the ordinary command queue and remains available when that queue is full. Unknown or retired IDs return `false`. There is no `toneend` event. Identifiers are never reused during the module lifetime, including shutdown/start. `stopAll`, shutdown and destruction invalidate all repetitions and their pending edits.
405
+
318
406
  ## Native sequence transport
319
407
 
320
408
  `start({ maxSequences: 1 })` preallocates capacity for 1-4 independent sequences; the default is 1. All sequences, timed sounds and held notes share one renderer and the same pool of at most 32 voices. No sequence or category reserves voices.
@@ -374,7 +462,7 @@ synth.stopSequence({ sequenceId: musicId })
374
462
 
375
463
  Selected audio follows fixed input gain → chorus → ping-pong delay → reverb → master volume → optional saturation. Dry audio receives the same global input/master gains, then recombines before one final sample-peak protector with no retained samples. Independent notes never trigger voice-count normalization. The limiter remains the final protection.
376
464
 
377
- Requires a running engine and returns Boolean acceptance. Partial updates merge into the last accepted processing configuration, validate the complete result and publish atomically. Continuous controls ramp over 10 ms. Unknown fields, numeric booleans and non-finite values reject the whole edit. Detect method presence before use.
465
+ This method requires a running engine and returns Boolean acceptance. It merges partial updates into the last accepted processing configuration, validates the complete result and publishes it atomically. Continuous controls ramp over 10 ms. Unknown fields, numeric booleans and non-finite values reject the whole edit. Check that the method exists before calling it.
378
466
 
379
467
  | Section | Fields and initial values |
380
468
  | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -385,15 +473,15 @@ Requires a running engine and returns Boolean acceptance. Partial updates merge
385
473
  | `reverb` | `enabled`: false; `decay`: 0.2-8 seconds, initially 1.5; `damping`: 0-1, initially 0.5; `mix`: 0-1, initially 0.2. Stereo damped comb/allpass Schroeder network. |
386
474
  | `saturation` | `enabled`: false; `drive`: 1-4, initially 1; `mix`: 0-1, initially 1. Creative soft waveshaping that changes timbre. |
387
475
 
388
- Delay sync accepts `off`, `quarter`, `eighth`, `sixteenth`, `quarterTriplet`, `eighthTriplet` or `sixteenthTriplet`. Sync follows the native transport BPM, initially 120. Disabling an effect gradually closes its input and drains its tail, then restores exact bypass. Tails are bounded to 30 seconds with a final fade. `stopAll()` performs panic: it fades voices, stops the new transport and invalidates effect histories in bounded work. `shutdown()` stops the stream and frees buffers outside the callback.
476
+ Delay sync accepts `off`, `quarter`, `eighth`, `sixteenth`, `quarterTriplet`, `eighthTriplet` or `sixteenthTriplet`. Sync follows the native transport BPM, initially 120. Disabling an effect gradually closes its input and drains its tail, then restores exact bypass. Tails are bounded to 30 seconds with a final fade. `stopAll()` performs panic: it fades voices, stops the transport and invalidates effect histories in bounded work. `shutdown()` stops the stream and frees buffers outside the callback.
389
477
 
390
478
  Processing uses one shared chain for the selected categories. `applyTo` is an array drawn from `music`, `effects`, `ui` and `ambience`; default all four. Each event or held voice uses `bus: 'effects'` by default. Optional `effectSend: 0…1` overrides category selection for that voice. Input gain and master volume remain global. Selected audio receives chorus, delay, reverb and optional saturation, then recombines with dry audio before one final limiter. Send changes ramp over 10 ms.
391
479
 
392
- Defaults keep all effects and saturation disabled, input gain at unity, and preserve the earlier render exactly. Enabling saturation is an explicit sound-design choice. Output protection is still required with dense voices and effect feedback.
480
+ Defaults keep all effects and saturation disabled, input gain at unity, and route audio through the unprocessed path. Enabling saturation is an explicit sound-design choice. Output protection is still required with dense voices and effect feedback.
393
481
 
394
482
  ## Additional voice modulation
395
483
 
396
- `noteOn` and `updateVoice` accept `modulations`, an array of up to two LFO objects. An empty array disables them. Omission on update preserves the previous configuration. Each object accepts only these fields:
484
+ `noteOn` and `updateNote` accept `modulations`, an array of up to two LFO objects. An empty array disables them. Omission on update preserves the previous configuration. Each object accepts only these fields:
397
485
 
398
486
  | Field | Rule |
399
487
  | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -403,7 +491,7 @@ Defaults keep all effects and saturation disabled, input gain at unity, and pres
403
491
  | `depth` | 0-1, default 0; scales up to four cutoff octaves, 0.45 PWM offset or ten FM-index units. Two contributions sum with safe clamping. |
404
492
  | `sync` | Same musical values as delay; default off. Synced rates follow native tempo (up to 30 Hz at 300 BPM in sixteenth-note triplets). |
405
493
 
406
- Edits preserve oscillator/LFO phases. Continuous modulation contributions ramp over 10 ms; cutoff stays within 20 Hz and the frequency maximum, PWM within 0.05-0.95, FM index within 0-10. Initial modulations may also be inherited by sequence voices and overridden per step. These two slots coexist with the independent `lfoFreq`/`lfoDepth` pitch vibrato; they are not accepted by timed tones, chords .
494
+ Edits preserve oscillator/LFO phases. Continuous modulation contributions ramp over 10 ms; cutoff stays within 20 Hz and the frequency maximum, PWM within 0.05-0.95, FM index within 0-10. Sequence voices can inherit initial modulations, and steps can override them. These two slots coexist with the independent `lfoFreq`/`lfoDepth` pitch vibrato. Timed tones and chords reject them.
407
495
 
408
496
  ## `playChord(options)`
409
497
 
@@ -603,7 +691,7 @@ Listen with stereo headphones. Height perception depends on the listener, headph
603
691
 
604
692
  Use `typeof synth.getStats === 'function'` to check that the expected module is installed before enabling a diagnostics panel.
605
693
 
606
- Returns a new snapshot without resetting counters. Available before start and after shutdown. Occupancy is the last completed render block; queued commands are read at query time. No audio-thread allocation, locks or waiting are added.
694
+ This method returns a new snapshot without resetting counters and is available before start and after shutdown. Occupancy comes from the last completed render block; queued commands are read at query time. The query adds no audio-thread allocation, locks or waiting.
607
695
 
608
696
  | Field | Meaning |
609
697
  | -------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
@@ -631,7 +719,7 @@ Returns a new snapshot without resetting counters. Available before start and af
631
719
  | `bufferFrames` | Android Oboe output buffer size; otherwise `null`. This is distinct from `callbackFrames`. |
632
720
  | `estimatedOutputLatencyMs` | Android timestamp-based stream-to-DAC estimate, when valid; otherwise `null` (always `null` on this iOS backend). |
633
721
 
634
- In the current 1.0.2/1.1.0 implementations, iOS early rejections in tempo/sequence operations and a
722
+ In the native implementation, iOS early rejections in tempo/sequence operations and a
635
723
  stopped `setMasterProcessing` call can bypass rejection counters. Incompatible
636
724
  held spatial-mode or modulation updates also have uncounted iOS returns.
637
725
  Android counts more of these cases, but an unknown/retired `stopSequence` return
@@ -642,9 +730,13 @@ rejected request. These counters are diagnostics; the API return is authoritativ
642
730
 
643
731
  Render load is a fraction: multiply by 100 for a percentage. For example, `0.012` is 1.2% of the block budget, while `1.0` is 100%. An open stream still renders silent blocks, including output/limiter work and enabled signal measurements, so no active voices does not imply zero load. Callback elapsed time varies with scheduling and system activity; displayed decimal places do not establish measurement accuracy. Compare the same configuration on the same device. Percentages from a simulator and a phone do not provide a direct performance comparison.
644
732
 
645
- ## `setStatsEnabled(enabled)`
733
+ ## `enableStats()` and `disableStats()`
734
+
735
+ Both return Boolean acceptance. `enableStats()` enables optional timing and signal measurements; `disableStats()` disables them. Default `false`. Enabling starts a new diagnostic window, without restarting audio. Calling `enableStats()` again also starts a fresh window. Disabling hides timing and signal fields immediately. Basic counters remain available in either mode. The setting persists across shutdown/start, while a new engine starts with fresh counters.
736
+
737
+ ## `setStatsEnabled(enabled)` (deprecated)
646
738
 
647
- Accepts a Boolean directly, returns Boolean acceptance. Default `false`. Enabling starts a new diagnostic window, without restarting audio. Calling it again with `true` also starts a fresh window. Disabling hides timing and signal fields immediately. Basic counters remain available in either mode. The setting persists across shutdown/start, while a new engine starts with fresh counters.
739
+ Compatibility alias for the same statistics logic. Requires a Boolean and returns Boolean acceptance. Use `enableStats()` or `disableStats()` in new code. It remains available without repeated runtime deprecation warnings or a scheduled removal date.
648
740
 
649
741
  ## `resetStats()`
650
742
 
@@ -652,11 +744,11 @@ Returns nothing. Resets accumulated counters and maxima without stopping sounds,
652
744
 
653
745
  ```javascript
654
746
  // Optional diagnostics; poll at 5 Hz and stop the timer with the owner.
655
- if (synth.setStatsEnabled(true)) {
747
+ if (synth.enableStats()) {
656
748
  var statsTimer = setInterval(function () {
657
749
  Ti.API.info(JSON.stringify(synth.getStats()))
658
750
  }, 200)
659
- // Later: clearInterval(statsTimer); synth.setStatsEnabled(false)
751
+ // Later: clearInterval(statsTimer); synth.disableStats()
660
752
  }
661
753
  ```
662
754
 
@@ -664,4 +756,4 @@ if (synth.setStatsEnabled(true)) {
664
756
 
665
757
  `playTone`, `playChord`, `noteOn` and sequence voices accept `bus`, `effectSend` and `priority`. Categories are `music`, `effects` (default), `ui` and `ambience`. An omitted send follows `setMasterProcessing({ applyTo })`; an explicit 0-1 send replaces that selection. Priority is integer 0-3, default 0. When the pool fills, the lowest eligible priority is replaced first; a lower-priority event cannot steal a higher-priority voice. Existing release/age tie-breakers remain. Chords are admitted or dropped completely; no voices are reserved. `getStats().priorityDropCount` counts these drops; `rejectedPublicationCount` counts unavailable sequence capacity/banks separately from output diagnostics.
666
758
 
667
- `updateVoice` additionally accepts `noiseRate`, `lowPassHz`, `pan`, `spatialX`, `spatialY` and `effectSend`, with 10 ms transitions. Noise rate 0 means white noise; low-pass 0 means bypass. Noise phase, oscillator phase and spatial history survive compatible edits. Pan updates require a stereo voice; coordinates require a voice started with spatial coordinates. An omitted coordinate retains its previous value. The source/filter type still requires a new attack. Updates are generation-scoped so queued old commands cannot change a later attack sharing the public voice ID.
759
+ `updateNote` also accepts `noiseRate`, `lowPassHz`, `pan`, `spatialX`, `spatialY` and `effectSend`, with 10 ms transitions. Noise rate 0 means white noise; low-pass 0 means bypass. Noise phase, oscillator phase and spatial history survive compatible edits. Pan updates require a stereo voice; coordinates require a voice started with spatial coordinates. An omitted coordinate retains its previous value. The source/filter type still requires a new attack. Updates are generation-scoped so queued old commands cannot change a later attack sharing the public voice ID.
@@ -1,8 +1,8 @@
1
- > Source snapshot: official TiSynthEngine documentation at commit `b07c97c762be5905f132cecf33e9d60ea0c21316` (2026-11-02). Published 1.0.2; local 1.1.0 candidates; public API 1.0.0. Upstream `documentation/audio-engine.md` SHA-256: `a3231af42efb2e3bf0ec457083f4c1128728f09c4b2f52d1cbf1df1e76b04886`.
1
+ > Source snapshot: official TiSynthEngine documentation at commit `9d74aa45f84d3f2822d4aab76874efe37d81257f` (2026-11-03). Module 2.0.0. Upstream `documentation/audio-engine.md` SHA-256: `d5cabb81a1f36d4f32d13d8a03bd492a73c3e34c1c0888bb8af9dfa15a4e4d41`.
2
2
 
3
3
  # Audio engine
4
4
 
5
- The native signal path explains how mixing profiles and voice capacity affect the output. Use this reference when choosing those settings or investigating device behavior; it is optional for basic playback.
5
+ This reference explains how the native signal path, mixing profiles and voice capacity affect the output. Use it to choose those settings or investigate device behavior. It is optional for basic playback.
6
6
 
7
7
  <!-- TOC-START -->
8
8
  ## Contents
@@ -29,6 +29,7 @@ The native signal path explains how mixing profiles and voice capacity affect th
29
29
  - [Sustained voices](#sustained-voices)
30
30
  - [Native transport and processing](#native-transport-and-processing)
31
31
  - [Unified transport acceptance boundaries](#unified-transport-acceptance-boundaries)
32
+ - [Native finite-tone repetition](#native-finite-tone-repetition)
32
33
 
33
34
  <!-- TOC-END -->
34
35
 
@@ -68,7 +69,7 @@ The engine has six sources:
68
69
  | `NOISE` | White or variable-rate sample-and-hold pseudo-random generator. |
69
70
  | `WAVETABLE` | Caller-supplied 2 to 64-sample stepped cycle. |
70
71
 
71
- Pitch can move linearly from `frequency` to `frequencyEnd`. A sine LFO adds `lfoDepth * sin(lfoPhase)` in hertz before the oscillator advances. This makes vibrato depth absolute in hertz rather than cents.
72
+ Pitch can move linearly from `frequency` to `frequencyEnd`. A sine LFO adds `lfoDepth * sin(lfoPhase)` in hertz before the oscillator advances. Vibrato depth is therefore an absolute value in hertz, rather than a value in cents.
72
73
 
73
74
  Noise rate advances an independent phase accumulator. A zero `noiseRate` uses one new pseudo-random value per output sample; a positive rate holds that value until the accumulator crosses the next update. `noiseRateEnd` changes the increment linearly over the event.
74
75
 
@@ -78,7 +79,7 @@ The source is optionally quantized to the selected `bitDepth` (2 to 16 bits) and
78
79
 
79
80
  A `SINE` voice with a positive `fmDepth`, or with `fmDepthEnd`, is phase modulated: the carrier computes `sin(2 * pi * phase + depth * sin(2 * pi * modPhase))`. The modulator phase advances by the carrier's instantaneous phase increment times `fmRatio`, so pitch sweeps and vibrato keep the ratio. The index moves linearly from `fmDepth` to `fmDepthEnd` over the event. With a depth of exactly 0 and no end, the voice takes the plain sine path.
80
81
 
81
- The modulated sine has constant amplitude, so event gain is unchanged. Nothing band-limits the sidebands, so a high carrier with a large index aliases.
82
+ The modulated sine has constant amplitude, so event gain is unchanged. The sidebands are not band-limited; a high carrier with a large index aliases.
82
83
 
83
84
  ### State-variable filter
84
85
 
@@ -172,15 +173,15 @@ Both ports use a 128-slot single-consumer ring buffer. Producers are serialized
172
173
 
173
174
  `playChord()` publishes its note data and advances the queue index once. The consumer receives the complete event or nothing. `stopAll` and `shutdown` have a control fallback so cleanup is not lost under queue pressure.
174
175
 
175
- `playSequence` runs inside the sample-clock callback; JavaScript publishes entire configurations rather than individual notes. Scheduled releases execute independently of public command-queue capacity. `sequenceend` records are delivered outside the callback and retain their sequence identifier when slots are reused.
176
+ `playSequence` runs inside the sample-clock callback; JavaScript publishes entire configurations instead of individual notes. Scheduled releases execute independently of public command-queue capacity. `sequenceend` records are delivered outside the callback and retain their sequence identifier when slots are reused.
176
177
 
177
178
  ## Shared DSP core
178
179
 
179
- The envelope, the state-variable filter, the sweep helpers, the FM sine and the cents conversion live once, in `shared/synth_core.h`. It is plain C, header-only and allocation-free. The Android renderer includes it from `android/jni/SynthOboeEngine.cpp` and the iOS renderer from `ios/Classes/TiSynthengineModule.m`, so both platforms run the same code. Backend voice integration and mixing remain separate per platform. Master ramps and final stereo protection share `shared/synth_output.h`. Held envelopes/ramps, transport, master effects, extra modulation and compatible retrigger support also have shared C cores.
180
+ The envelope, state-variable filter, sweep helpers, FM sine and cents conversion share one implementation in `shared/synth_core.h`. It is plain C, header-only and allocation-free. The Android renderer includes it from `android/jni/SynthOboeEngine.cpp` and the iOS renderer from `ios/Classes/TiSynthengineModule.m`, so both platforms run the same code. Backend voice integration and mixing remain separate per platform. Master ramps and final stereo protection share `shared/synth_output.h`. Held envelopes/ramps, transport, master effects, extra modulation and compatible retrigger support also have shared C cores.
180
181
 
181
182
  ## Native per-attack variation
182
183
 
183
- Local 1.1.0 candidates add optional `variation`; published 1.0.2 packages reject it. Check both `capabilities.variation === true` and `variationSchemaVersion === 1`, then select bounded values at admitted attacks. The chosen configuration stays stable during that voice; existing sweeps, envelopes and explicit live edits keep their normal roles. Parsing prepares fixed-size state outside audio; the callback uses bounded shared arithmetic without allocation or locks. See [native variation](native-variation.md) for supported locations, source constraints, inheritance and seeds, and [technical verification](native-variation-validation.md) for measured compatibility and limits.
184
+ TiSynth supports optional `sound.variation` on new attacks. Check both `capabilities.variation === true` and `variationSchemaVersion === 1`, then select bounded values at admitted attacks. The chosen configuration stays stable during that voice; existing sweeps, envelopes and explicit live edits keep their normal roles. Parsing prepares fixed-size state outside audio; the callback uses bounded shared arithmetic without allocation or locks. See [native variation](native-variation.md) for supported locations, source constraints, inheritance and seeds, and [technical verification](https://github.com/macCesar/TiSynthEngine/blob/9d74aa45f84d3f2822d4aab76874efe37d81257f/documentation/native-variation-validation.md) for measured compatibility and limits.
184
185
 
185
186
  ## Android implementation
186
187
 
@@ -200,7 +201,7 @@ The module targets iOS 15 or newer. The module archive contains device, simulato
200
201
 
201
202
  ## Latency
202
203
 
203
- The final output stage retains zero samples, removing the previous mandatory 10 ms lookahead. End-to-end latency still includes the hardware and operating-system output buffers, route transport (including Bluetooth), and JavaScript publication plus callback-boundary latency for a newly submitted event. Accepted sequence steps then follow the sample clock. Attack envelopes and spatial/effect processing retain their existing timing.
204
+ The final output stage retains zero samples. End-to-end latency still includes the hardware and operating-system output buffers, route transport (including Bluetooth), and JavaScript publication plus callback-boundary latency for a newly submitted event. Accepted sequence steps then follow the sample clock. Attack envelopes and spatial/effect processing retain their existing timing.
204
205
 
205
206
  Android reports a valid timestamp-based stream-to-DAC estimate without adding the former lookahead. Unavailable estimates remain `null`; this iOS backend continues to report `null`.
206
207
 
@@ -295,53 +296,11 @@ Android ARM64 reuses coefficients across eight consecutive samples, with four-sa
295
296
 
296
297
  ## Own-platform performance verification
297
298
 
298
- The measurements in this section predate native variation and describe the archived revisions in the cited reports. They do not measure the local 1.1.0 candidate. Its separate [variation verification](native-variation-validation.md) records compatibility, storage and callback checks.
299
-
300
- The [performance review](performance-review.json) separates measured host improvements, exact compatibility checks and pending device acceptance. Its [raw evidence](performance-review-evidence.zip) retains rejected prototypes and the investigation of idle/stereo regressions. The Apple FIR, stable-control cache and exact idle-effects bypass were evaluated as one atomic candidate and then rejected after physical 32-stereo timing regressed. All three production changes were removed together; the full-score publication and control-memory fixes remain.
301
-
302
- Matched 48 kHz/1024-frame iPad full-load runs completed 600 audio seconds without callback deadline misses, with p95 load upper bounds 0.427 for the reference and 0.383 for the rejected initial candidate. The original Apple callback was restored because 32-stereo bypass p95 regressed reproducibly, including continuous-voice windows. Android Sim20/Sim80 frame presentation remains near 60 FPS, while occasional Sim80 output underruns persist in both versions. Compact 48 kHz iPad memory growth is about 18.2 MiB in both versions; no material RAM saving or physical 192 kHz acceptance is claimed. These results and qualified listening observations are recorded separately in the review.
303
-
304
- `tools/build_ios_render_host.py` compiles the actual Objective-C render-block body and voice/transport helpers on macOS. It replaces hardware/session/Titanium startup and fixes only the noise-seed/disabled-diagnostic clock. Output and semantic state are compared against a separately archived reference from the same platform. This is an Apple renderer test; it does not execute AVAudioEngine or establish iPad scheduling, route recovery, footprint or listening acceptance.
305
-
306
- The commands below reproduce the historical pre-pipeline performance review. Use its archived candidate source when requiring exact PCM/state equality with `7ab3110`; the renderer since 1.0.2 intentionally removes the old final-output delay and changes overload behavior, so it cannot satisfy that earlier exact-output comparison. For the later correction, use the [audio pipeline validation](audio-pipeline-validation.md) protocol: align only the former output delay, compare no-overload PCM within the declared tolerance, and retain overload differences separately. The module's `tools/compare_output_pcm.py` checks the corresponding matched Android/Apple capture sets; host comparisons do not establish physical output latency.
307
-
308
- ```sh
309
- mkdir -p /tmp/synth-reference
310
- git archive 7ab3110 | tar -x -C /tmp/synth-reference
311
- python3 tools/build_ios_render_host.py --source /tmp/synth-reference --output /tmp/apple-reference
312
- python3 tools/build_ios_render_host.py --output /tmp/apple-candidate
313
- python3 tools/compare_ios_render.py /tmp/apple-reference /tmp/apple-candidate
314
- python3 tools/benchmark_performance_review.py --baseline /tmp/apple-reference --candidate /tmp/apple-candidate --output /tmp/apple-performance.json
315
- /tmp/apple-candidate --metadata
316
- ```
317
-
318
- The build tool defaults to the module's Apple Release `-Os` optimization. The comparator streams PCM for every fixture block and full original voice/tail, FIR/filter, limiter, effect-buffer and transport state at fixture checkpoints. Cases include five engine rates, zero/short/irregular blocks, all sources, held updates, replacements, send and position ramps, four sequences, effects, panic, drain and later re-enabling. A deliberately changed phase increment was rejected as a negative control.
319
-
320
- Finish compilation and fidelity checks before timing. The benchmark uses fixed matched warmups and at least ten alternating pairs; CPU and elapsed totals, p95, maxima and process high-water memory remain separate. Investigate repeatable total regressions above 2% or p95 regressions above 5%. Short-process or single-kernel gains do not establish whole-app performance. The Android producer harness exercises the actual JNI publication with supplied JVM array accessors, so its numbers exclude Java validation and VM allocation time.
321
-
322
- Full-score producers reserve a free bank without copying the previous score. Generic partial-bank callers still inherit the latest accepted bank, and publication retains its ownership, validation and mailbox checks. The iOS producer commits an already prepared score once; Android prepares its complete packet at native publication. Empty completion polls and empty metadata pruning avoid transient objects. Accepted edits of unknown iOS IDs do not create retained spatial metadata. Sequence capacity, diagnostics, event delivery cadence and public defaults are unchanged.
323
-
324
- The shared processor check is `cc -std=c99 -O2 -Wall -Wextra -Wpedantic tests/spatial_regression.c -lm -o /tmp/spatial_regression` followed by `/tmp/spatial_regression`. `tests/spatial_engine_host.cpp` uses the same compile flags as `engine_render_host.cpp` and verifies native left/right rendering, elevation changes, stealing and stopping. Java spatial validation is checked by `SynthSpatialContractRegression`; the common Java/iOS corpus also covers spatial acceptance and conflicts.
325
-
326
- ### Reproduce a host CPU comparison
327
-
328
- Compile `tests/spatial_benchmark_host.cpp` with the same Oboe/JDK includes and definitions as `engine_render_host.cpp` above, replacing the source filename and choosing an output such as `/tmp/spatial_benchmark`. Supply a whitespace-separated timeline, one event per line, with these 25 columns in order:
329
-
330
- ```text
331
- timeMs frequency frequencyEnd durationMs attackMs releaseMs decayMs sustain waveType volume noiseRate noiseRateEnd lowPassHz pulseWidth lfoFreq lfoDepth filterTypeIndex filterHz filterHzEnd filterQ fmRatio fmDepth fmDepthEnd pan panEnd
332
- ```
333
-
334
- The filter indices are 0=none, 1=lowpass, 2=highpass and 3=bandpass. This internal harness bypasses public contract validation, so only feed valid event recipes. In binaural mode every event uses its `pan` column as spatialX and a synthetic moving elevation `0.8 * sin(timeMs / 1000)`; the other pan column is irrelevant to that mode. This tests CPU, not the localization of the original timeline.
335
-
336
- ```sh
337
- python3 tools/benchmark_spatial.py /tmp/spatial_benchmark /path/to/timeline.txt
338
- ```
339
-
340
- The script compares stereo and binaural using a warmup and five measured trials, reporting the median by audio processing time. There is no hardware stream or JavaScript bridge. Results include peak level, non-finite/clipped frames, voice replacements and processing time; they do not measure device CPU, latency, underruns, heat or battery.
299
+ Source-specific measurements and reproduction commands are preserved in the [historical engine validation record](https://github.com/macCesar/TiSynthEngine/blob/9d74aa45f84d3f2822d4aab76874efe37d81257f/documentation/audio-engine-validation.md). Current Sound-interface acoustic and runtime checks are recorded in [Sound validation](https://github.com/macCesar/TiSynthEngine/blob/9d74aa45f84d3f2822d4aab76874efe37d81257f/documentation/sound-api-validation.md).
341
300
 
342
301
  ## Engine statistics
343
302
 
344
- The [statistics API](api.md#getstats) has the same shape on both platforms. `shared/synth_stats.h` owns telemetry math and publication. Voice transitions update render-owned occupancy and maxima, including primary FIR draining and external replacement/stop tails. Whole empty groups can then be skipped without changing the signal. Producers are serialized separately from the renderer; querying queue depth does not include future scheduled pattern steps.
303
+ The [statistics API](api.md#getstats) has the same shape on both platforms. `shared/synth_stats.h` owns telemetry math and publication. Voice transitions update render-owned occupancy and maxima, including primary FIR draining and external replacement/stop tails. The renderer can then skip whole empty groups without changing the signal. Producers are serialized separately from the renderer; querying queue depth does not include future scheduled pattern steps.
345
304
 
346
305
  The callback publishes atomic words behind a sequence counter. Readers retry a snapshot if publication overlaps their read; the callback never waits for them. Eight-byte alignment and a compile-time lock-free assertion also cover Android's 32-bit ABIs. Reset/enable requests are consumed at callback boundaries. A new control generation hides stale optional measurements immediately. Shutdown waits for the backend callback to stop before clearing occupancy. Queries allocate result objects on the calling thread. Pattern publication and shutdown share a control monitor; that monitor is never taken by the audio callback.
347
306
 
@@ -357,11 +316,11 @@ python3 tests/run_stats_java.py
357
316
 
358
317
  `tests/stats_engine_host.cpp` uses the Oboe/JDK host flags shown above. It tests native occupancy, reset while playing, saturated queues, binaural drain, shutdown and exact output equality with diagnostics enabled/disabled. CI obtains Oboe 1.11.0 headers and runs this native integration harness alongside the portable shared tests and stopped-engine Java API tests. iOS builds use the same telemetry core, but host Android renders do not execute the iOS audio callback.
359
318
 
360
- `tests/stats_benchmark_host.cpp` extends the spatial replay harness with silence, one/32 voices, replacement pressure, chords, scheduled note publications and short FIR drains in stereo/binaural. Build it twice with identical `-O3` flags: once against the previous engine using `-DSTATS_BASELINE`, and once against the current engine. Run `python3 tools/benchmark_stats.py BASELINE CURRENT` for one warmup and ten alternating trials/configuration, including current snapshots at 5 Hz. The harness verifies identical output hashes across all modes. The [measured comparison and limitations](stats-performance.md) explain what the host percentages establish and which results still require device tests.
319
+ `tests/stats_benchmark_host.cpp` extends the spatial replay harness with silence, one/32 voices, replacement pressure, chords, scheduled note publications and short FIR drains in stereo/binaural. Build it twice with identical `-O3` flags: once against the previous engine using `-DSTATS_BASELINE`, and once against the current engine. Run `python3 tools/benchmark_stats.py BASELINE CURRENT` for one warmup and ten alternating trials/configuration, including current snapshots at 5 Hz. The harness verifies identical output hashes across all modes. The [measured comparison and limitations](https://github.com/macCesar/TiSynthEngine/blob/9d74aa45f84d3f2822d4aab76874efe37d81257f/documentation/stats-performance.md) explain what the host percentages establish and which results still require device tests.
361
320
 
362
321
  ## Sustained voices
363
322
 
364
- `noteOn`, `noteOff` and `updateVoice` use the existing bounded native command queue. Voice storage, smoothing ramps and identity snapshots are preallocated. The callback owns envelopes, oscillator/filter state and replacement decisions; it does not allocate or acquire producer locks. `shared/synth_held.h` supplies the same held ADSR, current-level release and independent 10 ms linear parameter ramps to both renderers. Held attack/decay counters stop advancing after those stages, avoiding sample-counter overflow on long notes.
323
+ `noteOn`, `noteOff` and `updateNote` use the existing bounded native command queue. Voice storage, smoothing ramps and identity snapshots are preallocated. The callback owns envelopes, oscillator/filter state and replacement decisions; it does not allocate or acquire producer locks. `shared/synth_held.h` supplies the same held ADSR, current-level release and independent 10 ms linear parameter ramps to both renderers. Held attack/decay counters stop advancing after those stages, avoiding sample-counter overflow on long notes.
365
324
 
366
325
  IDs are app-supplied positive 32-bit integers. Atomic 32-bit identity snapshots and queued attacks let producers validate update source compatibility without reading mutable DSP state. The callback checks compatibility again. A full queue rejects note release with `false`; the caller owns its bounded retry and shutdown policy. See the [sustained contract](api.md#noteonoptions) for immutable configuration and supported updates.
367
326
 
@@ -372,18 +331,24 @@ Run `tests/held_core_regression.c` with C11 for shared envelope/ramp/identity ch
372
331
 
373
332
  `shared/synth_transport.h` owns up to four independent sequences and one global tempo/beat/millisecond clock; `shared/synth_sequence.h` advances each traversal with compensated sample increments. Beat lengths follow tempo changes; millisecond lengths remain constant. A serialized producer prepares sorted traversal and all pitches before publishing one of three immutable banks. The callback accepts tempo at the block boundary and structure at a step boundary; swing changes at a pair boundary. Transport voices share the sustained pool, retain owner identifiers and absolute note deadlines beyond internal slot reuse and release independently of the public command queue. See the [public transport contract](api.md#native-sequence-transport).
374
333
 
375
- `shared/synth_effects.h` supplies chorus, ping-pong delay, damped Schroeder reverb and optional saturation; `shared/synth_modulation.h` supplies two phase-preserving LFOs. Buffers allocate at engine initialization and free after the audio backend stops. Epoch tags on 16-sample pages invalidate histories during panic without walking delay memory; recovery clears at most one short page on a first write. Effects start disabled and default rendering follows exact bypass. Enabled effects drain on disable, bounded to 30 seconds. No new external DSP dependency is used.
334
+ `shared/synth_effects.h` supplies chorus, ping-pong delay, damped Schroeder reverb and optional saturation; `shared/synth_modulation.h` supplies two phase-preserving LFOs. Buffers allocate at engine initialization and free after the audio backend stops. Epoch tags on 16-sample pages invalidate histories during panic without walking delay memory; recovery clears at most one short page on a first write. Effects start disabled and default rendering follows exact bypass. Enabled effects drain on disable, bounded to 30 seconds. The implementation uses no additional external DSP dependency.
376
335
 
377
336
  Pre-limiter peak and maximum limiter reduction accumulate natively until reset, so polling cannot miss a brief intervention. The callback tracks linear extrema and converts to decibels once per block. Existing output peak/RMS and reduction windows remain approximately 200 ms. Diagnostics are optional; host measurements do not establish device audio deadlines or listening acceptance.
378
337
 
379
- Portable checks: compile and run `tests/sequence_core_regression.c`, `tests/effects_core_regression.c` and `tests/modulation_core_regression.c` with C11, optimization and libm. The transport test checks ten minutes at five sample rates against calculated event times, plus edits, swing, rests, chords and publication pressure. Effects tests check bypass, tails, panic, automation, memory and stability at 16, 44.1, 48, 96 and 192 kHz. On macOS, `tests/studio_contract_ios.m` validates the new iOS configuration contract without Titanium.
338
+ Portable checks: compile and run `tests/sequence_core_regression.c`, `tests/effects_core_regression.c` and `tests/modulation_core_regression.c` with C11, optimization and libm. The transport test checks ten minutes at five sample rates against calculated event times, plus edits, swing, rests, chords and publication pressure. Effects tests check bypass, tails, panic, automation, memory and stability at 16, 44.1, 48, 96 and 192 kHz. On macOS, `tests/studio_contract_ios.m` validates the iOS configuration contract without Titanium.
380
339
 
381
- `tools/calibrate_studio.py --studio-root PATH` measures the production Android callback; add `--platform ios` for the production Apple callback. The report covers each preset/scenario at 1, 2, 4, 8, 16 and 32 voices, including independent low/mid/high notes, grouped chords, coherent unisons and overlapping releases. Recommendations target −6 dBFS before protection (with 0.01 dB margin) using the maximum measured peak of the declared usual 1-8-voice configuration and the public voice-volume limit. Larger workloads are separate stress diagnostics. The tool reports fixed scalar recommendations and never edits Studio. Effects and new LFOs are disabled, so recommendations do not guarantee headroom for other configurations. See [audio pipeline validation](audio-pipeline-validation.md).
340
+ `tools/calibrate_studio.py --studio-root PATH` measures the production Android callback; add `--platform ios` for the production Apple callback. The report covers each preset/scenario at 1, 2, 4, 8, 16 and 32 voices, including independent low/mid/high notes, grouped chords, coherent unisons and overlapping releases. Recommendations target −6 dBFS before protection (with 0.01 dB margin) using the maximum measured peak of the declared usual 1-8-voice configuration and the public voice-volume limit. Larger workloads are separate stress diagnostics. The tool reports fixed scalar recommendations and never edits Studio. Effects and additional LFOs are disabled, so recommendations do not guarantee headroom for other configurations. See [audio pipeline validation](https://github.com/macCesar/TiSynthEngine/blob/9d74aa45f84d3f2822d4aab76874efe37d81257f/documentation/audio-pipeline-validation.md).
382
341
 
383
342
  Device feedback superseded that headroom target for Studio's initial Cristal preset: its fixed gain is restored to 1 because the calibrated setting made a fresh Android installation barely audible. This adds 10.83 dB without saturation or voice-count normalization. Three coherent factory notes remain below the limiter ceiling at maximum master; eight independent coherent notes can require limiting. The calibration recommendations remain recorded separately from this active preset exception.
384
343
 
385
- The [Studio technical validation report](studio-validation.json) records the calibration and corrected full-chain host benchmark, including failed deadline acceptance and pending device checks. Its host performance result must not be presented as hardware realtime acceptance.
344
+ The [Studio technical validation report](https://github.com/macCesar/TiSynthEngine/blob/9d74aa45f84d3f2822d4aab76874efe37d81257f/docs/validation/studio-validation.json) records the calibration and corrected full-chain host benchmark, including failed deadline acceptance and pending device checks. Its host performance result must not be presented as hardware realtime acceptance.
386
345
 
387
346
  ## Unified transport acceptance boundaries
388
347
 
389
- Public API 1.0.0 uses one renderer, at most 32 voices and capacity for 1-4 sequences. `bus` categories and per-voice sends share one effects chain. These API changes do not establish complete multiplatform ten-minute maximum-load acceptance; the measured iPad result and Android limits are recorded separately in [physical validation](physical-validation.json) and [the performance summary](stats-performance.md). Host timing, bypass comparisons, memory accounting, actual Android/iOS builds and the remaining device/listening checks are separate evidence. The cited Studio and unified-transport benchmarks are historical observations. In the pre-variation layout, effects buffers at 192 kHz used approximately 13.1 MiB and four maximum-size triple-bank transports used 8,627,712 bytes (8.23 MiB), excluding renderer voices and queues. The local 1.1.0 candidate grows those four transports to 9,537,152 bytes (9.10 MiB) on the measured 64-bit macOS layout, an increase of 909,440 bytes; see [variation storage measurements](native-variation-validation.md#storage-and-callback-work). These structure sizes are not a new whole-process or physical-device measurement. Total accounting must also include renderer voices/tails, command queues, effects, bridge/canonical options and recovery peaks. The 32 MiB additional-memory target remains a target, not a certified bound for the candidate.
348
+ Source-specific measurements and reproduction commands are preserved in the [historical engine validation record](https://github.com/macCesar/TiSynthEngine/blob/9d74aa45f84d3f2822d4aab76874efe37d81257f/documentation/audio-engine-validation.md). Current Sound-interface acoustic and runtime checks are recorded in [Sound validation](https://github.com/macCesar/TiSynthEngine/blob/9d74aa45f84d3f2822d4aab76874efe37d81257f/documentation/sound-api-validation.md).
349
+
350
+ ## Native finite-tone repetition
351
+
352
+ The shared C scheduler owns 32 repetition slots independently of sequence capacity. It retains fractional sample deadlines across normal and spatial render blocks. Each `repeatTone` attack emits the same finite-tone command used by a standalone `playTone`. The first attack starts at the first render opportunity, independent of tempo. Preallocated publication banks carry complete validated configurations; the callback allocates no memory, takes no producer locks and runs no JavaScript. Stop publication has a separate atomic path, so a full ordinary command queue cannot prevent cancellation at the next block.
353
+
354
+ Each repetition owns its variation stream and admitted-attack counter. Admission failure skips that attack without advancing variation or queuing catch-up work. Updates preserve the pending deadline and affect its attack; a new interval controls the subsequent gap. `stopTone` cancels only future attacks, leaving admitted finite tones to complete. Global stop, shutdown and destruction invalidate slots and pending edits. Historical measurements elsewhere on this page do not measure this scheduler. See [the public contract](api.md#finite-tone-repetition).