@maccesar/titools 5.2.0 → 5.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CONTRIBUTING.md +82 -0
  2. package/README.md +83 -72
  3. package/agents/ti-pro.md +1 -1
  4. package/commands/ti-audit.md +2 -2
  5. package/docs/examples.md +91 -0
  6. package/docs/knowledge-index.md +57 -0
  7. package/lib/commands/uninstall.js +19 -16
  8. package/lib/commands/update.js +1 -1
  9. package/lib/downloader.js +1 -1
  10. package/lib/utils.js +2 -0
  11. package/package.json +4 -2
  12. package/skills/purgetss/references/multi-density-images.md +1 -1
  13. package/skills/ti-admob-unified/SKILL.md +21 -7
  14. package/skills/ti-admob-unified/assets/example/app.js +229 -25
  15. package/skills/ti-admob-unified/assets/example/lib/ads.js +30 -2
  16. package/skills/ti-admob-unified/assets/example/library-usage.js +33 -1
  17. package/skills/ti-admob-unified/references/api-reference.md +39 -16
  18. package/skills/ti-admob-unified/references/banners.md +9 -9
  19. package/skills/ti-admob-unified/references/contributing.md +14 -9
  20. package/skills/ti-admob-unified/references/dependencies.md +31 -25
  21. package/skills/ti-admob-unified/references/fullscreen.md +43 -28
  22. package/skills/ti-admob-unified/references/game-monetization.md +138 -7
  23. package/skills/ti-admob-unified/references/getting-started.md +75 -31
  24. package/skills/ti-admob-unified/references/index.md +5 -2
  25. package/skills/ti-admob-unified/references/integration.md +8 -6
  26. package/skills/ti-admob-unified/references/library.md +41 -10
  27. package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
  28. package/skills/ti-admob-unified/references/mediation.md +22 -9
  29. package/skills/ti-admob-unified/references/migration.md +11 -7
  30. package/skills/ti-admob-unified/references/module-changelog.md +41 -11
  31. package/skills/ti-admob-unified/references/privacy.md +35 -8
  32. package/skills/ti-admob-unified/references/production.md +67 -0
  33. package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
  34. package/skills/ti-admob-unified/references/roadmap.md +28 -31
  35. package/skills/ti-admob-unified/references/source-notes.md +51 -4
  36. package/skills/ti-admob-unified/references/source-snapshot.json +249 -45
  37. package/skills/ti-admob-unified/references/third-party-notices.md +4 -4
  38. package/skills/ti-game/SKILL.md +23 -6
  39. package/skills/ti-game/references/api.md +34 -30
  40. package/skills/ti-game/references/debugging-performance.md +18 -9
  41. package/skills/ti-game/references/examples.md +103 -20
  42. package/skills/ti-game/references/input.md +101 -0
  43. package/skills/ti-game/references/patterns.md +39 -15
  44. package/skills/ti-game/references/physics.md +80 -52
  45. package/skills/ti-game/references/project-setup.md +118 -56
  46. package/skills/ti-game/references/recipes-action.md +107 -8
  47. package/skills/ti-game/references/recipes-adventure-effects.md +41 -9
  48. package/skills/ti-game/references/roadmap.md +10 -3
  49. package/skills/ti-game/references/source-notes.md +81 -0
  50. package/skills/ti-game/references/tilemaps.md +12 -4
  51. package/skills/ti-synthengine/SKILL.md +81 -95
  52. package/skills/ti-synthengine/assets/example/README.md +84 -0
  53. package/skills/ti-synthengine/assets/example/app.js +1178 -0
  54. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
  55. package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
  56. package/skills/ti-synthengine/references/advanced-examples.md +174 -0
  57. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  58. package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
  59. package/skills/ti-synthengine/references/api.md +434 -380
  60. package/skills/ti-synthengine/references/audio-engine.md +157 -230
  61. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  62. package/skills/ti-synthengine/references/examples.md +123 -253
  63. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  64. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  65. package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
  66. package/skills/ti-synthengine/references/guide.md +188 -156
  67. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  68. package/skills/ti-synthengine/references/performance-review.json +1146 -0
  69. package/skills/ti-synthengine/references/physical-validation.json +1457 -0
  70. package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
  71. package/skills/ti-synthengine/references/recipes.md +423 -599
  72. package/skills/ti-synthengine/references/recommendations.md +63 -113
  73. package/skills/ti-synthengine/references/sound-design.md +134 -258
  74. package/skills/ti-synthengine/references/source-notes.md +93 -105
  75. package/skills/ti-synthengine/references/source-snapshot.json +379 -32
  76. package/skills/ti-synthengine/references/stats-performance.md +64 -20
  77. package/skills/ti-synthengine/references/studio-validation.json +6202 -0
  78. package/skills/ti-synthengine/references/troubleshooting.md +109 -121
  79. package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
  80. package/skills/ti-synthengine/references/unified-validation.json +314 -0
  81. package/AGENTS-VERCEL-RESEARCH.md +0 -175
