@maccesar/titools 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +44 -12
  2. package/agents/ti-pro.md +4 -0
  3. package/commands/ti-audit.md +4 -0
  4. package/lib/config.js +2 -0
  5. package/package.json +1 -1
  6. package/skills/purgetss/SKILL.md +4 -4
  7. package/skills/purgetss/references/EXAMPLES.md +3 -3
  8. package/skills/purgetss/references/adopting-purgetss.md +2 -2
  9. package/skills/purgetss/references/animation-advanced.md +2 -2
  10. package/skills/purgetss/references/arbitrary-values.md +8 -5
  11. package/skills/purgetss/references/class-categories.md +5 -5
  12. package/skills/purgetss/references/class-index-properties.md +6 -6
  13. package/skills/purgetss/references/class-index.md +2 -2
  14. package/skills/purgetss/references/cli-commands.md +2 -2
  15. package/skills/purgetss/references/dynamic-component-creation.md +2 -2
  16. package/skills/purgetss/references/grid-layout.md +1 -1
  17. package/skills/purgetss/references/migration-guide.md +34 -4
  18. package/skills/purgetss/references/multi-density-images.md +6 -6
  19. package/skills/purgetss/references/platform-modifiers.md +21 -0
  20. package/skills/purgetss/references/ui-ux-design.md +1 -1
  21. package/skills/purgetss/references/version-history.md +9 -4
  22. package/skills/ti-admob-unified/SKILL.md +44 -0
  23. package/skills/ti-admob-unified/agents/openai.yaml +4 -0
  24. package/skills/ti-admob-unified/assets/example/app.js +1003 -0
  25. package/skills/ti-admob-unified/assets/example/lib/ads.js +618 -0
  26. package/skills/ti-admob-unified/assets/example/library-usage.js +104 -0
  27. package/skills/ti-admob-unified/references/api-reference.md +592 -0
  28. package/skills/ti-admob-unified/references/banners.md +435 -0
  29. package/skills/ti-admob-unified/references/contributing.md +228 -0
  30. package/skills/ti-admob-unified/references/dependencies.md +85 -0
  31. package/skills/ti-admob-unified/references/fullscreen.md +223 -0
  32. package/skills/ti-admob-unified/references/game-monetization.md +17 -0
  33. package/skills/ti-admob-unified/references/getting-started.md +275 -0
  34. package/skills/ti-admob-unified/references/index.md +30 -0
  35. package/skills/ti-admob-unified/references/integration.md +85 -0
  36. package/skills/ti-admob-unified/references/library.md +189 -0
  37. package/skills/ti-admob-unified/references/mediation.md +132 -0
  38. package/skills/ti-admob-unified/references/migration.md +255 -0
  39. package/skills/ti-admob-unified/references/module-changelog.md +54 -0
  40. package/skills/ti-admob-unified/references/module-license.txt +21 -0
  41. package/skills/ti-admob-unified/references/privacy.md +254 -0
  42. package/skills/ti-admob-unified/references/roadmap.md +146 -0
  43. package/skills/ti-admob-unified/references/source-notes.md +33 -0
  44. package/skills/ti-admob-unified/references/source-snapshot.json +440 -0
  45. package/skills/ti-admob-unified/references/third-party-notices.md +22 -0
  46. package/skills/ti-game/SKILL.md +10 -1
  47. package/skills/ti-game/references/api.md +10 -2
  48. package/skills/ti-game/references/debugging-performance.md +6 -0
  49. package/skills/ti-game/references/examples.md +11 -1
  50. package/skills/ti-game/references/patterns.md +24 -2
  51. package/skills/ti-game/references/physics.md +502 -0
  52. package/skills/ti-game/references/project-setup.md +23 -1
  53. package/skills/ti-game/references/recipes-action.md +46 -14
  54. package/skills/ti-game/references/recipes-adventure-effects.md +12 -1
  55. package/skills/ti-game/references/roadmap.md +2 -2
  56. package/skills/ti-game/references/tilemaps.md +2 -1
  57. package/skills/ti-game-assets/SKILL.md +53 -0
  58. package/skills/ti-game-assets/agents/openai.yaml +4 -0
  59. package/skills/ti-game-assets/references/alpha.md +26 -0
  60. package/skills/ti-game-assets/references/art-direction.md +37 -0
  61. package/skills/ti-game-assets/references/integration.md +49 -0
  62. package/skills/ti-game-assets/references/manifest.example.json +429 -0
  63. package/skills/ti-game-assets/references/manifest.md +39 -0
  64. package/skills/ti-game-assets/references/offline-audio.md +33 -0
  65. package/skills/ti-game-assets/references/prompts.md +33 -0
  66. package/skills/ti-game-assets/references/repeating-textures.md +17 -0
  67. package/skills/ti-game-assets/references/responsible-ads.md +31 -0
  68. package/skills/ti-game-assets/references/sound-assets.md +42 -0
  69. package/skills/ti-game-assets/references/vector-sources.md +21 -0
  70. package/skills/ti-game-assets/scripts/alpha_tools.py +160 -0
  71. package/skills/ti-game-assets/scripts/requirements.txt +1 -0
  72. package/skills/ti-game-assets/scripts/validate_manifest.py +267 -0
  73. package/skills/ti-synthengine/SKILL.md +37 -13
  74. package/skills/ti-synthengine/references/api.md +397 -31
  75. package/skills/ti-synthengine/references/audio-engine.md +235 -14
  76. package/skills/ti-synthengine/references/examples.md +396 -17
  77. package/skills/ti-synthengine/references/guide.md +331 -12
  78. package/skills/ti-synthengine/references/recipes.md +212 -22
  79. package/skills/ti-synthengine/references/recommendations.md +53 -16
  80. package/skills/ti-synthengine/references/sound-design.md +359 -15
  81. package/skills/ti-synthengine/references/source-notes.md +121 -0
  82. package/skills/ti-synthengine/references/source-snapshot.json +105 -0
  83. package/skills/ti-synthengine/references/stats-performance.json +2992 -0
  84. package/skills/ti-synthengine/references/stats-performance.md +38 -0
  85. package/skills/ti-synthengine/references/troubleshooting.md +61 -4
