@maccesar/titools 5.2.2 → 5.2.4

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 (25) hide show
  1. package/README.md +87 -83
  2. package/docs/examples.md +3 -1
  3. package/package.json +1 -1
  4. package/skills/ti-game/SKILL.md +2 -0
  5. package/skills/ti-game/references/source-notes.md +20 -4
  6. package/skills/ti-synthengine/SKILL.md +26 -7
  7. package/skills/ti-synthengine/assets/example/README.md +14 -7
  8. package/skills/ti-synthengine/assets/example/app.js +20 -0
  9. package/skills/ti-synthengine/references/advanced-examples.md +27 -2
  10. package/skills/ti-synthengine/references/api.md +238 -227
  11. package/skills/ti-synthengine/references/audio-engine.md +44 -20
  12. package/skills/ti-synthengine/references/audio-pipeline-validation.md +19 -0
  13. package/skills/ti-synthengine/references/examples.md +1 -1
  14. package/skills/ti-synthengine/references/guide.md +30 -13
  15. package/skills/ti-synthengine/references/instruments-and-effects.md +157 -0
  16. package/skills/ti-synthengine/references/native-variation-proposal.md +182 -0
  17. package/skills/ti-synthengine/references/native-variation-validation.md +63 -0
  18. package/skills/ti-synthengine/references/native-variation.md +283 -0
  19. package/skills/ti-synthengine/references/recipes.md +1 -2
  20. package/skills/ti-synthengine/references/recommendations.md +3 -3
  21. package/skills/ti-synthengine/references/sound-design.md +16 -14
  22. package/skills/ti-synthengine/references/source-notes.md +75 -37
  23. package/skills/ti-synthengine/references/source-snapshot.json +203 -192
  24. package/skills/ti-synthengine/references/stats-performance.md +1 -1
  25. package/skills/ti-synthengine/references/troubleshooting.md +36 -28
@@ -1,4 +1,4 @@
1
- > Source snapshot: official TiSynthEngine 1.0.0 documentation at release source commit `e7cbbe8a6f9ab38088fd8029c7b43cef6501101f` (2026-10-24). Upstream `documentation/audio-engine.md` SHA-256: `2299982e6949f6f2efe7ddad6cc4d28ea1b54e0f31fff36617e0528c5823ac4d`.
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`.
2
2
 
3
3
  # Audio engine
4
4
 
@@ -17,6 +17,7 @@ The native signal path explains how mixing profiles and voice capacity affect th
17
17
  - [Voice pool](#voice-pool)
18
18
  - [Command queue](#command-queue)
19
19
  - [Shared DSP core](#shared-dsp-core)
20
+ - [Native per-attack variation](#native-per-attack-variation)
20
21
  - [Android implementation](#android-implementation)
21
22
  - [iOS implementation](#ios-implementation)
22
23
  - [Latency](#latency)
@@ -48,7 +49,7 @@ playTone / playChord / noteOn playSequence / updateSequence
48
49
  master -> optional saturation master
49
50
  \ /
50
51
  stereo recombination
51
- one final lookahead limiter
52
+ one final sample-peak protector
52
53
  floating-point output
53
54
  ```
54
55
 
@@ -141,19 +142,21 @@ The profile is fixed when `start()` opens the stream.
141
142
 
142
143
  A single stereo-linked peak limiter sits after master volume and optional creative saturation. Linking preserves the stereo image when only one channel has the largest peak.
143
144
 
144
- | Parameter | Value |
145
- | --------- | -------------------------------- |
146
- | Lookahead | 10 ms |
147
- | Release | 100 ms, exponential |
148
- | Ceiling | 0.85, or 0.70 with `speakerSafe` |
145
+ | Parameter | Value |
146
+ | ---------------- | -------------------------------- |
147
+ | Retained samples | 0 (no lookahead) |
148
+ | Release | 100 ms, exponential |
149
+ | Ceiling | 0.85, or 0.70 with `speakerSafe` |
149
150
 
150
- Lookahead gives the gain time to reach a safe value before the peak reaches the output. Below the ceiling, the limiter converges back to unity. It catches exceptional sums without normalizing every callback.
151
+ The shared `synth_output.h` core finds the larger absolute channel value for each stereo frame and immediately applies any required reduction to both channels. Recovery toward unity uses a 100 ms exponential constant, bounded by the current frame's required gain; two consecutive quiet stereo frames (each channel ≤1e-12 in magnitude) reset reduction; an isolated zero crossing does not. There is no audio delay buffer. The ceilings bound sample values, not inter-sample peaks or loudspeaker excursion.
152
+
153
+ A mix below the ceiling is unchanged when protection is at unity. Overload changes the waveform, and the recovery can attenuate later quiet samples. This protection does not promise equal loudness for one and 32 voices or transparency under deliberate overload.
151
154
 
152
155
  Overlapping events can trigger audible limiting when their sum exceeds the ceiling. Check the accumulated reduction measurement before attributing a volume dip to this protection: repeated same-pitch oscillators can also cancel one another even below the ceiling. Compatible public sustained-note repetitions preserve the phase of a releasing predecessor while retaining separate envelopes and tails. Timed calls, transport notes and simultaneous held unisons keep their existing phase policy.
153
156
 