@@ -1,10 +1,8 @@
1
- > Source snapshot: official TiSynthEngine 1.0.0 beta documentation from the reviewed working tree based on `95901c98af5353b86d9dc7c60b29ab9186b038cd` (2026-10-14), including uncommitted statistics changes. Upstream `documentation/audio-engine.md` SHA-256: `7b5707ee22e01a351af3633be14458726f8956069fb9355d1fb31cc4f7b9438a`.
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`.
2
2
 
3
3
  # Audio engine
4
4
 
5
- This page explains the native signal path behind the JavaScript API. You do not
6
- need it to play a sound, but it helps when choosing a mixing profile, sizing the
7
- voice pool or investigating device behavior.
5
+ The native signal path explains how mixing profiles and voice capacity affect the output. Use this reference when choosing those settings or investigating device behavior; it is optional for basic playback.
8
6
 
9
7
  <!-- TOC-START -->
10
8
  ## Contents
@@ -25,112 +23,86 @@ voice pool or investigating device behavior.
25
23
  - [Route behavior](#route-behavior)
26
24
  - [Regression coverage](#regression-coverage)
27
25
  - [Binaural front-plane renderer](#binaural-front-plane-renderer)
26
+ - [Own-platform performance verification](#own-platform-performance-verification)
28
27
  - [Engine statistics](#engine-statistics)
28
+ - [Sustained voices](#sustained-voices)
29
+ - [Native transport and processing](#native-transport-and-processing)
30
+ - [Unified transport acceptance boundaries](#unified-transport-acceptance-boundaries)
29
31
 
30
32
  <!-- TOC-END -->
31
33
 
32
34
  ## Signal path
33
35
 
34
36
  ```text
