@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.
- package/CONTRIBUTING.md +82 -0
- package/README.md +83 -72
- package/agents/ti-pro.md +1 -1
- package/commands/ti-audit.md +2 -2
- package/docs/examples.md +91 -0
- package/docs/knowledge-index.md +57 -0
- package/lib/commands/uninstall.js +19 -16
- package/lib/commands/update.js +1 -1
- package/lib/downloader.js +1 -1
- package/lib/utils.js +2 -0
- package/package.json +4 -2
- package/skills/purgetss/references/multi-density-images.md +1 -1
- package/skills/ti-admob-unified/SKILL.md +21 -7
- package/skills/ti-admob-unified/assets/example/app.js +229 -25
- package/skills/ti-admob-unified/assets/example/lib/ads.js +30 -2
- package/skills/ti-admob-unified/assets/example/library-usage.js +33 -1
- package/skills/ti-admob-unified/references/api-reference.md +39 -16
- package/skills/ti-admob-unified/references/banners.md +9 -9
- package/skills/ti-admob-unified/references/contributing.md +14 -9
- package/skills/ti-admob-unified/references/dependencies.md +31 -25
- package/skills/ti-admob-unified/references/fullscreen.md +43 -28
- package/skills/ti-admob-unified/references/game-monetization.md +138 -7
- package/skills/ti-admob-unified/references/getting-started.md +75 -31
- package/skills/ti-admob-unified/references/index.md +5 -2
- package/skills/ti-admob-unified/references/integration.md +8 -6
- package/skills/ti-admob-unified/references/library.md +41 -10
- package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
- package/skills/ti-admob-unified/references/mediation.md +22 -9
- package/skills/ti-admob-unified/references/migration.md +11 -7
- package/skills/ti-admob-unified/references/module-changelog.md +41 -11
- package/skills/ti-admob-unified/references/privacy.md +35 -8
- package/skills/ti-admob-unified/references/production.md +67 -0
- package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
- package/skills/ti-admob-unified/references/roadmap.md +28 -31
- package/skills/ti-admob-unified/references/source-notes.md +51 -4
- package/skills/ti-admob-unified/references/source-snapshot.json +249 -45
- package/skills/ti-admob-unified/references/third-party-notices.md +4 -4
- package/skills/ti-game/SKILL.md +23 -6
- package/skills/ti-game/references/api.md +34 -30
- package/skills/ti-game/references/debugging-performance.md +18 -9
- package/skills/ti-game/references/examples.md +103 -20
- package/skills/ti-game/references/input.md +101 -0
- package/skills/ti-game/references/patterns.md +39 -15
- package/skills/ti-game/references/physics.md +80 -52
- package/skills/ti-game/references/project-setup.md +118 -56
- package/skills/ti-game/references/recipes-action.md +107 -8
- package/skills/ti-game/references/recipes-adventure-effects.md +41 -9
- package/skills/ti-game/references/roadmap.md +10 -3
- package/skills/ti-game/references/source-notes.md +81 -0
- package/skills/ti-game/references/tilemaps.md +12 -4
- package/skills/ti-synthengine/SKILL.md +81 -95
- package/skills/ti-synthengine/assets/example/README.md +84 -0
- package/skills/ti-synthengine/assets/example/app.js +1178 -0
- package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
- package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
- package/skills/ti-synthengine/references/advanced-examples.md +174 -0
- package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
- package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
- package/skills/ti-synthengine/references/api.md +434 -380
- package/skills/ti-synthengine/references/audio-engine.md +157 -230
- package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
- package/skills/ti-synthengine/references/examples.md +123 -253
- package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
- package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
- package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
- package/skills/ti-synthengine/references/guide.md +188 -156
- package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
- package/skills/ti-synthengine/references/performance-review.json +1146 -0
- package/skills/ti-synthengine/references/physical-validation.json +1457 -0
- package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
- package/skills/ti-synthengine/references/recipes.md +423 -599
- package/skills/ti-synthengine/references/recommendations.md +63 -113
- package/skills/ti-synthengine/references/sound-design.md +134 -258
- package/skills/ti-synthengine/references/source-notes.md +93 -105
- package/skills/ti-synthengine/references/source-snapshot.json +379 -32
- package/skills/ti-synthengine/references/stats-performance.md +64 -20
- package/skills/ti-synthengine/references/studio-validation.json +6202 -0
- package/skills/ti-synthengine/references/troubleshooting.md +109 -121
- package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
- package/skills/ti-synthengine/references/unified-validation.json +314 -0
- package/AGENTS-VERCEL-RESEARCH.md +0 -175
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
> Source snapshot: official TiSynthEngine 1.0.0
|
|
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
|
-
|
|
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 /
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
63
|
-
|
|
|
64
|
-
| `SINE`
|
|
65
|
-
| `SQUARE`
|
|
66
|
-
| `SAWTOOTH`
|
|
67
|
-
| `TRIANGLE`
|
|
68
|
-
| `NOISE`
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
104
|
-
|
|
|
105
|
-
| `lowpass`
|
|
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`.
|
|
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
|
-
|
|
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
|
|
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
|
|
150
|
-
|
|
|
151
|
-
| Sine
|
|
152
|
-
| Square
|
|
153
|
-
| Sawtooth, triangle, noise and wavetable |
|
|
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 `
|
|
129
|
+
The profile is fixed when `start()` opens the stream.
|
|
163
130
|
|
|
164
|
-
| Profile
|
|
165
|
-
|
|
|
166
|
-
| `conservative` |
|
|
167
|
-
| `balanced`
|
|
168
|
-
| `speakerSafe`
|
|
169
|
-
| `raw`
|
|
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
|
|
184
|
-
| Ceiling
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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.
|