154
157
  ## Master volume and pan
155
158
 
156
- `setVolume()` ramps the master to avoid a discontinuous gain step. Per-event `volume` is multiplied before the bus. Values up to 2 are valid, but values above 1 use headroom and make limiting more likely.
159
+ `setVolume()` uses the same linear 10 ms ramp on both platforms, independently of peak protection. The next processed sample takes the first step and the last reaches the destination exactly (`round(sampleRate * 0.010)` frames). A new target starts a fresh ramp from the current gain. Per-event `volume` is multiplied before the bus. Values up to 2 are valid, but values above 1 use headroom and make limiting more likely.
157
160
 
158
161
  Pan uses equal-power coefficients. A centered sound feeds both channels; the sum does not simply double. `panEnd` interpolates the position across a tone or sequence note. Chords use `panSpread` or the explicit `pans` array.
159
162
 
@@ -173,7 +176,11 @@ Both ports use a 128-slot single-consumer ring buffer. Producers are serialized
173
176
 
174
177
  ## Shared DSP core
175
178
 
176
- 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, mixing and the limiter remain separate per platform. Held envelopes/ramps, transport, master effects, extra modulation and compatible retrigger support also have shared C cores.
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
+
181
+ ## Native per-attack variation
182
+
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.
177
184
 
178
185
  ## Android implementation
179
186
 
@@ -185,15 +192,17 @@ The renderer mixes in the data callback and targets a small device buffer. A rou
185
192
 
186
193
  iOS renders with `AVAudioEngine` and `AVAudioSourceNode`, using the active session's sample rate. The module configures the audio session as `Playback`, or `Ambient` when `start()` receives `audioSession: 'ambient'`, activates it, and observes route and engine configuration notifications. If a route change stops the graph, the port reapplies the category, reactivates the session and resumes the existing renderer.
187
194
 
195
+ The main-queue event timer runs every 10 ms only while accepted sequences or held-voice metadata need retirement. It stops after the last pending item is retired and starts again on an accepted sequence or held note. This timer delivers notifications and cleans control metadata; it does not generate audio or schedule musical attacks. A shutdown or restart invalidates callbacks from the previous timer.
196
+
197
+ Run `python3 tests/run_sequence_events_ios.py` on macOS to check idle polling, sequence acceptance and retirement, held metadata cleanup, and shutdown/restart from an event listener against the production methods with real dispatch timers and the shared transport. The harness replaces the audio device and Titanium event sink, so it does not establish device listening acceptance.
198
+
188
199
  The module targets iOS 15 or newer. The module archive contains device, simulator and Mac Catalyst slices.
189
200
 
190
201
  ## Latency
191
202
 
192
- End-to-end latency contains three main pieces:
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.
193
204
 
194
- 1. The fixed 10 ms limiter lookahead.
195
- 2. The hardware and operating-system output buffer.
196
- 3. JavaScript publication and callback-boundary latency for a newly submitted event; accepted sequence steps then follow the sample clock.
205
+ 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`.
197
206
 
198
207
  Immediate calls are suitable for interface and game feedback. A visual or musical event that must align to a known future time should be scheduled ahead by the app instead of relying on a JavaScript callback at that instant.
199
208
 
@@ -214,6 +223,7 @@ cd android
214
223
  test_dir=$(mktemp -d /tmp/ti-synth-test.XXXXXX)
215
224
  javac -d "$test_dir" \
216
225
  src/ti/synthengine/SynthContract.java \
226
+ src/ti/synthengine/SynthStudioContract.java \
217
227
  test/ti/synthengine/SynthContractRegression.java \
218
228
  test/ti/synthengine/SynthSpatialContractRegression.java \
219
229
  src/ti/synthengine/SynthDsp.java \
@@ -238,10 +248,10 @@ cc -std=c99 -O2 -Wall -Wextra -o "$test_dir/dsp_core" \
238
248
 
239
249
  It checks that the envelope equals the original attack, hold and release shape for every attack and release combination when decay is off, the decay and sustain endpoints and continuity, the response of each filter type (slope, cutoff and the resonance peak), stability of every filter type under a cutoff sweep on noise, FM sidebands against the Bessel function values and the cents conversion.
240
250
 
241
- The Android engine renders on the host. `tests/engine_render_host.cpp` compiles `SynthOboeEngine.cpp` without opening an audio stream and drives the same command and callback path Oboe uses. It needs the Oboe headers, which any Gradle cache that built the module has, and the JDK's `jni.h`, so it is a local check and does not run in CI:
251
+ The Android engine renders on the host. `tests/engine_render_host.cpp` compiles `SynthOboeEngine.cpp` without opening an audio stream and drives the same command and callback path Oboe uses. It needs the Oboe 1.11.0 headers and the JDK's `jni.h`. The Linux job in [the test workflow](https://github.com/macCesar/TiSynthEngine/blob/main/.github/workflows/tests.yml) downloads those headers and runs this harness through `python3 tests/run_native_hosts.py`, alongside the other production Android host checks. The macOS job runs the extracted production Apple callback and start-profile checks. Host checks do not open hardware streams. To run the Android harness locally with cached headers:
242
252
 
243
253
  ```sh