35
- playTone / playChord / playPattern
36
- |
37
- strict validation
38
- |
39
- native command ring buffer
40
- |
41
- voice pool: source (or FM) -> quantizer -> one-pole low-pass
42
- -> state-variable filter -> envelope -> gain
43
- -> stereo pan OR binaural FIR
44
- |
45
- sum voices and steal tails
46
- |
47
- smoothed master volume
48
- |
49
- stereo-linked lookahead limiter
50
- |
51
- floating-point output
37
+ playTone / playChord / noteOn playSequence / updateSequence
38
+ strict commands atomic preallocated score banks
39
+ \ /
40
+ shared 32-voice renderer
41
+ source/FM -> quantizer -> filters -> envelope -> event gain
42
+ stereo pan OR binaural FIR
43
+ |
44
+ per-voice category selection / explicit send
45
+ / \
46
+ selected share dry share
47
+ input gain -> chorus -> delay -> reverb input gain
48
+ master -> optional saturation master
49
+ \ /
50
+ stereo recombination
51
+ one final lookahead limiter
52
+ floating-point output
52
53
  ```
53
54
 
54
- The audio callback does not allocate memory or wait on a lock. JavaScript and
55
- the pattern scheduler publish small commands; the real-time thread consumes
56
- them and renders the samples.
55
+ The audio callback does not allocate memory or wait on a lock. JavaScript publishes control commands and immutable score banks; the real-time thread schedules and renders the samples.
57
56
 
58
57
  ## Oscillators
59
58
 
60
59
  The engine has six sources:
61
60
 
62
- | Waveform | Source behavior |
63
- | --- | --- |
64
- | `SINE` | Direct sinusoid. |
65
- | `SQUARE` | PolyBLEP band-limited discontinuity. |
66
- | `SAWTOOTH` | PolyBLEP band-limited discontinuity. |
67
- | `TRIANGLE` | Continuous triangle oscillator. |
68
- | `NOISE` | White or variable-rate sample-and-hold pseudo-random generator. |
69
- | `WAVETABLE` | Caller-supplied 2 to 64-sample stepped cycle. |
70
-
71
- Pitch can move linearly from `frequency` to `frequencyEnd`. A sine LFO adds
72
- `lfoDepth * sin(lfoPhase)` in hertz before the oscillator advances. This makes
73
- vibrato depth absolute in hertz rather than cents.
74
-
75
- Noise rate advances an independent phase accumulator. A zero `noiseRate` uses
76
- one new pseudo-random value per output sample; a positive rate holds that value
77
- until the accumulator crosses the next update. `noiseRateEnd` changes the
78
- increment linearly over the event.
79
-
80
- The source is optionally quantized to the selected `bitDepth` (2 to 16 bits)
81
- and then passes through a one-pole low-pass filter when `lowPassHz` is nonzero.
82
- Square waves use `pulseWidth` for the second PolyBLEP discontinuity and can
83
- interpolate that duty cycle toward `pulseWidthEnd`.
61
+ | Waveform | Source behavior |
62
+ | ----------- | --------------------------------------------------------------- |
63
+ | `SINE` | Direct sinusoid. |
64
+ | `SQUARE` | PolyBLEP band-limited discontinuity. |
65
+ | `SAWTOOTH` | PolyBLEP band-limited discontinuity. |
66
+ | `TRIANGLE` | Continuous triangle oscillator. |
67
+ | `NOISE` | White or variable-rate sample-and-hold pseudo-random generator. |
68
+ | `WAVETABLE` | Caller-supplied 2 to 64-sample stepped cycle. |
69
+
70
+ Pitch can move linearly from `frequency` to `frequencyEnd`. A sine LFO adds `lfoDepth * sin(lfoPhase)` in hertz before the oscillator advances. This makes vibrato depth absolute in hertz rather than cents.
71
+
72
+ Noise rate advances an independent phase accumulator. A zero `noiseRate` uses one new pseudo-random value per output sample; a positive rate holds that value until the accumulator crosses the next update. `noiseRateEnd` changes the increment linearly over the event.
73
+
74
+ The source is optionally quantized to the selected `bitDepth` (2 to 16 bits) and then passes through a one-pole low-pass filter when `lowPassHz` is nonzero. Square waves use `pulseWidth` for the second PolyBLEP discontinuity and can interpolate that duty cycle toward `pulseWidthEnd`.
84
75
 
85
76
  ### Frequency modulation
86
77
 
87
- A `SINE` voice with a positive `fmDepth`, or with `fmDepthEnd`, is phase
88
- modulated: the carrier computes `sin(2 * pi * phase + depth * sin(2 * pi *
89
- modPhase))`. The modulator phase advances by the carrier's instantaneous phase
90
- increment times `fmRatio`, so pitch sweeps and vibrato keep the ratio. The
91
- index moves linearly from `fmDepth` to `fmDepthEnd` over the event. With a depth
92
- of exactly 0 and no end, the voice takes the plain sine path.
78
+ A `SINE` voice with a positive `fmDepth`, or with `fmDepthEnd`, is phase modulated: the carrier computes `sin(2 * pi * phase + depth * sin(2 * pi * modPhase))`. The modulator phase advances by the carrier's instantaneous phase increment times `fmRatio`, so pitch sweeps and vibrato keep the ratio. The index moves linearly from `fmDepth` to `fmDepthEnd` over the event. With a depth of exactly 0 and no end, the voice takes the plain sine path.
93
79
 
94
- The modulated sine has constant amplitude, so event gain is unchanged. Nothing
95
- band-limits the sidebands, so a high carrier with a large index aliases.
80
+ The modulated sine has constant amplitude, so event gain is unchanged. Nothing band-limits the sidebands, so a high carrier with a large index aliases.
96
81
 
97
82
  ### State-variable filter
98
83
 
99
- `filterType` selects a topology-preserving state-variable filter that runs
100
- after the one-pole `lowPassHz` stage. It is stable while its coefficients move,
101
- which is why a cutoff sweep cannot blow up even at high Q.
84
+ `filterType` selects a topology-preserving state-variable filter that runs after the one-pole `lowPassHz` stage. It stays stable as its coefficients change, including during a cutoff sweep at high Q.
102
85
 
103
- | Output | Response |
104
- | --- | --- |
105
- | `lowpass` | 12 dB per octave, peak of about Q at the cutoff. |
86
+ | Output | Response |
87
+ | ---------- | ------------------------------------------------ |
88
+ | `lowpass` | 12 dB per octave, peak of about Q at the cutoff. |
106
89
  | `highpass` | 12 dB per octave, peak of about Q at the cutoff. |
107
- | `bandpass` | Unity gain at the center for every Q. |
90
+ | `bandpass` | Unity gain at the center for every Q. |
108
91
 
109
- The cutoff is held between 20 Hz and `sampleRate * 0.45`. With `filterHzEnd`
110
- the cutoff follows `from * (to / from) ^ t`, where `t` runs from 0 to 1 across
111
- the event, and coefficients are recomputed every eighth sample. Without a
112
- sweep they are computed once, when the note starts.
92
+ The cutoff is held between 20 Hz and `sampleRate * 0.45`. For timed tones and chords, `filterHzEnd` follows `from * (to / from) ^ t`, where `t` runs from 0 to 1 across the event, and coefficients are recomputed every eighth sample. Native sequence cutoff sweeps instead use note length and then hold their target through release. The current Android renderer refreshes them every sixteenth sample, also forcing the target at the release deadline; the Apple renderer refreshes them every eighth sample. These renderer paths do not establish sample-identical platform output. Held live cutoff/Q and modulation updates have their own ramp/control paths. Without a sweep or live modulation, coefficients are computed once at note start.
113
93
 
114
94
  ## Envelope
115
95
 
116
- Each voice has a smoothstep attack, an optional smoothstep decay to `sustain`,
117
- a hold at that level and a smoothstep release. `duration` includes the whole
118
- envelope. Defaults are adjusted to fit a short event, while an explicit
119
- envelope that cannot fit is rejected by the contract.
96
+ Each voice has a smoothstep attack, an optional smoothstep decay to `sustain`, a hold at that level and a smoothstep release. For timed tones and chords, `duration` includes the whole envelope: defaults are adjusted to fit a short event, while an explicit envelope that cannot fit is rejected. Held voices release on `noteOff`; sequence notes release at their gate/length deadline. Held/sequence ADSR has no timed-event duration budget, and their release may continue beyond the sequence interval.
120
97
 
121
98
  ```text
122
99
  base = attack ramp, then decay ramp from 1 to sustain, then sustain
123
100
  level = base * release ramp
124
101
  ```
125
102
 
126
- The release ramp multiplies the base level, so a release that begins during the
127
- decay is continuous. With `decay: 0` and `sustain: 1` the base is 1 after the
128
- attack and the result is the plain attack, hold and release shape. Voice-steal
129
- tails read the final level, so they stay continuous with the decay too.
103
+ The release ramp multiplies the base level, so a release that begins during the decay is continuous. With `decay: 0` and `sustain: 1` the base is 1 after the attack and the result is the plain attack, hold and release shape. Voice-steal tails read the final level, so they stay continuous with the decay too.
130
104
 
131
- Voice stealing does not cut the old waveform at an arbitrary sample. The old
132
- voice contributes a short tail of roughly 6 ms while the slot begins the new
133
- event. `stopAll()` uses a release of roughly 20 ms for the same reason.
105
+ When a slot is reused, the old voice contributes a short tail of roughly 6 ms while the new event begins, avoiding a cut at an arbitrary sample. `stopAll()` uses a release of roughly 20 ms to avoid the same discontinuity.
134
106
 
135
107
  ## Event gain
136
108
 
@@ -140,124 +112,80 @@ In protected profiles, gain is fixed when an event starts:
140
112
  voiceGain = waveformGain(waveType) * profileFactor / sqrt(eventVoiceCount)
141
113
  ```