@@ -1,4 +1,4 @@
1
- > Source snapshot: official TiSynthEngine 1.0.0 documentation at commit `6685b81` (2026-08-29).
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`.
2
2
 
3
3
  # Audio engine
4
4
 
@@ -6,6 +6,29 @@ This page explains the native signal path behind the JavaScript API. You do not
6
6
  need it to play a sound, but it helps when choosing a mixing profile, sizing the
7
7
  voice pool or investigating device behavior.
8
8
 
9
+ <!-- TOC-START -->
10
+ ## Contents
11
+
12
+ - [Signal path](#signal-path)
13
+ - [Oscillators](#oscillators)
14
+ - [Envelope](#envelope)
15
+ - [Event gain](#event-gain)
16
+ - [Mixing profiles](#mixing-profiles)
17
+ - [Limiter](#limiter)
18
+ - [Master volume and pan](#master-volume-and-pan)
19
+ - [Voice pool](#voice-pool)
20
+ - [Command queue](#command-queue)
21
+ - [Shared DSP core](#shared-dsp-core)
22
+ - [Android implementation](#android-implementation)
23
+ - [iOS implementation](#ios-implementation)
24
+ - [Latency](#latency)
25
+ - [Route behavior](#route-behavior)
26
+ - [Regression coverage](#regression-coverage)
27
+ - [Binaural front-plane renderer](#binaural-front-plane-renderer)
28
+ - [Engine statistics](#engine-statistics)
29
+
30
+ <!-- TOC-END -->
31
+
9
32
  ## Signal path
10
33
 
11
34
  ```text
@@ -15,7 +38,9 @@ playTone / playChord / playPattern
15
38
  |
16
39
  native command ring buffer
17
40
  |
18
- voice pool: oscillator * envelope * event gain * stereo pan
41
+ voice pool: source (or FM) -> quantizer -> one-pole low-pass
42
+ -> state-variable filter -> envelope -> gain
43
+ -> stereo pan OR binaural FIR
19
44
  |
20
45
  sum voices and steal tails
21
46
  |
@@ -32,7 +57,7 @@ them and renders the samples.
32
57
 
33
58
  ## Oscillators
34
59
 
35
- The engine has five sources:
60
+ The engine has six sources:
36
61
 
37
62
  | Waveform | Source behavior |
38
63
  | --- | --- |
@@ -40,18 +65,68 @@ The engine has five sources:
40
65
  | `SQUARE` | PolyBLEP band-limited discontinuity. |
41
66
  | `SAWTOOTH` | PolyBLEP band-limited discontinuity. |
