@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
@@ -17,6 +17,10 @@ Treat levels as starting points, not mastering targets. Test on the actual
17
17
  phone speaker and leave headroom when release tails or independent events can
18
18
  overlap.
19
19
 
20
+ The recipes under "Expressive envelopes, filters and FM" pass the real
21
+ contract, but they come from standard synthesis ranges and have not been
22
+ auditioned on a device. Listen before shipping them.
23
+
20
24
  <!-- TOC-START -->
21
25
  ## Contents
22
26
 
@@ -24,6 +28,7 @@ overlap.
24
28
  - [Playable xylophone](#playable-xylophone)
25
29
  - [Four-pad memory cues](#four-pad-memory-cues)
26
30
  - [Polyphony and layered chords](#polyphony-and-layered-chords)
31
+ - [Expressive envelopes, filters and FM](#expressive-envelopes-filters-and-fm)
27
32
  - [Preset architecture for strict contracts](#preset-architecture-for-strict-contracts)
28
33
  - [Recipe verification checklist](#recipe-verification-checklist)
29
34
 
@@ -105,19 +110,21 @@ were chosen for it.
105
110
 
106
111
  ### Three-note bonus cue
107
112
 
108
- Short ascending square notes read as collection or reward. `stepDuration`
109
- controls spacing; each step's `duration` controls its gate.
113
+ Short ascending square notes read as collection or reward. A step's `duration`
114
+ sets both its voice length and the time before the next step; it overrides
115
+ `stepDuration` and `noteValue`. Explicit rests separate 95 ms notes by 110 ms.
110
116
 
111
117
  ```javascript
112
118
  synth.playPattern({
113
- stepDuration: 110,
114
119
  attack: 2,
115
120
  release: 30,
116
121
  volume: 0.55,
117
122
  waveType: WAVE.SQUARE,
118
123
  steps: [
119
124
  { frequency: 523.25, duration: 95 },
125
+ { rest: true, duration: 15 },
120
126
  { frequency: 587.33, duration: 95 },
127
+ { rest: true, duration: 15 },
121
128
  { frequency: 659.25, duration: 130 }
122
129
  ]
123
130
  })
@@ -126,19 +133,22 @@ synth.playPattern({
126
133
  ### Fast four-note arpeggio
127
134
 
128
135
  Very short square notes with a small attack retain the hard cartridge-era
129
- edge while reducing uncontrolled clicks.
136
+ edge while reducing uncontrolled clicks. The 10 ms rests keep the first three
137
+ 40 ms notes on a 50 ms grid; the final note rings for 80 ms.
130
138
 
131
139
  ```javascript
132
140
  synth.playPattern({
133
- stepDuration: 50,
134
141
  attack: 5,
135
142
  release: 15,
136
143
  volume: 0.5,
137
144
  waveType: WAVE.SQUARE,
138
145
  steps: [
139
146
  { frequency: 261.63, duration: 40 },
147
+ { rest: true, duration: 10 },
140
148
  { frequency: 329.63, duration: 40 },
149
+ { rest: true, duration: 10 },
141
150
  { frequency: 392, duration: 40 },
151
+ { rest: true, duration: 10 },
142
152
  { frequency: 523.25, duration: 80 }
143
153
  ]
144
154
  })
@@ -147,8 +157,8 @@ synth.playPattern({
147
157
  ### Rapid score ticks
148
158
 
149
159
  Use one native pattern instead of ten JavaScript timers. Explicit attack and
150
- release values make every 60 ms step valid without relying on shortened
151
- defaults.
160
+ release values make every 60 ms tone valid without relying on shortened
161
+ defaults. A 10 ms rest after each tone places the ticks 70 ms apart.
152
162
 
153
163
  ```javascript
154
164
  var scoreSteps = []
@@ -160,10 +170,10 @@ for (var index = 0; index < 10; index++) {
160
170
  attack: 2,
161
171
  release: 20
162
172
  })
173
+ scoreSteps.push({ rest: true, duration: 10 })
163
174
  }
164
175
 
165
176
  synth.playPattern({
166
- stepDuration: 70,
167
177
  volume: 0.42,
168
178
  waveType: WAVE.SQUARE,
169
179
  steps: scoreSteps
@@ -194,19 +204,20 @@ synth.playTone({
194
204
  ### Alternating warning alarm
195
205
 
196
206
  An octave-like high/low alternation is easy to identify even through a small
197
- speaker. The envelope fits exactly inside every 120 ms tone:
198
- `10 + 100 <= 120`.
207
+ speaker. Each 120 ms tone has a fitted envelope (`10 + 100 <= 120`), followed
208
+ by a 10 ms rest, so the pitch changes every 130 ms.
199
209
 
200
210
  ```javascript
201
211
  var warningSteps = []
202
212
 
203
213
  for (var pulse = 0; pulse < 5; pulse++) {
204
214
  warningSteps.push({ frequency: 800, duration: 120 })
215
+ warningSteps.push({ rest: true, duration: 10 })
205
216
  warningSteps.push({ frequency: 400, duration: 120 })
217
+ warningSteps.push({ rest: true, duration: 10 })
206
218
  }
207
219
 
208
220
  synth.playPattern({
209
- stepDuration: 130,
210
221
  attack: 10,
211
222
  release: 100,
212
223
  volume: 0.45,
@@ -412,8 +423,8 @@ function auditionTimbre (profileName) {
412
423
 
413
424
  ### Convert MIDI pitch to hertz
414
425
 
415
- The standard A4 = 440 Hz formula makes it easy to build interval recipes and
416
- small detunings without embedding a long frequency table.
426
+ The standard A4 = 440 Hz formula makes it easy to build interval recipes
427
+ without embedding a long frequency table.
417
428
 
418
429
  ```javascript
419
430
  function midiToHz (midi) {
@@ -421,14 +432,14 @@ function midiToHz (midi) {
421
432
  }
422
433
  ```
423
434
 
424
- A cent is one hundredth of a semitone, so add `cents / 100` to the MIDI value
425
- before conversion.
435
+ Detuning needs no conversion: `detune` on a tone or pattern and `detunes` on a
436
+ chord take cents, one hundredth of a semitone.
426
437
 
427
438
  ### Build a wide layered chord
428
439
 
429
440
  The interval cycle `[0, 4, 7, ...]` stacks major-triad chord tones across
430
- octaves. Later layers repeat those tones with small cent offsets, which creates
431
- width and beating without requiring an unsupported detune option.
441
+ octaves. Later layers repeat those tones with small cent offsets, passed as
442
+ `detunes`, which creates width and beating.
432
443
 
433
444
  ```javascript
434
445
  var CHORD_INTERVALS = [0, 4, 7, 12, 16, 19, 24, 28, 31, 36]
@@ -446,15 +457,24 @@ function buildLayeredFrequencies (voiceCount) {
446
457
 
447
458
  for (var index = 0; index < count; index++) {
448
459
  var intervalIndex = index % CHORD_INTERVALS.length
449
- var layer = Math.floor(index / CHORD_INTERVALS.length)
450
- var cents = LAYER_DETUNE_CENTS[layer % LAYER_DETUNE_CENTS.length]
451
- var midi = 60.12 + CHORD_INTERVALS[intervalIndex] + (cents / 100)
452
- frequencies.push(midiToHz(midi))
460
+ frequencies.push(midiToHz(60.12 + CHORD_INTERVALS[intervalIndex]))
453
461
  }
454
462
 
455
463
  return frequencies
456
464
  }
457
465
 
466
+ function buildLayerDetunes (voiceCount) {
467
+ var count = normalizeVoiceCount(voiceCount)
468
+ var detunes = []
469
+
470
+ for (var index = 0; index < count; index++) {
471
+ var layer = Math.floor(index / CHORD_INTERVALS.length)
472
+ detunes.push(LAYER_DETUNE_CENTS[layer % LAYER_DETUNE_CENTS.length])
473
+ }
474
+
475
+ return detunes
476
+ }
477
+
458
478
  function buildSymmetricPans (voiceCount, spread) {
459
479
  var count = normalizeVoiceCount(voiceCount)
460
480
  var numericSpread = Number(spread)
@@ -477,6 +497,7 @@ function playLayeredChord (voiceCount) {
477
497
 
478
498
  return synth.playChord({
479
499
  frequencies: buildLayeredFrequencies(count),
500
+ detunes: buildLayerDetunes(count),
480
501
  duration: 800,
481
502
  attack: 35,
482
503
  release: 220,
@@ -558,6 +579,171 @@ win.addEventListener('close', cleanupAudio)
558
579
  Do not call `shutdown()` from a child control if another visible screen owns
559
580
  the shared engine.
560
581
 
582
+ ## Expressive envelopes, filters and FM
583
+
584
+ ### Plucked marimba note
585
+
586
+ A short attack, a decay that falls to a low sustain and a release that fits
587
+ give the struck-and-ring shape of a mallet or plucked string. A sustain below 1
588
+ needs a decay, and `attack + decay` must fit inside `duration`.
589
+
590
+ ```javascript
591
+ synth.playTone({
592
+ note: 'C5',
593
+ duration: 700,
594
+ attack: 2,
595
+ decay: 260,
596
+ sustain: 0.12,
597
+ release: 240,
598
+ waveType: WAVE.TRIANGLE,
599
+ volume: 0.62
600
+ })
601
+ ```
602
+
603
+ ### Kick drum
604
+
605
+ A sine that falls in pitch while its level decays reads as a drum. The pitch
606
+ sweep is linear over the whole event.
607
+
608
+ ```javascript
609
+ synth.playTone({
610
+ frequency: 150,
611
+ frequencyEnd: 45,
612
+ duration: 320,
613
+ attack: 1,
614
+ decay: 140,
615
+ sustain: 0.35,
616
+ release: 140,
617
+ volume: 0.9
618
+ })
619
+ ```
620
+
621
+ ### Closed hi-hat
622
+
623
+ White noise through a high-pass filter, with a decay that drops to a quiet
624
+ tail. The filter fields only work together with a `filterType`.
625
+
626
+ ```javascript
627
+ synth.playTone({
628
+ waveType: WAVE.NOISE,
629
+ duration: 90,
630
+ attack: 1,
631
+ decay: 20,
632
+ sustain: 0.3,
633
+ release: 60,
634
+ filterType: 'highpass',
635
+ filterHz: 6500,
636
+ volume: 0.45
637
+ })
638
+ ```
639
+
640
+ ### Electric piano
641
+
642
+ Frequency modulation with a 1:1 ratio gives a tine-like tone. Letting the
643
+ modulation index fall through `fmDepthEnd` makes the note mellow as it rings,
644
+ which is what the ear expects from a struck tine. FM needs a sine carrier.
645
+
646
+ ```javascript
647
+ synth.playTone({
648
+ note: 'E3',
649
+ duration: 1400,
650
+ attack: 3,
651
+ decay: 700,
652
+ sustain: 0.25,
653
+ release: 500,
654
+ fmRatio: 1,
655
+ fmDepth: 2.2,
656
+ fmDepthEnd: 0.3,
657
+ volume: 0.55
658
+ })
659
+ ```
660
+
661
+ ### Bell
662
+
663
+ A non-integer ratio produces an inharmonic spectrum, and an index that decays
664
+ to nothing leaves a pure ring. `fmDepthEnd: 0` is a real target, so it is
665
+ accepted; omit the key when no sweep is wanted.
666
+
667
+ ```javascript
668
+ synth.playTone({
669
+ note: 'A5',
670
+ duration: 2400,
671
+ attack: 1,
672
+ decay: 1800,
673
+ sustain: 0,
674
+ release: 500,
675
+ fmRatio: 3.5,
676
+ fmDepth: 4,
677
+ fmDepthEnd: 0,
678
+ volume: 0.5
679
+ })
680
+ ```
681
+
682
+ ### Wah sweep
683
+
684
+ A resonant band-pass opening on a sawtooth. The cutoff moves exponentially, so
685
+ equal distances in pitch take equal time. Keep `filterQ` moderate: high values
686
+ make the resonance peak loud enough to engage the limiter.
687
+
688
+ ```javascript
689
+ synth.playTone({
690
+ note: 'A2',
691
+ duration: 900,
692
+ attack: 20,
693
+ release: 200,
694
+ waveType: WAVE.SAWTOOTH,
695
+ filterType: 'bandpass',
696
+ filterHz: 350,
697
+ filterHzEnd: 2200,
698
+ filterQ: 4,
699
+ volume: 0.7
700
+ })
701
+ ```
702
+
703
+ ### Thick unison pad
704
+
705
+ Repeat a pitch and spread the copies a few cents apart. The slow beating
706
+ between them is what makes the sound wide. `detunes` is aligned with the
707
+ pitches and can be shorter than the array.
708
+
709
+ ```javascript
710
+ synth.playChord({
711
+ notes: ['A3', 'A3', 'A3', 'E4', 'E4', 'E4'],
712
+ detunes: [-9, 0, 9, -7, 0, 7],
713
+ pans: [-0.6, 0, 0.6, -0.4, 0, 0.4],
714
+ duration: 2400,
715
+ attack: 350,
716
+ release: 900,
717
+ waveType: WAVE.SAWTOOTH,
718
+ volume: 0.45
719
+ })
720
+ ```
721
+
722
+ ### Chaining after a pattern
723
+
724
+ `patternend` fires once for every accepted pattern. `completed` is true when it
725
+ ran to its end and false when a newer pattern replaced it, so only the first
726
+ case should start the next cue. `stopAll()` and `shutdown()` fire nothing.
727
+
728
+ ```javascript
729
+ function onPatternEnd (event) {
730
+ if (event.completed) playOutroCue()
731
+ }
732
+
733
+ synth.addEventListener('patternend', onPatternEnd)
734
+
735
+ synth.playPattern({
736
+ noteValue: '8n',
737
+ waveType: WAVE.SQUARE,
738
+ volume: 0.5,
739
+ steps: [{ note: 'C5' }, { note: 'E5' }, { note: 'G5' }, { note: 'C6' }]
740
+ })
741
+
742
+ function cleanupPatternEvents () {
743
+ synth.removeEventListener('patternend', onPatternEnd)
744
+ }
745
+ ```
746
+
561
747
  ## Preset architecture for strict contracts
562
748
 
563
749
  Keep descriptive metadata outside the native options object. Build a new
@@ -600,8 +786,12 @@ Before returning or shipping a variation:
600
786
  - use exactly one pitch source for a tone and exactly one pitch array for a
601
787
  chord;
602
788
  - include a pitch field on every sounding pattern step, including noise;
789
+ - treat each resolved step duration as both its voice length and timeline
790
+ advance; insert explicit rests when a shorter note needs a longer interval;
603
791
  - verify `attack < duration` and `release <= duration - attack` for every
604
- explicit event or step;
792
+ explicit event or step, and `attack + decay <= duration` when a decay is set;
793
+ - give `sustain` below 1 a `decay`, give filter fields a `filterType`, and give
794
+ FM a sine carrier and a depth;
605
795
  - keep all numbers finite and integer-only fields integral;
606
796
  - use `WAVE` constants, not copied numeric waveform values;
607
797
  - check Boolean results at important control boundaries;
@@ -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/recommendations.md` SHA-256: `0f09ee6ef0876b3a23021fddb509dfd1e1206d08e836acae8f32b58b6481c055`.
2
2
 
3
3
  # Recommendations
4
4
 
@@ -9,7 +9,8 @@ audio systems sharing one lifecycle.
9
9
  ## Group simultaneous notes
10
10
 
11
11
  Use `playChord()` when pitches form one known simultaneous event. The chord is
12
- atomic and its source gain accounts for its voice count. Separate `playTone()`
12
+ atomic. Protected profiles normalize per-voice source gain by the square root
13
+ of the event voice count; `raw` bypasses that normalization. Separate `playTone()`
13
14
  calls are appropriate for independent touches and effects, but their start
14
15
  times and gain are independent too.
15
16
 
@@ -65,28 +66,47 @@ play later sounds normally once the system has moved to the new route.
65
66
  ## Coexisting with other audio
66
67
 
67
68
  On iOS, the audio session is app-wide and the last component to configure it
68
- wins. TiSynthEngine selects `Playback`, which ignores the ring/silent switch and
69
- normally interrupts other apps. That behavior fits an instrument or an app
70
- whose sound must be heard.
69
+ wins. By default TiSynthEngine selects `Playback`, which ignores the ring/silent
70
+ switch and normally interrupts other apps. That behavior fits an instrument or
71
+ an app whose sound must be heard.
71
72
 
72
- A game that should respect the silent switch may set Titanium's ambient
73
- category after the last engine startup:
73
+ A game that should respect the silent switch and let other apps' music keep
74
+ playing starts the engine with the ambient session:
74
75
 
75
76
  ```javascript
76
- if (Ti.Platform.osname !== 'android') {
77
- Ti.Media.audioSessionCategory = Ti.Media.AUDIO_SESSION_CATEGORY_AMBIENT
78
- }
77
+ synth.startEngine({ audioSession: 'ambient' })
79
78
  ```
80
79
 
81
- The order is important because `startEngine()` configures the session. Other
82
- native audio modules may configure it again. Test the final startup sequence,
83
- interruptions, background behavior and route changes on iOS before relying on
84
- an override.
80
+ The engine applies the category at startup and again when it resumes after a
81
+ route change. Other native audio modules may still configure the app-wide
82
+ session later. Test the final startup sequence, interruptions, background
83
+ behavior and route changes on iOS. Android accepts the option and ignores it.
85
84
 
86
85
  Android mixes independent output streams at the system level, but another app
87
86
  or stale test process can still consume low-latency resources. Force-stop old
88
87
  test builds when diagnosing a glitch that does not reproduce after reboot.
89
88
 
89
+ ## Keep resonance, FM and detune in check
90
+
91
+ The resonant filter, FM and stacked detuned voices change level and spectrum
92
+ more than the basic controls do.
93
+
94
+ - A `lowpass` or `highpass` boosts the cutoff region by about `filterQ`, so Q
95
+ 10 is roughly 20 dB. Raise Q in small steps and lower the event `volume` as
96
+ you go. `bandpass` keeps unity gain at its center when level must stay put.
97
+ - A swept cutoff passes over every harmonic, so the level moves during the
98
+ sweep. Judge a wah or laser by the loudest moment, not the start.
99
+ - A resonance that keeps reaching the limiter makes the whole mix pump. Lower
100
+ the volume or the Q before reaching for a different profile.
101
+ - FM is not band-limited. A high carrier with a large `fmDepth` and `fmRatio`
102
+ aliases. Keep carriers of bell-like sounds around 500 Hz, or lower the index.
103
+ - A detuned stack is one voice per copy. Count them when choosing `maxVoices`.
104
+ Protected profiles divide per-voice source gain by the square root of the
105
+ event voice count; `raw` does not. Correlated or phase-aligned voices can still
106
+ reinforce each other, so this is not a guarantee of constant loudness or peaks.
107
+ - `patternend` follows the pattern scheduler. Use it to chain cues and release
108
+ state, not to line up audio to the sample.
109
+
90
110
  ## Validate values at the app boundary
91
111
 
92
112
  The native API rejects invalid values instead of clamping them. Use
@@ -106,6 +126,12 @@ function acceptedFrequency (raw) {
106
126
  Do not copy the frequency maximum into application code. It depends on the
107
127
  active sample rate and should be read again after startup.
108
128
 
129
+ The same rule applies to `noiseRate`, `lowPassHz` and `filterHz`. Validate a
130
+ custom `waveTable` as 2 to 64 finite values from -1 to 1, and remember that
131
+ `bitDepth` accepts zero or an integer from 2 to 16. For `detune`, check the
132
+ resulting pitch too: a shifted pitch that leaves the frequency range is
133
+ rejected even when the cents value is in range.
134
+
109
135
  Check Boolean return values during development. A rejected call includes the
110
136
  method, key and reason in the native log. Logging that result at the caller is
111
137
  often enough to identify a stale preset or misspelled field.
@@ -134,6 +160,10 @@ Exercise the real worst case:
134
160
  An underrun counter only describes transport. A clean counter cannot tell you
135
161
  whether a waveform, pitch or mix is pleasant.
136
162
 
163
+ ## Binaural placement
164
+
165
+ Use the [integration guide](guide.md#integrate-binaural-in-a-2d-app) to convert each effect source into signed spatial coordinates. Keep switch and amplitude percentages in the app, enable the controls only when both spatial limits are present, and test with headphones. Count concurrently spatial voices and replacement tails when measuring CPU.
166
+
137
167
  ## Known limits
138
168
 
139
169
  - There are no per-voice handles. A single active note cannot be edited or
@@ -141,10 +171,17 @@ whether a waveform, pitch or mix is pleasant.
141
171
  - Every event has a finite duration of at most 60 seconds. There is no looping.
142
172
  - The module synthesizes waveforms and does not play audio files.
143
173
  - Only one pattern scheduler is pending at a time.
144
- - There is no completion event for a note, chord or pattern.
145
- - The module emits no JavaScript events.
174
+ - There is no completion event for a single note or chord. Patterns report
175
+ their end through `patternend`, which is the only event the module emits.
146
176
  - Voice capacity and mixing profile remain fixed until shutdown.
147
177
  - Pattern scheduling is timer-based, not sample-accurate.
178
+ - `lowPassHz` is a plain one-pole filter. The resonant filter has fixed 12 dB
179
+ per octave and one exponential sweep; there is no filter envelope.
180
+ - FM has one modulator and works only on `SINE`.
181
+ - Binaural positions are fixed per event on a front plane; there is no per-voice movement, rear positioning, distance attenuation or head tracking. Output is two channels, without a multichannel 2.1/5.1 routing API.
182
+ - There is no effects bus: no reverb, delay or chorus.
183
+ - The module provides generic synthesis primitives, not cycle-accurate hardware
184
+ emulation.
148
185
 
149
186
  Use a sample player or a dedicated music engine when a product needs streaming,
150
187
  loop points, per-voice control or a synchronized composition timeline.