142
114
 
143
- One tone and one pattern step have an event voice count of 1. A chord uses the
144
- number of pitches in that chord. Dividing by the square root of the count keeps
145
- the event close to constant power without changing gain sample by sample.
115
+ One tone and one independently held note have an event voice count of 1. Chords, including sequence chords, use their initial pitch count for fixed event compensation. Adding or releasing other voices never rescales an existing event.
146
116
 
147
117
  Waveforms are calibrated toward a common source RMS:
148
118
 
149
- | Waveform | Base gain | Approximate output RMS |
150
- | --- | ---: | ---: |
151
- | Sine | 0.440 | 0.311 |
152
- | Square | 0.311 | 0.311 |
153
- | Sawtooth, triangle, noise and wavetable | 0.539 | signal-dependent |
119
+ | Waveform | Base gain | Approximate output RMS |
120
+ | --------------------------------------- | --------: | ---------------------: |
121
+ | Sine | 0.440 | 0.311 |
122
+ | Square | 0.311 | 0.311 |
123
+ | Sawtooth, triangle, noise and wavetable | 0.539 | signal-dependent |
154
124
 
155
- This calculation is why one three-note `playChord` and three `playTone` calls
156
- are not equivalent. A protected sine triad assigns about 0.254 to each chord
157
- voice (`0.440 / sqrt(3)`). Three separate tone events each receive 0.440, for a
158
- much hotter worst-case sum.
125
+ This calculation is why one three-note `playChord` and three `playTone` calls are not equivalent. A protected sine triad assigns about 0.254 to each chord voice (`0.440 / sqrt(3)`). Three separate tone events each receive 0.440, for a much hotter worst-case sum.
159
126
 
160
127
  ## Mixing profiles
161
128
 
162
- The profile is fixed when `startEngine()` opens the stream.
129
+ The profile is fixed when `start()` opens the stream.
163
130
 
164
- | Profile | Factor | Sine gain | Limiter ceiling | Intended use |
165
- | --- | ---: | ---: | ---: | --- |
166
- | `conservative` | 1 | 0.440 | 0.85 | Default headroom for polyphony and effects. |
167
- | `balanced` | `sqrt(2)` | 0.622 | 0.85 | Mostly one or two simultaneous voices. |
168
- | `speakerSafe` | `1 / sqrt(2)` | 0.311 | 0.70 | Small speakers and demanding low pitches. |
169
- | `raw` | no source reduction | 1.000 | 0.85 | App-controlled gain staging. |
131
+ | Profile | Factor | Sine gain | Limiter ceiling | Intended use |
132
+ | -------------- | ------------------: | --------: | --------------: | ------------------------------------------- |
133
+ | `conservative` | 1 | 0.440 | 0.85 | Default headroom for polyphony and effects. |
134
+ | `balanced` | `sqrt(2)` | 0.622 | 0.85 | Mostly one or two simultaneous voices. |
135
+ | `speakerSafe` | `1 / sqrt(2)` | 0.311 | 0.70 | Small speakers and demanding low pitches. |
136
+ | `raw` | no source reduction | 1.000 | 0.85 | App-controlled gain staging. |
170
137
 
171
- `raw` lets one full-volume oscillator reach full scale before the master. Any
172
- overlap then relies on the limiter, so it is not a general-purpose loudness
173
- setting.
138
+ `raw` lets one full-volume oscillator reach full scale before the master. Any overlap then relies on the limiter, so it is not a general-purpose loudness setting.
174
139
 
175
140
  ## Limiter
176
141
 
177
- A single stereo-linked peak limiter sits after master volume. Linking preserves
178
- the stereo image when only one channel has the largest peak.
142
+ 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.
179
143
 
180
- | Parameter | Value |
181
- | --- | --- |
182
- | Lookahead | 10 ms |
183
- | Release | 100 ms, exponential |
184
- | Ceiling | 0.85, or 0.70 with `speakerSafe` |
144
+ | Parameter | Value |
145
+ | --------- | -------------------------------- |
146
+ | Lookahead | 10 ms |
147
+ | Release | 100 ms, exponential |
148
+ | Ceiling | 0.85, or 0.70 with `speakerSafe` |
185
149
 
186
- Lookahead gives the gain time to reach a safe value before the peak reaches the
187
- output. Below the ceiling the limiter converges back to unity. It is meant to
188
- catch exceptional sums, not to normalize every callback.
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.
189
151
 
190
- Repeated overlapping events can make any limiter audibly raise and lower the
191
- bus. A loop that starts a 300 ms chord every 100 ms is therefore a useful load
192
- test and a poor tonal reference. Judge the chord with one sustained event; use
193
- the overlapping loop only when testing scheduling, stealing and recovery.
152
+ 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.
194
153
 
195
154
  ## Master volume and pan
196
155
 