42
67
  | `TRIANGLE` | Continuous triangle oscillator. |
43
- | `NOISE` | White-noise generator. |
68
+ | `NOISE` | White or variable-rate sample-and-hold pseudo-random generator. |
69
+ | `WAVETABLE` | Caller-supplied 2 to 64-sample stepped cycle. |
44
70
 
45
71
  Pitch can move linearly from `frequency` to `frequencyEnd`. A sine LFO adds
46
72
  `lfoDepth * sin(lfoPhase)` in hertz before the oscillator advances. This makes
47
73
  vibrato depth absolute in hertz rather than cents.
48
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`.
84
+
85
+ ### Frequency modulation
86
+
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.
93
+
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.
96
+
97
+ ### State-variable filter
98
+
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.
102
+
103
+ | Output | Response |
104
+ | --- | --- |
105
+ | `lowpass` | 12 dB per octave, peak of about Q at the cutoff. |
106
+ | `highpass` | 12 dB per octave, peak of about Q at the cutoff. |
107
+ | `bandpass` | Unity gain at the center for every Q. |
108
+
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.
113
+
49
114
  ## Envelope
50
115
 
51
- Each voice has a linear attack, a full-level middle section and a linear
52
- release. `duration` includes the whole envelope. Defaults are adjusted to fit a
53
- short event, while an explicit envelope that cannot fit is rejected by the
54
- contract.
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.
120
+
121
+ ```text
122
+ base = attack ramp, then decay ramp from 1 to sustain, then sustain
123
+ level = base * release ramp
124
+ ```
125
+
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.
55
130
 
56
131
  Voice stealing does not cut the old waveform at an arbitrary sample. The old
57
132
  voice contributes a short tail of roughly 6 ms while the slot begins the new
@@ -75,7 +150,7 @@ Waveforms are calibrated toward a common source RMS:
75
150
  | --- | ---: | ---: |
76
151
  | Sine | 0.440 | 0.311 |
77
152
  | Square | 0.311 | 0.311 |
78
- | Sawtooth, triangle and noise | 0.539 | 0.311 |
153
+ | Sawtooth, triangle, noise and wavetable | 0.539 | signal-dependent |
79
154
 
80
155
  This calculation is why one three-note `playChord` and three `playTone` calls
81
156
  are not equivalent. A protected sine triad assigns about 0.254 to each chord
@@ -149,7 +224,18 @@ a control fallback so cleanup is not lost under queue pressure.
149
224
 
150
225
  Pattern timing runs outside the audio callback. A platform timer places each
151
226
  step on the command queue. The sequence is suitable for game cues and simple
152
- musical patterns, but it is not a sample-accurate composition timeline.
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.
230
+
231
+ ## Shared DSP core
232
+
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.
153
239
 
154
240
  ## Android implementation
155
241
 
@@ -165,11 +251,12 @@ ABIs are `arm64-v8a`, `armeabi-v7a`, `x86` and `x86_64`.
165
251
 
166
252
  iOS renders with `AVAudioEngine` and `AVAudioSourceNode`, using the active
167
253
  session's sample rate. The module configures the audio session as `Playback`,
168
- activates it, and observes route and engine configuration notifications. If a
169
- route change stops the graph, the port reactivates the session and resumes the
170
- existing renderer.
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.
171
258
 
172
- `AVAudioSourceNode` requires iOS 13 or newer. The module archive contains device,
259
+ The module targets iOS 15 or newer. The module archive contains device,
173
260
  simulator and Mac Catalyst slices.
174
261
 
175
262
  ## Latency
@@ -199,15 +286,18 @@ device:
199
286
  ```sh
200
287
  node tests/api_contract_regression.js
201
288
  node tests/default_profile_regression.js
289
+ node tests/documentation_regression.js
202
290
 
203
291
  cd android
204
292
  test_dir=$(mktemp -d /tmp/ti-synth-test.XXXXXX)
205
293
  javac -d "$test_dir" \
206
294
  src/ti/synthengine/SynthContract.java \
207
295
  test/ti/synthengine/SynthContractRegression.java \
296
+ test/ti/synthengine/SynthSpatialContractRegression.java \
208
297
  src/ti/synthengine/SynthDsp.java \
209
298
  test/ti/synthengine/SynthDspRegression.java
210
299
  java -cp "$test_dir" ti.synthengine.SynthContractRegression