244
- OBOE=$(find ~/.gradle -path '*oboe-1.10.0/prefab/modules/oboe/include' | head -1)
254
+ OBOE=$(find ~/.gradle -path '*oboe-1.11.0/prefab/modules/oboe/include' | head -1)
245
255
  JH=$(/usr/libexec/java_home)
246
256
  test_dir=$(mktemp -d /tmp/ti-synth-engine.XXXXXX)
247
257
  clang++ -std=c++17 -O1 -Wall -I"$OBOE" -I"$JH/include" -I"$JH/include/darwin" \
@@ -251,6 +261,16 @@ clang++ -std=c++17 -O1 -Wall -I"$OBOE" -I"$JH/include" -I"$JH/include/darwin" \
251
261
  "$test_dir/engine_render"
252
262
  ```
253
263
 
264
+ On Linux, point `JH` at the JDK directory, use its `include/linux` directory, pre-include `<cstring>` because Oboe's headers rely on it, and let the linker ignore the Oboe symbols that the host never calls:
265
+
266
+ ```sh
267
+ clang++ -std=c++17 -O1 -Wall -include cstring -I"$OBOE" -I"$JH/include" -I"$JH/include/linux" \
268
+ -DHAS_EXPRESSIVE -DENGINE_CPP='"../android/jni/SynthOboeEngine.cpp"' \
269
+ -Wl,--unresolved-symbols=ignore-all tests/engine_render_host.cpp \
270
+ -o "$test_dir/engine_render"
271
+ "$test_dir/engine_render"
272
+ ```
273
+
254
274
  It renders decay and sustain levels, filter attenuation, an opening cutoff sweep, FM sidebands and an FM index that decays to nothing, plus voice stealing and `stopAll` with every control active. Built without `-DHAS_EXPRESSIVE` it renders only the original controls and prints one hash per scenario, so the same file can be built against an older copy of the engine and the hashes compared.
255
275
 
256
276
  The iOS contract replays the cases that the Java contract test writes to `tests/contract_corpus.jsonl`. The Java test fails when that file no longer matches the Java contract; regenerate it from `android/` with `-Dsynth.corpus.out=../tests/contract_corpus.jsonl`. On macOS, run the iOS side from the repository root:
@@ -271,16 +291,20 @@ The iOS renderer uses the same DSP header. Its actual Objective-C render body ha
271
291
 
272
292
  Spatial voice-steal and stop tails copy the filter history as well as oscillator state. After the source envelope or fade ends, both ports drain the FIR with zero input for `taps - 1` samples before freeing the voice; this preserves even 1 ms events. Ordinary voices bypass the FIR. CPU cost grows with spatial voices, including short replacement tails. Spatial held updates prepare coefficients outside audio, then ramp the FIR over 10 ms while retaining history. The intended output-rate range is 8 to 192 kHz. Interpolated generic HRTFs provide directional cues, but localization varies by listener.
273
293
 
274
- Android ARM64 reuses coefficients across eight consecutive samples, with four-sample and streaming fallbacks for partial blocks and position ramps. Tiles stop before native sequence attacks and retain per-sample clocks, gates, phase, history and mixing order. Apple retains its original streaming renderer. The proposed Apple adaptation was bit exact and faster in host/binaural tests but was removed after a reproducible physical stereo timing regression. Output buffers and lookahead latency are unchanged.
294
+ Android ARM64 reuses coefficients across eight consecutive samples, with four-sample and streaming fallbacks for partial blocks and position ramps. Tiles stop before native sequence attacks and retain per-sample clocks, gates, phase, history and mixing order. Apple retains its original streaming renderer. The proposed Apple adaptation was bit exact and faster in host/binaural tests but was removed after a reproducible physical stereo timing regression. That historical optimization left output buffering unchanged. The later audio-pipeline correction removes final limiter lookahead on both platforms.
275
295
 
276
296
  ## Own-platform performance verification
277
297
 
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
+
278
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.
279
301
 
280
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.
281
303
 
282
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.
283
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
+
284
308
  ```sh
285
309
  mkdir -p /tmp/synth-reference
286
310
  git archive 7ab3110 | tar -x -C /tmp/synth-reference
@@ -331,7 +355,7 @@ c++ -std=c++17 -O2 -Wall -Wextra -pthread tests/stats_regression.cpp -o /tmp/sta
331
355
  python3 tests/run_stats_java.py