197
- `setVolume()` ramps the master rather than applying a discontinuous gain step.
198
- Per-event `volume` is multiplied before the bus. Values up to 2 are valid, but
199
- values above 1 use headroom and make limiting more likely.
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.
200
157
 
201
- Pan uses equal-power coefficients. A centered sound feeds both channels; the
202
- sum does not simply double. `panEnd` interpolates the position across a tone or
203
- pattern step. Chords use `panSpread` or the explicit `pans` array.
158
+ 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.
204
159
 
205
160
  ## Voice pool
206
161
 
207
- `maxVoices` reserves between 1 and 32 active voice slots. The setting controls
208
- capacity, not event gain. When every slot is occupied, the voice with the least
209
- time remaining is faded and its slot is reused.
162
+ `maxVoices` reserves between 1 and 32 active voice slots. The setting controls capacity and leaves event gain unchanged. Replacement first considers the lowest eligible priority (0-3); higher-priority voices are protected. Among that priority, timed-only replacement selects least remaining duration; sustained/transport replacement prefers releasing voices, then oldest attack. A reused slot receives a short fade tail; no category owns reserved slots.
210
163
 
211
- A chord is one command with up to 32 pitches. If its size exceeds the session's
212
- voice capacity, slots with the least time remaining are reused. For normal apps,
213
- choose the largest real overlap plus room for release tails.
164
+ A chord is an atomic group of up to 32 pitches. If capacity or priority cannot accommodate every pitch, the entire group is dropped. When any held/transport voice is active, that policy also applies to incoming timed tones/chords. For normal apps, choose the largest real overlap plus room for release tails.
214
165
 
215
166
  ## Command queue
216
167
 
217
- Both ports use a 128-slot single-consumer ring buffer. Producers are serialized
218
- with a lock that the audio callback never touches. When note commands cannot be
219
- queued, the public call returns `false` and logs an `overflow` warning.
168
+ Both ports use a 128-slot single-consumer ring buffer. Producers are serialized with a lock that the audio callback never touches. When note commands cannot be queued, the public call returns `false` and logs an `overflow` warning.
220
169
 
221
- `playChord()` publishes its note data and advances the queue index once. The
222
- consumer receives the complete event or nothing. `stopAll` and `shutdown` have
223
- a control fallback so cleanup is not lost under queue pressure.
170
+ `playChord()` publishes its note data and advances the queue index once. The consumer receives the complete event or nothing. `stopAll` and `shutdown` have a control fallback so cleanup is not lost under queue pressure.
224
171
 
225
- Pattern timing runs outside the audio callback. A platform timer places each
226
- step on the command queue. The sequence is suitable for game cues and simple
227
- musical patterns, but it is not a sample-accurate composition timeline. The
228
- same timer fires `patternend`, about 1 ms after the sum of the step lengths,
229
- and the event is handed to the JavaScript runtime asynchronously.
172
+ `playSequence` runs inside the sample-clock callback; JavaScript publishes entire configurations rather than individual notes. Scheduled releases execute independently of public command-queue capacity. `sequenceend` records are delivered outside the callback and retain their sequence identifier when slots are reused.
230
173
 
231
174
  ## Shared DSP core
232
175
 
233
- The envelope, the state-variable filter, the sweep helpers, the FM sine and
234
- the cents conversion live once, in `shared/synth_core.h`. It is plain C,
235
- header-only and allocation-free. The Android renderer includes it from
236
- `android/jni/SynthOboeEngine.cpp` and the iOS renderer from
237
- `ios/Classes/TiSynthengineModule.m`, so both platforms run the same code. Voice
238
- management, mixing and the limiter are still separate per platform.
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.
239
177
 
240
178
  ## Android implementation
241
179
 
242
- Android renders in C++ through Oboe. The stream requests floating-point output,
243
- low-latency performance and exclusive sharing when the device supports it. If
244
- exclusive access is unavailable, Oboe can use shared mode.
180
+ Android renders in C++ through Oboe. The stream requests floating-point output, low-latency performance and exclusive sharing when the device supports it. If exclusive access is unavailable, Oboe can use shared mode.
245
181
 
246
- The renderer mixes in the data callback and targets a small device buffer. A
247
- route or stream error triggers a reopen on the current output. The supported
248
- ABIs are `arm64-v8a`, `armeabi-v7a`, `x86` and `x86_64`.
182
+ The renderer mixes in the data callback and targets a small device buffer. A route or stream error triggers a reopen on the current output. The supported ABIs are `arm64-v8a`, `armeabi-v7a`, `x86` and `x86_64`.
249
183
 
250
184
  ## iOS implementation
251
185
 
252
- iOS renders with `AVAudioEngine` and `AVAudioSourceNode`, using the active
253
- session's sample rate. The module configures the audio session as `Playback`,
254
- or `Ambient` when `startEngine()` receives `audioSession: 'ambient'`, activates
255
- it, and observes route and engine configuration notifications. If a route
256
- change stops the graph, the port reapplies the category, reactivates the
257
- session and resumes the existing renderer.
186
+ 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.
258
187
 
259
- The module targets iOS 15 or newer. The module archive contains device,
260
- simulator and Mac Catalyst slices.
188
+ The module targets iOS 15 or newer. The module archive contains device, simulator and Mac Catalyst slices.
261
189
 
262
190
  ## Latency
263
191
 
@@ -265,23 +193,17 @@ End-to-end latency contains three main pieces:
265
193
 