300
+ java -cp "$test_dir" ti.synthengine.SynthSpatialContractRegression
211
301
  java -cp "$test_dir" ti.synthengine.SynthDspRegression
212
302
  ```
213
303
 
@@ -215,3 +305,134 @@ The DSP suite covers stable RMS from 1 through 32 voices, limiter transparency
215
305
  and recovery, waveform discontinuities, envelope endpoints, master smoothing,
216
306
  voice stealing and representative dense chords. Device tests remain necessary
217
307
  for route changes, startup underruns and speaker acoustics.
308
+
309
+ Three more checks cover the shared core, the Android engine and the iOS contract.
310
+
311
+ The shared DSP core builds with any C99 compiler:
312
+
313
+ ```sh
314
+ test_dir=$(mktemp -d /tmp/ti-synth-core.XXXXXX)
315
+ cc -std=c99 -O2 -Wall -Wextra -o "$test_dir/dsp_core" \
316
+ tests/dsp_core_regression.c -lm
317
+ "$test_dir/dsp_core"
318
+ ```
319
+
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.
326
+
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:
332
+
333
+ ```sh
334
+ OBOE=$(find ~/.gradle -path '*oboe-1.10.0/prefab/modules/oboe/include' | head -1)
335
+ JH=$(/usr/libexec/java_home)
336
+ test_dir=$(mktemp -d /tmp/ti-synth-engine.XXXXXX)
337
+ clang++ -std=c++17 -O1 -Wall -I"$OBOE" -I"$JH/include" -I"$JH/include/darwin" \
338
+ -DHAS_EXPRESSIVE -DENGINE_CPP='"../android/jni/SynthOboeEngine.cpp"' \
339
+ -Wl,-undefined,dynamic_lookup tests/engine_render_host.cpp \
340
+ -o "$test_dir/engine_render"
341
+ "$test_dir/engine_render"
342
+ ```
343
+
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.
350
+
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:
356
+
357
+ ```sh
358
+ test_dir=$(mktemp -d /tmp/ti-synth-ios.XXXXXX)
359
+ clang -fobjc-arc -Wall -Wextra -framework Foundation \
360
+ tests/contract_corpus_ios.m ios/Classes/TiSynthengineContract.m \
361
+ -o "$test_dir/contract_corpus_ios"
362
+ "$test_dir/contract_corpus_ios"
363
+ ```
364
+
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.
367
+
368
+ ## Binaural front-plane renderer
369
+
370
+ `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
+
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.
373
+
374
+ 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
+
376
+ ### Reproduce a host CPU comparison
377
+
378
+ Compile `tests/spatial_benchmark_host.cpp` with the same Oboe/JDK includes and definitions as `engine_render_host.cpp` above, replacing the source filename and choosing an output such as `/tmp/spatial_benchmark`. Supply a whitespace-separated timeline, one event per line, with these 25 columns in order:
379
+
380
+ ```text
381
+ timeMs frequency frequencyEnd durationMs attackMs releaseMs decayMs sustain waveType volume noiseRate noiseRateEnd lowPassHz pulseWidth lfoFreq lfoDepth filterTypeIndex filterHz filterHzEnd filterQ fmRatio fmDepth fmDepthEnd pan panEnd
382
+ ```
383
+
384
+ The filter indices are 0=none, 1=lowpass, 2=highpass and 3=bandpass. This internal harness bypasses public contract validation, so only feed valid event recipes. In binaural mode every event uses its `pan` column as spatialX and a synthetic moving elevation `0.8 * sin(timeMs / 1000)`; the other pan column is irrelevant to that mode. This tests CPU, not the localization of the original timeline.
385
+
386
+ ```sh
387
+ python3 tools/benchmark_spatial.py /tmp/spatial_benchmark /path/to/timeline.txt
388
+ ```
389
+
390
+ The script compares stereo and binaural using a warmup and five measured trials, reporting the median by audio processing time. There is no hardware stream or JavaScript bridge. Results include peak level, non-finite/clipped frames, voice replacements and processing time; they do not measure device CPU, latency, underruns, heat or battery.
391
+
392
+ ## Engine statistics
393
+
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.
414
+
415
+ Run portable telemetry and Java API tests with:
416
+
417
+ ```sh
418
+ c++ -std=c++17 -O2 -Wall -Wextra -pthread tests/stats_regression.cpp -o /tmp/stats
419
+ /tmp/stats
420
+ python3 tests/run_stats_java.py
421
+ ```
422
+
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.