332
356
  ```
333
357
 
334
- `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. This native integration harness requires local Oboe headers; portable shared tests and the stopped-engine Java API tests also run in CI. iOS builds use the same telemetry core, but host Android renders do not execute the iOS audio callback.
358
+ `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.
335
359
 
336
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.
337
361
 
@@ -354,7 +378,7 @@ Pre-limiter peak and maximum limiter reduction accumulate natively until reset,
354
378
 
355
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.
356
380
 
357
- `tools/calibrate_studio.py --studio-root PATH` renders Studio presets through the native Android callback at maximum master gain with new effects and modulation disabled. It measures eight independent notes, coherent unisons and repeated attacks with release overlap across low, middle and high registers, then adjusts only a fixed preset gain. The target is at most -6 dBFS before the limiter and at most 0.1 dB limiter reduction; this does not promise zero reduction for arbitrary 32-voice combinations. The report records each scenario before and after calibration. Listening and device performance remain separate acceptance checks.
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).
358
382
 
359
383
  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.
360
384
 
@@ -362,4 +386,4 @@ The [Studio technical validation report](studio-validation.json) records the cal
362
386
 
363
387
  ## Unified transport acceptance boundaries
364
388
 
365
- The 1.0.0 contract 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. Historical Studio benchmarks below predate these changes and remain historical observations. At 192 kHz, the revised effects buffers use approximately 13.1 MiB; the maximum sequence pool uses approximately 8.23 MiB before renderer voices/queues. Total native accounting must include those remaining structures and recovery peaks; the target is at most 32 MiB additional memory.
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.
@@ -0,0 +1,19 @@
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-pipeline-validation.md` SHA-256: `7359d85de63e9fe94f85477e03a22fc1531c8acaabe30333e7ff2515cc5ec3e9`.
2
+
3
+ # Audio pipeline correction
4
+
5
+ Version 1.0.2 removes final-output retention on Android and iOS while retaining one stereo output per platform, fixed event gains, waveform profiles and chord compensation. It adds no public options. Existing envelopes, release tails and parameter transitions keep their roles.
6
+
7
+ The final protector responds immediately to each stereo sample peak. This removes the old mandatory 10 ms delay, at the cost of a different overload response. Heavy mixes can audibly change waveform shape. Protection recovers with a 100 ms exponential time constant. The 0.85/0.70 ceilings describe sample values, not inter-sample peaks or speaker behavior. The master now follows the same linear 10 ms ramp on both platforms. iOS consumers requesting `raw` now receive its documented gain instead of the conservative gain; review those mixes.
8
+
9
+ `tests/output_core_regression.c` checks linked impulses, silence/reset, bounded recovery and exact/retargeted master ramps at 16, 44.1, 48, 96 and 192 kHz (absolute double tolerance 1e-12). Android host checks compile the real C++ callback. Apple checks extract the actual callback, helpers and start validation/profile selection, replacing device/Titanium plumbing. The historical Java DSP model is not an oracle for native sound. CI runs production output checks alongside transport, effects, spatial and statistics regressions.
10
+
11
+ `tools/calibrate_studio.py` (Android by default, `--platform ios` for Apple) reports all 37 Studio presets individually across independent low/mid/high notes, grouped chords, unisons and overlapping releases at 1, 2, 4, 8, 16 and 32 voices. Its recommendation uses the maximum measured pre-protection peak for the declared usual configuration (1-8 voices) to target −6 dBFS, with 0.01 dB measurement margin and the public volume limit. It reports 16/32-voice stress separately. The recommendations apply to the measured configuration. Event gains remain fixed, and other workloads can exceed the target. Effects and new LFOs are disabled in preset calibration. It never edits Studio.
12
+
13
+ The before/after host comparison covered 80 PCM pairs across both renderers at five sample rates. After aligning the former output delay, all 40 cases without overload preserved the waveform within an absolute float tolerance of 1e-7. Overload cases remained finite and within the sample ceiling; their waveform differences were measured. Matched host benchmarks found no median callback-time increase. These measurements do not establish whole-app CPU use or physical output latency.
14
+
15
+ Physical comparisons used Studio and Frontier on an OPPO CPH2639 running Android 16 and an iPad. Studio checks covered its 37 presets, short notes, Hold, chords, repetitions, sequences, effects and spatial controls. Frontier checks covered gameplay with simultaneous sounds. The same native candidate binaries were then integrated into the real Studio and Frontier apps, which built and launched on both devices. Their presets and gains were preserved. Route-wide latency, Bluetooth behavior and extended maximum-load acceptance remain outside these results.
16
+
17
+ Android can report an underrun during stream startup. An initial matched repeat recorded it in four of six starts for both the original and candidate, with no additional underruns during the subsequent note/chord checks. Further silent tests reproduced the symptom with an independent Oboe 1.11.0 callback that only writes zeros. The startup condition remains unresolved; the counter is retained. Silent tests do not establish whether it causes an audible defect. Oboe's open [LatencyTuner proposal](https://github.com/google/oboe/pull/2432) discusses how startup XRuns affect buffer tuning, but does not eliminate their cause. TiSynthEngine does not use that tuner.
18
+
19
+ PCM, original and candidate packages, commands, hashes and device traces are retained outside the app repositories. The physical results apply to the tested devices and scenarios.
@@ -1,4 +1,4 @@
1
- > Source snapshot: official TiSynthEngine 1.0.0 documentation at release source commit `e7cbbe8a6f9ab38088fd8029c7b43cef6501101f` (2026-10-24). Upstream `documentation/examples.md` SHA-256: `48104b9d413050d439ee605c92a2456a50b42c4c80e2d7c2c05706eecc7db215`.
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/examples.md` SHA-256: `637806516d2804c43427d187a37786cfcf2f0d28758762f01a570ba15194bae3`.
2
2
 
3
3
  # Examples
4
4
 
@@ -1,4 +1,4 @@
1
- > Source snapshot: official TiSynthEngine 1.0.0 documentation at release source commit `e7cbbe8a6f9ab38088fd8029c7b43cef6501101f` (2026-10-24). Upstream `documentation/guide.md` SHA-256: `dd03392e1d5f89e520f4be70dab2fbad8e86776bf3a60d6af93f0dc0695c8c69`.
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/guide.md` SHA-256: `4da2d85f329a49a8b81a9c671c75051b41788371b2c00990fbd499176623894d`.
2
2
 