266
194
  1. The fixed 10 ms limiter lookahead.
267
195
  2. The hardware and operating-system output buffer.
268
- 3. JavaScript or platform timer scheduling before the command reaches audio.
196
+ 3. JavaScript publication and callback-boundary latency for a newly submitted event; accepted sequence steps then follow the sample clock.
269
197
 
270
- Immediate calls are suitable for interface and game feedback. A visual or
271
- musical event that must align to a known future time should be scheduled ahead
272
- by the app instead of relying on a JavaScript callback at that instant.
198
+ 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.
273
199
 
274
200
  ## Route behavior
275
201
 
276
- Connecting headphones or changing another output route can interrupt the
277
- hardware stream. The ports recover automatically, though the event crossing
278
- the transition may be cut short and the operating system may keep the new
279
- route muted briefly. Start later sounds normally after the route settles.
202
+ Connecting headphones or changing another output route can interrupt the hardware stream. The ports recover automatically, though the event crossing the transition may be cut short and the operating system may keep the new route muted briefly. Start later sounds normally after the route settles.
280
203
 
281
204
  ## Regression coverage
282
205
 
283
- The repository checks the public contract and both source ports without a
284
- device:
206
+ The repository checks the public contract and both source ports without a device:
285
207
 
286
208
  ```sh
287
209
  node tests/api_contract_regression.js
@@ -301,10 +223,7 @@ java -cp "$test_dir" ti.synthengine.SynthSpatialContractRegression
301
223
  java -cp "$test_dir" ti.synthengine.SynthDspRegression
302
224
  ```
303
225
 
304
- The DSP suite covers stable RMS from 1 through 32 voices, limiter transparency
305
- and recovery, waveform discontinuities, envelope endpoints, master smoothing,
306
- voice stealing and representative dense chords. Device tests remain necessary
307
- for route changes, startup underruns and speaker acoustics.
226
+ The DSP suite covers stable RMS from 1 through 32 voices, limiter transparency and recovery, waveform discontinuities, envelope endpoints, master smoothing, voice stealing and representative dense chords. Device tests remain necessary for route changes, startup underruns and speaker acoustics.
308
227
 
309
228
  Three more checks cover the shared core, the Android engine and the iOS contract.
310
229
 
@@ -317,18 +236,9 @@ cc -std=c99 -O2 -Wall -Wextra -o "$test_dir/dsp_core" \
317
236
  "$test_dir/dsp_core"
318
237
  ```
319
238
 
320
- It checks that the envelope equals the original attack, hold and release shape
321
- for every attack and release combination when decay is off, the decay and
322
- sustain endpoints and continuity, the response of each filter type (slope,
323
- cutoff and the resonance peak), stability of every filter type under a cutoff
324
- sweep on noise, FM sidebands against the Bessel function values and the cents
325
- conversion.
239
+ 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.
326
240
 
327
- The Android engine renders on the host. `tests/engine_render_host.cpp` compiles
328
- `SynthOboeEngine.cpp` without opening an audio stream and drives the same
329
- command and callback path Oboe uses. It needs the Oboe headers, which any
330
- Gradle cache that built the module has, and the JDK's `jni.h`, so it is a local
331
- check and does not run in CI:
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:
332
242
 
333
243
  ```sh
334
244
  OBOE=$(find ~/.gradle -path '*oboe-1.10.0/prefab/modules/oboe/include' | head -1)
@@ -341,18 +251,9 @@ clang++ -std=c++17 -O1 -Wall -I"$OBOE" -I"$JH/include" -I"$JH/include/darwin" \
341
251
  "$test_dir/engine_render"
342
252
  ```
343
253
 
344
- It renders decay and sustain levels, filter attenuation, an opening cutoff
345
- sweep, FM sidebands and an FM index that decays to nothing, plus voice stealing
346
- and `stopAll` with every control active. Built without `-DHAS_EXPRESSIVE` it
347
- renders only the original controls and prints one hash per scenario, so the
348
- same file can be built against an older copy of the engine and the hashes
349
- compared.
254
+ 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.
350
255
 
351
- The iOS contract replays the cases that the Java contract test writes to
352
- `tests/contract_corpus.jsonl`. The Java test fails when that file no longer
353
- matches the Java contract; regenerate it from `android/` with
354
- `-Dsynth.corpus.out=../tests/contract_corpus.jsonl`. On macOS, run the iOS
355
- side from the repository root:
256
+ 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:
356
257
 
357
258
  ```sh
358
259
  test_dir=$(mktemp -d /tmp/ti-synth-ios.XXXXXX)
@@ -362,14 +263,39 @@ clang -fobjc-arc -Wall -Wextra -framework Foundation \
362
263
  "$test_dir/contract_corpus_ios"
