@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.
- package/README.md +87 -83
- package/docs/examples.md +3 -1
- package/package.json +1 -1
- package/skills/ti-game/SKILL.md +2 -0
- package/skills/ti-game/references/source-notes.md +20 -4
- package/skills/ti-synthengine/SKILL.md +26 -7
- package/skills/ti-synthengine/assets/example/README.md +14 -7
- package/skills/ti-synthengine/assets/example/app.js +20 -0
- package/skills/ti-synthengine/references/advanced-examples.md +27 -2
- package/skills/ti-synthengine/references/api.md +238 -227
- package/skills/ti-synthengine/references/audio-engine.md +44 -20
- package/skills/ti-synthengine/references/audio-pipeline-validation.md +19 -0
- package/skills/ti-synthengine/references/examples.md +1 -1
- package/skills/ti-synthengine/references/guide.md +30 -13
- package/skills/ti-synthengine/references/instruments-and-effects.md +157 -0
- package/skills/ti-synthengine/references/native-variation-proposal.md +182 -0
- package/skills/ti-synthengine/references/native-variation-validation.md +63 -0
- package/skills/ti-synthengine/references/native-variation.md +283 -0
- package/skills/ti-synthengine/references/recipes.md +1 -2
- package/skills/ti-synthengine/references/recommendations.md +3 -3
- package/skills/ti-synthengine/references/sound-design.md +16 -14
- package/skills/ti-synthengine/references/source-notes.md +75 -37
- package/skills/ti-synthengine/references/source-snapshot.json +203 -192
- package/skills/ti-synthengine/references/stats-performance.md +1 -1
- package/skills/ti-synthengine/references/troubleshooting.md +36 -28
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
> Source snapshot: official TiSynthEngine
|
|
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
|
|
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
|
|
145
|
-
|
|
|
146
|
-
|
|
|
147
|
-
| Release
|
|
148
|
-
| Ceiling
|
|
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
|
-
|
|
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()`
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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`
|
|
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
|
-
|
|
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
|
+
> 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
|
+
> 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.
|
|
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.
|
|
39
|
-
- `ti.synthengine-iphone-1.0.
|
|
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.
|
|
46
|
-
<module platform="iphone" version="1.0.
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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.
|