3
3
  # Guide
4
4
 
@@ -28,27 +28,30 @@ Install TiSynthEngine, start it after layout settles, and release its native aud
28
28
  - [Read engine statistics](#read-engine-statistics)
29
29
  - [Next steps](#next-steps)
30
30
  - [Diagnose mix headroom](#diagnose-mix-headroom)
31
+ - [Local native-variation candidate](#local-native-variation-candidate)
31
32
 
32
33
  <!-- TOC-END -->
33
34
 
34
35
  ## Install the module
35
36
 
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`:
37
+ Download both official packages from [Android 1.0.2](https://github.com/macCesar/TiSynthEngine/releases/tag/android-1.0.2) and [iOS 1.0.2](https://github.com/macCesar/TiSynthEngine/releases/tag/ios-1.0.2). For the published 1.0.2 revision, matching archives are produced under `android/dist` and `ios/dist`:
37
38
 
38
- - `ti.synthengine-android-1.0.0.zip`
39
- - `ti.synthengine-iphone-1.0.0.zip`
39
+ - `ti.synthengine-android-1.0.2.zip`
40
+ - `ti.synthengine-iphone-1.0.2.zip`
40
41
 
41
42
  Put the archives in the Titanium project root or install them through the Titanium CLI. Pin the same version for both platforms:
42
43
 
43
44
  ```xml
44
45
  <modules>
45
- <module platform="android" version="1.0.0">ti.synthengine</module>
46
- <module platform="iphone" version="1.0.0">ti.synthengine</module>
46
+ <module platform="android" version="1.0.2">ti.synthengine</module>
47
+ <module platform="iphone" version="1.0.2">ti.synthengine</module>
47
48
  </modules>
48
49
  ```
49
50
 
50
51
  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.
51
52
 
53
+ These declarations select the published 1.0.2 packages. Current source builds produce unpublished local 1.1.0 candidates; use the separate [candidate setup](#local-native-variation-candidate) below when testing native attack variation.
54
+
52
55
  ## Load and verify the contract
53
56
 
54
57
  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.
@@ -108,11 +111,11 @@ win.addEventListener('close', closeAudio)
108
111
  win.open()
109
112
  ```
110
113
 
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.
114
+ 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. On Android, the module also shuts down when Titanium destroys the root activity, so that a cached process does not keep the audio stream open. This is a safety net, not a replacement for the owner's `shutdown()` call. 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
115
 
113
116
  ## Detect required capabilities
114
117
 
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:
118
+ Public API version 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
119
 
117
120
  ```javascript
118
121
  function hasMethods (names) {
@@ -129,7 +132,7 @@ Start the engine successfully before publishing held notes, sequence edits or pr
129
132
 
130
133
  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
134
 
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.
135
+ `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. Source, table, filter type, quantization, ADSR and stereo-versus-spatial mode are fixed at attack. 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
136
 
134
137
  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
138
 
@@ -143,7 +146,7 @@ Store the positive identifier returned by `playSequence`, and include it in each
143
146
  synth.setTempo({ bpm: 112 })
144
147
  var sequenceId = synth.playSequence({
145
148
  stepDuration: 0.5, swing: 0.12, bus: 'music',
146
- voice: { waveType: WAVE.SQUARE, volume: 0.55 },
149
+ voice: { waveType: contract.waveTypes.SQUARE, volume: 0.55 },
147
150
  steps: [{ notes: [60] }, { notes: [64], overrides: { pan: -0.4, panEnd: 0.4 } },
148
151
  { rest: true }, { notes: [67], duration: 1, length: 0.8 }]
149
152
  })
@@ -160,7 +163,7 @@ The native chain is voices, fixed input gain, chorus, ping-pong delay, stereo Sc
160
163
 
161
164
  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
165
 
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).
166
+ `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 every 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
167
 
165
168
  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).
166
169
 
@@ -365,7 +368,7 @@ synth.playChord({
365
368
  synth.setTempo({ bpm: 112 })
366
369
  var sequenceId = synth.playSequence({
367
370
  stepDuration: 0.5, swing: 0.12, bus: 'music',
368
- voice: { waveType: WAVE.SQUARE, volume: 0.55 },
371
+ voice: { waveType: contract.waveTypes.SQUARE, volume: 0.55 },
369
372
  steps: [{ notes: [60] }, { notes: [64], overrides: { pan: -0.4, panEnd: 0.4 } },
370
373
  { rest: true }, { notes: [67], duration: 1, length: 0.8 }]
371
374
  })
@@ -394,7 +397,7 @@ synth.setVolume({ volume: 0.7 })
394
397
  synth.stopAll()
395
398
  ```
396
399
 
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.
400
+ Master volume changes use a linear 10 ms ramp. `stopAll()` stops every native sequence, cancels future attacks, fades active voices and invalidates effect histories. Call `stopSequence({ sequenceId })` to stop only one sequence, or `shutdown()` to close the stream and free resources.
398
401
 
399
402
  ## Output route changes
400
403
 
@@ -601,3 +604,17 @@ See the [complete field contract](api.md#getstats), [measurement limits](stats-p
601
604
  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
605
 
603
606
  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.
607
+
608
+
609
+ ## Local native-variation candidate
610
+
611
+ Current source builds unpublished local candidates at `android/dist/ti.synthengine-android-1.1.0.zip` and `ios/dist/ti.synthengine-iphone-1.1.0.zip`. Keep them separate from the published 1.0.2 packages. Install both matching candidates in the consumer app and select them explicitly:
612
+
613
+ ```xml
614
+ <modules>
615
+ <module platform="android" version="1.1.0">ti.synthengine</module>
616
+ <module platform="iphone" version="1.1.0">ti.synthengine</module>
617
+ </modules>
618
+ ```
619
+
620
+ Rebuild and reinstall the application after replacing the native modules. Both package lines retain public API `1.0.0`; it does not identify variation support. Send a `variation` dictionary only when `getDefaults().capabilities.variation === true` and `getDefaults().variationSchemaVersion === 1`. If either announcement is absent, omit the property completely, including at amount zero. See [native attack variation](native-variation.md) for accepted locations, bounded ranges and seed behavior. The 1.1.0 candidates have not been published as a release.
@@ -0,0 +1,157 @@
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/instruments-and-effects.md` SHA-256: `bcd7c2191bfae2b07688c9dbe19d813fe4eb8fcc45ab38d49b29dff7bdc2981c`.
2
+
3
+ # Instruments, sound effects and complete sequences
4
+
5
+ A playable instrument and a missile sound can use the same oscillator. What makes them behave differently is the complete recipe: pitch, envelope, pitch/filter movement, layers, timing, and how the app starts and ends it. This guide explains which public API to use and how to preserve that recipe.
6
+
7
+ These are application roles built on the same native synthesizer and output chain. They are not different audio engines, quality modes, or guarantees against distortion. An instrument can be noisy or inharmonic; an effect can be a single pitched tone, a chord, or a musical phrase.
8
+
9
+ ## Choose the playback contract
10
+
11
+ | Intention | Native methods | Who ends the sound? | What to preserve |
12
+ | ---------------------------------------------------------------- | ------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------- |
13
+ | Play a key until the finger lifts | `noteOn`, `updateVoice`, `noteOff` | The app releases its unique voice ID | Timbre, tuning, envelope, contact ownership |
14
+ | Fire one finite sound | `playTone` | Native duration and release | Original frequency, endpoints, duration, envelope, gain |
15
+ | Fire a simultaneous chord with shared shaping | `playChord` | Native duration and release | All pitches, per-note pan/detune, shared envelope, native chord compensation |
16
+ | Play a melody, alarm, layered effect, or repeated firing pattern | `playSequence`, optionally `updateSequence` | Native step clock; `stopSequence` cancels future attacks | Complete ordered steps, rests, lengths, overrides and independent layers |
17
+ | Add chorus, delay, reverb or creative saturation | `setMasterProcessing` | The shared processor owns its tails | Routing, sends, input gain, effect settings and output headroom |
18
+
19
+ The last row describes **audio processing effects**. Those processors are different from an authored **sound effect**, such as a laser or an explosion. A laser may use no reverb or delay at all.
20
+
21
+ All examples below assume successful startup. Use constants from the installed module and check native acceptance:
22
+
23
+ ```javascript
24
+ var synth = require('ti.synthengine')
25
+ var WAVE = synth.getDefaults().waveTypes
26
+ if (!synth.start({ maxVoices: 32, maxSequences: 4,
27
+ mixingProfile: 'conservative', audioSession: 'playback' })) {
28
+ throw new Error('Could not start TiSynthEngine')
29
+ }
30
+ synth.setVolume({ volume: 0.6 })
31
+ ```
32
+
33
+ See [API limits](api.md) and [startup and lifecycle](guide.md) for application integration. Do not call `start` on each key just to reset a voice pool. Shut the engine down when its application owner finishes.
34
+
35
+ ## 1. Playable instruments and sustained voices
36
+
37
+ An instrument recipe defines the response of a new note: waveform, filter, FM, envelope, level and positioning. The keyboard supplies its pitch and release gesture. Each simultaneous contact needs its own positive voice ID.
38
+
39
+ ```javascript
40
+ var nextVoiceId = 1
41
+ function pressKey(frequency) {
42
+ var voiceId = nextVoiceId++
43
+ var accepted = synth.noteOn({ voiceId: voiceId, frequency: frequency,
44
+ waveType: WAVE.TRIANGLE, volume: 0.16,
45
+ attack: 8, decay: 220, sustain: 0.35, release: 180,
46
+ bus: 'music' })
47
+ return accepted ? voiceId : 0
48
+ }
49
+ function releaseKey(voiceId) {
50
+ return synth.noteOff({ voiceId: voiceId })
51
+ }
52
+ ```
53
+
54
+ Store the returned ID with that contact. Sliding to another key must release the old owner and admit the new attack; do not release another finger's voice. An app-level Hold control retains selected IDs until explicitly released. If `noteOff` is rejected, keep ownership and follow a bounded retry/error policy; silently forgetting the ID can leave a stuck voice. The [held voice guide](guide.md#hold-and-edit-a-voice) covers lifecycle and compatible live edits.
55
+
56
+ `noteOn` does not accept timed-event endpoint sweeps such as `frequencyEnd`. A held voice can use its supported vibrato/LFOs and compatible `updateVoice` edits. Removing a timed sweep, replacing the original frequency with the key's pitch, and holding indefinitely changes the sound's meaning: that is a new playable timbre, not an exact audition of the original effect.
57
+
58
+ A short envelope also does not automatically make a held voice finite. With `sustain: 0`, it can become silent while still requiring release of its ID. Choose a timed event for a self-contained strike when contact ownership is unnecessary.
59
+
60
+ ### Instrument demonstrations are musical performances
61
+
62
+ A demo provides notes and articulation for the current timbre. It should not silently substitute another sound or replace the user's editable pattern. A useful finite phrase has an opening, development and a resolved ending:
63
+
64
+ - Winds need an appropriate register, separated attacks and breathing spaces.
65
+ - Strings benefit from longer notes and nearly connected lengths.
66
+ - Basses benefit from a low register, rests and a deliberate groove.
67
+ - Guitars and keyboards can use arpeggios, chord changes and varied note lengths.
68
+ - Percussion needs separate strikes and enough room for its decay and release.
69
+ - Slow pads need broad entries long enough for their attack to develop.
70
+ - Unpitched textures demonstrate density, filtering and movement; changing a noise clock is not the same as transposing a harmonic note.
71
+
72
+ Schedule such phrases with the native transport. Make looping an explicit application choice. See [sequences](api.md#playsequenceoptions) and [sound-design envelopes](sound-design.md#envelope-and-total-duration).
73
+
74
+ ## 2. Independent finite sound effects
75
+
76
+ A sound effect often communicates a specific action: launch, impact, warning, confirmation. Its original pitch is part of that action. A rising missile and a descending shot can share the same square-wave timbre while conveying different movement.
77
+
78
+ ```javascript
79
+ function fireMissile() {
80
+ return synth.playTone({ frequency: 196, frequencyEnd: 523,
81
+ duration: 350, waveType: WAVE.SQUARE,
82
+ attack: 2, decay: 0, sustain: 1, release: 50,
83
+ volume: 0.15, bus: 'effects' })
84
+ }
85
+ ```
86
+
87
+ This event completes natively. Repeated independent calls can overlap naturally. Their frequencies and endpoints remain those of the recipe; they are not implicitly transposed to the key being touched.
88
+
89
+ A keyboard adapter that keeps only the square wave and envelope, starts at C7, and omits `frequencyEnd` will sound very different. Neither result establishes an engine fault: the adapter has selected a different pitch, duration model and motion. An effect audition should execute the whole recipe. Label a separately offered playable adaptation clearly.
90
+
91
+ Use `playChord` when its contract exactly represents the intended chord. It applies its own chord compensation. Independent layers authored with distinct levels, envelopes or waves are not automatically equivalent to one chord call. Preserve those layers rather than normalizing them again or flattening them into one source.
92
+
93
+ ## 3. Complete sound sequences and layered effects
94
+
95
+ Some effects need multiple ordered attacks. Others combine simultaneous body, noise and high-frequency detail, each with a different envelope. A sustained key containing only the first layer cannot reproduce the complete event.
96
+
97
+ This native sequence defines a missile with 300 ms of pitch motion followed by 50 ms of release. It is a sequence interpretation, not a sample-equivalent conversion of the timed example above:
98
+
99
+ ```javascript
100
+ function missileSequence() {
101
+ return synth.playSequence({ timeUnit: 'ms', loop: false,
102
+ voice: { waveType: WAVE.SQUARE, attack: 2,
103
+ decay: 0, sustain: 1, release: 50, volume: 0.15,
104
+ bus: 'effects' },
105
+ steps: [{ frequencies: [196], duration: 350, length: 300,
106
+ overrides: { frequencyEnd: 523 } }] })
107
+ }
108
+ ```
109
+
110
+ `duration` on a sequence step is the interval until the next attack. `length` is how long that note runs before release starts. Both use the sequence's time unit; attack, decay and release remain milliseconds. A note can extend beyond its step and overlap later attacks. In a timed `playTone` call, `duration` includes release. These values cannot be copied interchangeably without accounting for release.
111
+
112
+ **Timed and sequence routes are not automatically interchangeable.** In the current native implementation, a `playTone` endpoint sweep progresses across the total timed duration, including release. A sequence endpoint sweep progresses across its explicit `length`, then holds the endpoint during release. Thus the timed 350 ms example and the sequence with `length: 300` do not have the same pitch trajectory, even though both finish after 350 ms and use the same endpoints. Sequence voices also use the held envelope's minimum 3 ms attack/release edges; very short or zero timed edges can differ. This matters most for short weapon/noise effects and is not corrected by merely copying the option names.
113
+
114
+ For consistent manual and repeated auditions, use the same native interpretation for both. Preserve the original attack length when changing only the repetition interval; each new attack may overlap the prior voice. Do not claim exact timed-to-sequence fidelity from field equality or validator acceptance. Preserving the exact timed renderer inside a native repetition would require native support for that rendering contract; it cannot be obtained by a JavaScript adapter that simply renames `duration` to `length`.
115
+
116
+ For a sequence-owned burst, `stopSequence` also releases emitted voices. To stop repetitions while preserving those attacks, publish a finite traversal with `loop: false`, retain its current effect steps and suppress later variant attacks as appropriate. Natural completion leaves emitted voices to their own lengths/releases. Retain the first attack in an update that can arrive before the initial callback. Handle publication rejection explicitly, with bounded control retries if necessary; those retries must not trigger notes or queue new sound requests.
117
+
118
+ Use complete step dictionaries and their required overrides. Do not identify variants by guessing from silence, remove an intentional gap, round source frequencies to MIDI integers, discard a sweep, or merge independent layers solely because they begin together. A source importer should carry explicit interpretation, variant boundaries and demo association alongside the recipe.
119
+
120
+ ### Repeated firing uses the native clock
121
+
122
+ ```javascript
123
+ function repeatShortShots(intervalMs) {
124
+ return synth.playSequence({ timeUnit: 'ms', loop: true,
125
+ voice: { waveType: WAVE.SQUARE, attack: 2,
126
+ decay: 0, sustain: 1, release: 40, volume: 0.1,
127
+ bus: 'effects' },
128
+ steps: [{ frequencies: [1380], duration: intervalMs, length: 65,
129
+ overrides: { frequencyEnd: 340 } }] })
130
+ }
131
+ ```
132
+
133
+ Validate `intervalMs` against native limits and the application's voice budget. This example has a 105 ms event (65 ms plus 40 ms release). At a shorter interval, tails overlap; at 150 ms they have room to finish. A compound multi-attack effect also needs enough interval for its complete pattern if the app promises complete repetitions.
134
+
135
+ The app can begin an independent first attack immediately and publish delayed native repetitions, or publish a complete native repeating score with its first attack at time zero. Avoid emitting the same initial attack through both `touchstart` and `click`. JavaScript polling may display transport position, but `setInterval`/`setTimeout` should not schedule individual musical attacks.
136
+
137
+ Keep every positive sequence ID. Releasing a momentary pad, disabling a fixed burst, switching presets, backgrounding the app, and pressing Stop should cancel the sequences owned by that panel. `stopSequence` releases that sequence's voices and cancels future attacks; independent timed calls already emitted are separate and finish naturally. Stop is different from global `stopAll`/shutdown panic.
138
+
139
+ A successful stop request does not mean the native slot is already reusable. Wait for the matching `sequenceend` retirement when replacing a sequence; do not poll and enqueue a backlog of missed firing requests. Keep authored variant order explicit and advance it only for accepted work. Application recipe functions are ordinary JavaScript; there is no built-in public named-recipe registry. Local 1.1.0 candidates support optional [native per-attack variation](native-variation.md), which chooses once at each admitted attack, including native loop attacks. It does not continuously resample an already sounding voice, and `updateVoice` rejects the variation dictionary. Published 1.0.2 packages reject it entirely; require both the variation capability and schema announcement before adding that property.
140
+
141
+ ## 4. Polyphony, gain and output protection
142
+
143
+ Both musical notes and effects share the voice pool and output chain. Either can distort when too many contributions sum at high gain, when filters resonate strongly, when tails overlap, or when creative saturation is enabled. A successful effect in one game does not certify arbitrary held chords, denser firing, different processing, or a device's physical speaker volume.
144
+
145
+ - `maxVoices` is a capacity, not a loudness control. Reserve for simultaneous attacks and release tails, including future overlap in a repeating score.
146
+ - `maxSequences` limits independent native tracks. Do not silently drop a layer to fit the limit.
147
+ - Check native acceptance. A positive sequence ID acknowledges publication; it does not prove every future note will find enough voices.
148
+ - Native admission can replace eligible voices under pressure. If the product promises no stealing, the application must reserve capacity across all of its sound owners and reject excess work visibly.
149
+ - Do not apply chord normalization to an already authored set of independent layers unless that gain change is intended.
150
+ - Keep headroom at voice, processor input and master stages. Preserve source gains when importing a known recipe; changing the overlap pattern still needs new validation.
151
+ - Creative `saturation` intentionally changes timbre. Final output limiting protects peaks but is not proof that the earlier mix is clean.
152
+
153
+ Use `getStats` to inspect active/tail voices, pending commands, rejected publications, priority drops and voice steals. Enable diagnostics for `preLimiterPeakDb`, `maxLimiterReductionDb`, output peaks and callback load. See [gain staging](sound-design.md#chords-polyphony-and-gain), [processing](api.md#setmasterprocessingoptions), [statistics](stats-performance.md), and [troubleshooting](troubleshooting.md). Host measurements and successful validation do not replace listening on the target speaker/headphones at the intended overlap and physical volume.
154
+
155
+ ## Verify the route you actually ship
156
+
157
+ First compare an effect against its original complete recipe, including timing and levels. Separately test its playable adaptation, if offered. For instruments, test single keys, simultaneous keys, slides, Hold, release tails and finite demos. For effects, test rapid taps, native bursts, explicit variants, layered sounds, cancellation and exhausted capacity. Distinguish publication acceptance, rendered output, physical responsiveness and listening results in the report.