363
264
  ```
364
265
 
365
- The iOS renderer shares the DSP math through the same header, but nothing
366
- renders its output on the host, so check it on a device.
266
+ The iOS renderer uses the same DSP header. Its actual Objective-C render body has a host PCM/state comparison described below; device scheduling and audible output require separate checks.
367
267
 
368
268
  ## Binaural front-plane renderer
369
269
 
370
270
  `shared/synth_spatial.h` runs the same allocation-free FIR processor in both native ports, after each voice envelope and before summation/master/limiter. Nine stereo impulse responses from Gardner and Martin's MIT KEMAR measurements form a compact front-plane grid. The complete 128-sample source responses at 44.1 kHz are interpolated across direction and linearly resampled once at voice onset to the output rate. A single front-response energy calibration preserves directional differences. At 48 kHz the convolution uses 140 padded taps per ear. Buffers are reserved with the voice/tail pools; there are no runtime downloads, callback allocations, or additional callback locks.
371
271
 
372
- 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. A fixed position avoids changing the filter during an event. The intended output-rate range is 8 to 192 kHz. Interpolated generic HRTFs provide directional cues, not guaranteed individual localization.
272
+ 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
+
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.
275
+
276
+ ## Own-platform performance verification
277
+
278
+ 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
+
280
+ 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
+
282
+ `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
+
284
+ ```sh
285
+ mkdir -p /tmp/synth-reference
286
+ git archive 7ab3110 | tar -x -C /tmp/synth-reference
287
+ python3 tools/build_ios_render_host.py --source /tmp/synth-reference --output /tmp/apple-reference
288
+ python3 tools/build_ios_render_host.py --output /tmp/apple-candidate
289
+ python3 tools/compare_ios_render.py /tmp/apple-reference /tmp/apple-candidate
290
+ python3 tools/benchmark_performance_review.py --baseline /tmp/apple-reference --candidate /tmp/apple-candidate --output /tmp/apple-performance.json
291
+ /tmp/apple-candidate --metadata
292
+ ```
293
+
294
+ The build tool defaults to the module's Apple Release `-Os` optimization. The comparator streams PCM for every fixture block and full original voice/tail, FIR/filter, limiter, effect-buffer and transport state at fixture checkpoints. Cases include five engine rates, zero/short/irregular blocks, all sources, held updates, replacements, send and position ramps, four sequences, effects, panic, drain and later re-enabling. A deliberately changed phase increment was rejected as a negative control.
295
+
296
+ Finish compilation and fidelity checks before timing. The benchmark uses fixed matched warmups and at least ten alternating pairs; CPU and elapsed totals, p95, maxima and process high-water memory remain separate. Investigate repeatable total regressions above 2% or p95 regressions above 5%. Short-process or single-kernel gains do not establish whole-app performance. The Android producer harness exercises the actual JNI publication with supplied JVM array accessors, so its numbers exclude Java validation and VM allocation time.
297
+
298
+ Full-score producers reserve a free bank without copying the previous score. Generic partial-bank callers still inherit the latest accepted bank, and publication retains its ownership, validation and mailbox checks. The iOS producer commits an already prepared score once; Android prepares its complete packet at native publication. Empty completion polls and empty metadata pruning avoid transient objects. Accepted edits of unknown iOS IDs do not create retained spatial metadata. Sequence capacity, diagnostics, event delivery cadence and public defaults are unchanged.
373
299
 
374
300
  The shared processor check is `cc -std=c99 -O2 -Wall -Wextra -Wpedantic tests/spatial_regression.c -lm -o /tmp/spatial_regression` followed by `/tmp/spatial_regression`. `tests/spatial_engine_host.cpp` uses the same compile flags as `engine_render_host.cpp` and verifies native left/right rendering, elevation changes, stealing and stopping. Java spatial validation is checked by `SynthSpatialContractRegression`; the common Java/iOS corpus also covers spatial acceptance and conflicts.
375
301
 
@@ -391,26 +317,11 @@ The script compares stereo and binaural using a warmup and five measured trials,
391
317
 
392
318
  ## Engine statistics
393
319
 
394
- The [statistics API](api.md#getstats) has the same shape on both platforms.
395
- `shared/synth_stats.h` owns telemetry math and publication. Voice transitions
396
- update render-owned occupancy and maxima, including primary FIR draining and
397
- external replacement/stop tails. Whole empty groups can then be skipped without
398
- changing the signal. Producers are serialized separately from the renderer;
399
- querying queue depth does not include future scheduled pattern steps.
400
-
401
- The callback publishes atomic words behind a sequence counter. Readers retry a
402
- snapshot if publication overlaps their read; the callback never waits for them.
403
- Eight-byte alignment and a compile-time lock-free assertion also cover Android's
404
- 32-bit ABIs. Reset/enable requests are consumed at callback boundaries. A new
405
- control generation hides stale optional measurements immediately. Shutdown waits
406
- for the backend callback to stop before clearing occupancy. Queries allocate
407
- result objects on the calling thread. Pattern publication and shutdown share a
408
- control monitor; that monitor is never taken by the audio callback.
409
-
410
- Diagnostic timing uses a monotonic host clock only while enabled. Stereo peak,
411
- RMS and maximum limiter reduction accumulate over approximately 200 ms of final
412
- float output; logarithms are calculated at window boundaries. These readings do
413
- not diagnose physical distortion, latency, heat, battery use or overall app CPU.
320
+ The [statistics API](api.md#getstats) has the same shape on both platforms. `shared/synth_stats.h` owns telemetry math and publication. Voice transitions update render-owned occupancy and maxima, including primary FIR draining and external replacement/stop tails. Whole empty groups can then be skipped without changing the signal. Producers are serialized separately from the renderer; querying queue depth does not include future scheduled pattern steps.
321
+
322
+ The callback publishes atomic words behind a sequence counter. Readers retry a snapshot if publication overlaps their read; the callback never waits for them. Eight-byte alignment and a compile-time lock-free assertion also cover Android's 32-bit ABIs. Reset/enable requests are consumed at callback boundaries. A new control generation hides stale optional measurements immediately. Shutdown waits for the backend callback to stop before clearing occupancy. Queries allocate result objects on the calling thread. Pattern publication and shutdown share a control monitor; that monitor is never taken by the audio callback.
323
+
324
+ Diagnostic timing uses a monotonic host clock only while enabled. Stereo peak, RMS and maximum limiter reduction accumulate over approximately 200 ms of final float output; logarithms are calculated at window boundaries. These readings do not diagnose physical distortion, latency, heat, battery use or overall app CPU.
414
325
 
415
326
  Run portable telemetry and Java API tests with:
416
327
 
@@ -420,19 +331,35 @@ c++ -std=c++17 -O2 -Wall -Wextra -pthread tests/stats_regression.cpp -o /tmp/sta
420
331
  python3 tests/run_stats_java.py
421
332
  ```
422
333
 
423
- `tests/stats_engine_host.cpp` uses the Oboe/JDK host flags shown above. It tests
424
- native occupancy, reset while playing, saturated queues, binaural drain,
425
- shutdown and exact output equality with diagnostics enabled/disabled. This
426
- native integration harness requires local Oboe headers; portable shared tests
427
- and the stopped-engine Java API tests also run in CI. iOS builds use the same
428
- telemetry core, but host Android renders do not execute the iOS audio callback.
429
-
430
- `tests/stats_benchmark_host.cpp` extends the spatial replay harness with silence,
431
- one/32 voices, replacement pressure, chords, scheduled note publications and
432
- short FIR drains in stereo/binaural. Build it twice with identical `-O3` flags:
433
- once against the previous engine using `-DSTATS_BASELINE`, and once against the
434
- current engine. Run `python3 tools/benchmark_stats.py BASELINE CURRENT` for one
435
- warmup and ten alternating trials/configuration, including current snapshots at
436
- 5 Hz. The harness verifies identical output hashes across all modes. See the
437
- [measured comparison and limitations](stats-performance.md) before interpreting
438
- host percentages as a device guarantee.
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.
335
+
336
+ `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
+
338
+ ## Sustained voices
339
+
340
+ `noteOn`, `noteOff` and `updateVoice` use the existing bounded native command queue. Voice storage, smoothing ramps and identity snapshots are preallocated. The callback owns envelopes, oscillator/filter state and replacement decisions; it does not allocate or acquire producer locks. `shared/synth_held.h` supplies the same held ADSR, current-level release and independent 10 ms linear parameter ramps to both renderers. Held attack/decay counters stop advancing after those stages, avoiding sample-counter overflow on long notes.
341
+
342
+ IDs are app-supplied positive 32-bit integers. Atomic 32-bit identity snapshots and queued attacks let producers validate update source compatibility without reading mutable DSP state. The callback checks compatibility again. A full queue rejects note release with `false`; the caller owns its bounded retry and shutdown policy. See the [sustained contract](api.md#noteonoptions) for immutable configuration and supported updates.
343
+
344
+ Run `tests/held_core_regression.c` with C11 for shared envelope/ramp/identity checks. `tests/held_engine_host.cpp` uses the same build flags as `engine_render_host.cpp`; it covers long notes, release stages, phase-preserving targets, source capabilities, replacement, stale IDs, queue saturation and 32 mixed-source spatial voices. The Java held contract regression and the shared Java/iOS corpus verify the public option shapes. Host rendering and successful builds do not establish device audio quality or touch responsiveness.
345
+
346
+
347
+ ## Native transport and processing
348
+
349
+ `shared/synth_transport.h` owns up to four independent sequences and one global tempo/beat/millisecond clock; `shared/synth_sequence.h` advances each traversal with compensated sample increments. Beat lengths follow tempo changes; millisecond lengths remain constant. A serialized producer prepares sorted traversal and all pitches before publishing one of three immutable banks. The callback accepts tempo at the block boundary and structure at a step boundary; swing changes at a pair boundary. Transport voices share the sustained pool, retain owner identifiers and absolute note deadlines beyond internal slot reuse and release independently of the public command queue. See the [public transport contract](api.md#native-sequence-transport).
350
+
351
+ `shared/synth_effects.h` supplies chorus, ping-pong delay, damped Schroeder reverb and optional saturation; `shared/synth_modulation.h` supplies two phase-preserving LFOs. Buffers allocate at engine initialization and free after the audio backend stops. Epoch tags on 16-sample pages invalidate histories during panic without walking delay memory; recovery clears at most one short page on a first write. Effects start disabled and default rendering follows exact bypass. Enabled effects drain on disable, bounded to 30 seconds. No new external DSP dependency is used.
352
+
353
+ Pre-limiter peak and maximum limiter reduction accumulate natively until reset, so polling cannot miss a brief intervention. The callback tracks linear extrema and converts to decibels once per block. Existing output peak/RMS and reduction windows remain approximately 200 ms. Diagnostics are optional; host measurements do not establish device audio deadlines or listening acceptance.
354
+
355
+ 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
+
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.
358
+
359
+ 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
+
361
+ The [Studio technical validation report](studio-validation.json) records the calibration and corrected full-chain host benchmark, including failed deadline acceptance and pending device checks. Its host performance result must not be presented as hardware realtime acceptance.
362
+
363
+ ## Unified transport acceptance boundaries
364
+
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.