@maccesar/titools 5.2.0 → 5.2.1
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 +69 -0
- package/README.md +80 -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/downloader.js +1 -1
- 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
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
# Instrument, polyphony and expressive recipes
|
|
2
|
+
|
|
3
|
+
These curated TiTools recipes use the same running engine and runtime `WAVE`
|
|
4
|
+
constants as [the arcade recipes](recipes.md). Keep one owner, check native
|
|
5
|
+
acceptance and use their [verification checklist](recipes.md#recipe-verification-checklist).
|
|
6
|
+
The expressive envelope/filter/FM ranges are starting points verified against
|
|
7
|
+
the public contract, not device-auditioned presets. Listen before shipping.
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const synth = require('ti.synthengine')
|
|
11
|
+
const WAVE = synth.getDefaults().waveTypes
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
<!-- TOC-START -->
|
|
15
|
+
## Contents
|
|
16
|
+
|
|
17
|
+
- [Playable xylophone](#playable-xylophone)
|
|
18
|
+
- [Drag interaction without note storms](#drag-interaction-without-note-storms)
|
|
19
|
+
- [Four-pad memory cues](#four-pad-memory-cues)
|
|
20
|
+
- [Controlled timbre comparison](#controlled-timbre-comparison)
|
|
21
|
+
- [Polyphony and layered chords](#polyphony-and-layered-chords)
|
|
22
|
+
- [Convert MIDI pitch to hertz](#convert-midi-pitch-to-hertz)
|
|
23
|
+
- [Build a wide layered chord](#build-a-wide-layered-chord)
|
|
24
|
+
- [Clean reference versus overlap stress](#clean-reference-versus-overlap-stress)
|
|
25
|
+
- [Cancellable repetition](#cancellable-repetition)
|
|
26
|
+
- [Expressive envelopes, filters and FM](#expressive-envelopes-filters-and-fm)
|
|
27
|
+
- [Plucked marimba note](#plucked-marimba-note)
|
|
28
|
+
- [Kick drum](#kick-drum)
|
|
29
|
+
- [Closed hi-hat](#closed-hi-hat)
|
|
30
|
+
- [Electric piano](#electric-piano)
|
|
31
|
+
- [Bell](#bell)
|
|
32
|
+
- [Wah sweep](#wah-sweep)
|
|
33
|
+
- [Thick unison pad](#thick-unison-pad)
|
|
34
|
+
- [Chaining after a sequence](#chaining-after-a-sequence)
|
|
35
|
+
- [Preset architecture for strict contracts](#preset-architecture-for-strict-contracts)
|
|
36
|
+
|
|
37
|
+
<!-- TOC-END -->
|
|
38
|
+
|
|
39
|
+
## Playable xylophone
|
|
40
|
+
|
|
41
|
+
Triangle gives a mallet-like balance of a defined pitch and soft upper
|
|
42
|
+
harmonics. A 2 ms attack makes the strike immediate, while a 180 ms release
|
|
43
|
+
lets adjacent bars overlap musically.
|
|
44
|
+
|
|
45
|
+
```javascript
|
|
46
|
+
const XYLOPHONE = [
|
|
47
|
+
{ label: 'C5', frequency: 523.25 },
|
|
48
|
+
{ label: 'D5', frequency: 587.33 },
|
|
49
|
+
{ label: 'E5', frequency: 659.25 },
|
|
50
|
+
{ label: 'F5', frequency: 698.46 },
|
|
51
|
+
{ label: 'G5', frequency: 783.99 },
|
|
52
|
+
{ label: 'A5', frequency: 880 },
|
|
53
|
+
{ label: 'B5', frequency: 987.77 },
|
|
54
|
+
{ label: 'C6', frequency: 1046.5 }
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
function playXylophoneBar (index, pan) {
|
|
58
|
+
const bar = XYLOPHONE[index]
|
|
59
|
+
if (!bar || !isFinite(pan) || pan < -1 || pan > 1) return false
|
|
60
|
+
|
|
61
|
+
return synth.playTone({
|
|
62
|
+
frequency: bar.frequency,
|
|
63
|
+
duration: 420,
|
|
64
|
+
attack: 2,
|
|
65
|
+
release: 180,
|
|
66
|
+
volume: 0.65,
|
|
67
|
+
waveType: WAVE.TRIANGLE,
|
|
68
|
+
pan
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Map the bars across the stereo field with
|
|
74
|
+
`pan = -0.8 + (1.6 * index / (barCount - 1))`. Do not make pan carry essential
|
|
75
|
+
meaning because the device may have a mono output.
|
|
76
|
+
|
|
77
|
+
### Drag interaction without note storms
|
|
78
|
+
|
|
79
|
+
A `touchmove` event may fire many times while the finger remains on one bar.
|
|
80
|
+
Track the last entered bar and strike only when the index changes:
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
function createBarGesture (playIndex) {
|
|
84
|
+
let tracking = false
|
|
85
|
+
let activeIndex = -1
|
|
86
|
+
|
|
87
|
+
function enter (index) {
|
|
88
|
+
if (!tracking || index < 0 || index === activeIndex) return
|
|
89
|
+
activeIndex = index
|
|
90
|
+
playIndex(index)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
start (index) {
|
|
95
|
+
tracking = true
|
|
96
|
+
activeIndex = -1
|
|
97
|
+
enter(index)
|
|
98
|
+
},
|
|
99
|
+
move: enter,
|
|
100
|
+
finish () {
|
|
101
|
+
tracking = false
|
|
102
|
+
activeIndex = -1
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
In a Titanium view:
|
|
109
|
+
|
|
110
|
+
- compute each bar's hit rectangle after layout with `convertPointToView()`;
|
|
111
|
+
- convert move coordinates into one shared container before hit testing;
|
|
112
|
+
- call `gesture.start(index)` on `touchstart` and `gesture.move(index)` only
|
|
113
|
+
after resolving a valid bar;
|
|
114
|
+
- call `gesture.finish()` on both `touchend` and `touchcancel`;
|
|
115
|
+
- keep visual pressed-state counts separate if multiple touches are supported.
|
|
116
|
+
|
|
117
|
+
The audio release may continue after the finger leaves a bar. That is desired
|
|
118
|
+
for a xylophone. `stopAll()` is a global panic: it also cancels every native
|
|
119
|
+
sequence and invalidates shared effect histories. Use an owner's
|
|
120
|
+
`stopSequence({ sequenceId })` when only that sequence should stop.
|
|
121
|
+
|
|
122
|
+
## Four-pad memory cues
|
|
123
|
+
|
|
124
|
+
Four separated square-wave frequencies remain easy to distinguish at short
|
|
125
|
+
durations. Keep each bank as app metadata, then build a fresh closed options
|
|
126
|
+
object for the module.
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
const PAD_BANKS = {
|
|
130
|
+
low: [415, 310, 252, 209],
|
|
131
|
+
equalTempered: [391.995, 329.628, 261.626, 195.998]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function playMemoryPad (bankName, index) {
|
|
135
|
+
const bank = PAD_BANKS[bankName]
|
|
136
|
+
const frequency = bank && bank[index]
|
|
137
|
+
if (!isFinite(frequency)) return false
|
|
138
|
+
|
|
139
|
+
return synth.playTone({
|
|
140
|
+
frequency: frequency,
|
|
141
|
+
duration: 420,
|
|
142
|
+
attack: 6,
|
|
143
|
+
release: 100,
|
|
144
|
+
volume: 0.62,
|
|
145
|
+
waveType: WAVE.SQUARE
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
To demonstrate the whole pitch bank as one event, use a chord rather than four
|
|
151
|
+
independent full-gain calls:
|
|
152
|
+
|
|
153
|
+
```javascript
|
|
154
|
+
function playMemoryBank (bankName) {
|
|
155
|
+
const bank = PAD_BANKS[bankName]
|
|
156
|
+
if (!bank) return false
|
|
157
|
+
|
|
158
|
+
return synth.playChord({
|
|
159
|
+
frequencies: bank.slice(),
|
|
160
|
+
duration: 480,
|
|
161
|
+
attack: 8,
|
|
162
|
+
release: 120,
|
|
163
|
+
volume: 0.56,
|
|
164
|
+
waveType: WAVE.SQUARE,
|
|
165
|
+
pans: [-0.75, -0.25, 0.25, 0.75]
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Controlled timbre comparison
|
|
171
|
+
|
|
172
|
+
For an honest A/B test, hold pitches, duration, envelope and pan constant.
|
|
173
|
+
Change only waveform and its comparison level. Never pass a profile object
|
|
174
|
+
containing labels or other app metadata directly to `playChord()` because its
|
|
175
|
+
options dictionary is closed.
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
const TIMBRE_PROFILES = {
|
|
179
|
+
square: { waveType: WAVE.SQUARE, volume: 0.52 },
|
|
180
|
+
sine: { waveType: WAVE.SINE, volume: 0.68 },
|
|
181
|
+
triangle: { waveType: WAVE.TRIANGLE, volume: 0.64 },
|
|
182
|
+
sawtooth: { waveType: WAVE.SAWTOOTH, volume: 0.46 }
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function auditionTimbre (profileName) {
|
|
186
|
+
const profile = TIMBRE_PROFILES[profileName]
|
|
187
|
+
if (!profile) return false
|
|
188
|
+
|
|
189
|
+
return synth.playChord({
|
|
190
|
+
frequencies: PAD_BANKS.low.slice(),
|
|
191
|
+
duration: 420,
|
|
192
|
+
attack: 8,
|
|
193
|
+
release: 100,
|
|
194
|
+
volume: profile.volume,
|
|
195
|
+
waveType: profile.waveType,
|
|
196
|
+
pans: [-0.75, -0.25, 0.25, 0.75]
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Polyphony and layered chords
|
|
202
|
+
|
|
203
|
+
### Convert MIDI pitch to hertz
|
|
204
|
+
|
|
205
|
+
The standard A4 = 440 Hz formula makes it easy to build interval recipes
|
|
206
|
+
without embedding a long frequency table.
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
function midiToHz (midi) {
|
|
210
|
+
return 440 * Math.pow(2, (midi - 69) / 12)
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Detuning needs no conversion: `detune` on a tone or sequence root and `detunes` on a chord take cents, one hundredth of a semitone.
|
|
215
|
+
|
|
216
|
+
### Build a wide layered chord
|
|
217
|
+
|
|
218
|
+
The interval cycle `[0, 4, 7, ...]` stacks major-triad chord tones across
|
|
219
|
+
octaves. Later layers repeat those tones with small cent offsets, passed as
|
|
220
|
+
`detunes`, which creates width and beating.
|
|
221
|
+
|
|
222
|
+
```javascript
|
|
223
|
+
const CHORD_INTERVALS = [0, 4, 7, 12, 16, 19, 24, 28, 31, 36]
|
|
224
|
+
const LAYER_DETUNE_CENTS = [0, -8, 8, -12]
|
|
225
|
+
|
|
226
|
+
function normalizeVoiceCount (value) {
|
|
227
|
+
const count = Math.round(Number(value))
|
|
228
|
+
if (!isFinite(count)) return 1
|
|
229
|
+
return Math.max(1, Math.min(32, count))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildLayeredFrequencies (voiceCount) {
|
|
233
|
+
const count = normalizeVoiceCount(voiceCount)
|
|
234
|
+
const frequencies = []
|
|
235
|
+
|
|
236
|
+
for (let index = 0; index < count; index++) {
|
|
237
|
+
const intervalIndex = index % CHORD_INTERVALS.length
|
|
238
|
+
frequencies.push(midiToHz(60.12 + CHORD_INTERVALS[intervalIndex]))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return frequencies
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function buildLayerDetunes (voiceCount) {
|
|
245
|
+
const count = normalizeVoiceCount(voiceCount)
|
|
246
|
+
const detunes = []
|
|
247
|
+
|
|
248
|
+
for (let index = 0; index < count; index++) {
|
|
249
|
+
const layer = Math.floor(index / CHORD_INTERVALS.length)
|
|
250
|
+
detunes.push(LAYER_DETUNE_CENTS[layer % LAYER_DETUNE_CENTS.length])
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return detunes
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function buildSymmetricPans (voiceCount, spread) {
|
|
257
|
+
const count = normalizeVoiceCount(voiceCount)
|
|
258
|
+
const numericSpread = Number(spread)
|
|
259
|
+
const safeSpread = isFinite(numericSpread)
|
|
260
|
+
? Math.max(0, Math.min(1, numericSpread))
|
|
261
|
+
: 0
|
|
262
|
+
const pans = []
|
|
263
|
+
|
|
264
|
+
if (count === 1) return [0]
|
|
265
|
+
|
|
266
|
+
for (let index = 0; index < count; index++) {
|
|
267
|
+
pans.push(-safeSpread + (2 * safeSpread * index / (count - 1)))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return pans
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function playLayeredChord (voiceCount) {
|
|
274
|
+
const count = normalizeVoiceCount(voiceCount)
|
|
275
|
+
|
|
276
|
+
return synth.playChord({
|
|
277
|
+
frequencies: buildLayeredFrequencies(count),
|
|
278
|
+
detunes: buildLayerDetunes(count),
|
|
279
|
+
duration: 800,
|
|
280
|
+
attack: 35,
|
|
281
|
+
release: 220,
|
|
282
|
+
volume: 0.48,
|
|
283
|
+
waveType: WAVE.SINE,
|
|
284
|
+
pans: buildSymmetricPans(count, 0.8)
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
The engine must have `maxVoices` at least as large as the intended simultaneous
|
|
290
|
+
chord. Four to twelve voices are a practical musical starting point. Counts
|
|
291
|
+
near 32 are useful for deliberate capacity tests, not as a default.
|
|
292
|
+
|
|
293
|
+
### Clean reference versus overlap stress
|
|
294
|
+
|
|
295
|
+
A single sustained chord is the clean reference for judging waveform quality.
|
|
296
|
+
If a new 220 ms chord starts every 120 ms, release tails overlap and the summed
|
|
297
|
+
level rises and falls. Limiter pumping is expected in that stress condition;
|
|
298
|
+
it is not proof that a single chord is defective.
|
|
299
|
+
|
|
300
|
+
For a musical pulse, keep the event shorter than its period:
|
|
301
|
+
|
|
302
|
+
```javascript
|
|
303
|
+
let pulseSequenceId = 0
|
|
304
|
+
let pulseRequested = false
|
|
305
|
+
let pulseStopping = false
|
|
306
|
+
function publishChordPulse () {
|
|
307
|
+
pulseSequenceId = synth.playSequence({timeUnit: 'ms', stepDuration: 260, loop: true,
|
|
308
|
+
voice: {attack: 10, release: 60, volume: 0.42, waveType: WAVE.SINE, pan: 0},
|
|
309
|
+
steps: [{frequencies: buildLayeredFrequencies(4), length: 120}]
|
|
310
|
+
})
|
|
311
|
+
if (!pulseSequenceId) throw new Error('Chord pulse rejected')
|
|
312
|
+
}
|
|
313
|
+
function requestPulseStop () {
|
|
314
|
+
if (!pulseSequenceId || pulseStopping) return
|
|
315
|
+
pulseStopping = true
|
|
316
|
+
if (!synth.stopSequence({sequenceId: pulseSequenceId})) throw new Error('Pulse stop rejected')
|
|
317
|
+
}
|
|
318
|
+
function stopChordPulse () {
|
|
319
|
+
pulseRequested = false
|
|
320
|
+
requestPulseStop()
|
|
321
|
+
}
|
|
322
|
+
function startChordPulse () {
|
|
323
|
+
pulseRequested = true
|
|
324
|
+
if (pulseSequenceId) requestPulseStop()
|
|
325
|
+
else publishChordPulse()
|
|
326
|
+
}
|
|
327
|
+
function onPulseEnd (event) {
|
|
328
|
+
if (event.sequenceId !== pulseSequenceId) return
|
|
329
|
+
pulseSequenceId = 0
|
|
330
|
+
pulseStopping = false
|
|
331
|
+
if (pulseRequested) publishChordPulse()
|
|
332
|
+
}
|
|
333
|
+
synth.addEventListener('sequenceend', onPulseEnd)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
This repeated chord uses one centered native sequence. The one-off layered chord above retains its individual stereo pans. `length: 120` plus 60 ms release preserves the 180 ms envelope while attacks remain 260 ms apart.
|
|
337
|
+
|
|
338
|
+
### Cancellable repetition
|
|
339
|
+
|
|
340
|
+
Every repeating sound has an identified native owner. Repeated start requests coalesce until the matching retirement event frees its slot. Stop its sequence ID and cancel pending restarts when its window closes; no JavaScript timer schedules subsequent notes.
|
|
341
|
+
|
|
342
|
+
```javascript
|
|
343
|
+
function cleanupAudio () {
|
|
344
|
+
stopChordPulse()
|
|
345
|
+
synth.removeEventListener('sequenceend', onPulseEnd)
|
|
346
|
+
synth.shutdown()
|
|
347
|
+
}
|
|
348
|
+
win.addEventListener('close', cleanupAudio)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Call `shutdown()` only from the screen or service that owns the shared engine.
|
|
352
|
+
|
|
353
|
+
## Expressive envelopes, filters and FM
|
|
354
|
+
|
|
355
|
+
### Plucked marimba note
|
|
356
|
+
|
|
357
|
+
A short attack, a decay that falls to a low sustain and a release that fits
|
|
358
|
+
give the struck-and-ring shape of a mallet or plucked string. A sustain below 1
|
|
359
|
+
needs a decay, and `attack + decay` must fit inside `duration`.
|
|
360
|
+
|
|
361
|
+
```javascript
|
|
362
|
+
synth.playTone({
|
|
363
|
+
note: 'C5',
|
|
364
|
+
duration: 700,
|
|
365
|
+
attack: 2,
|
|
366
|
+
decay: 260,
|
|
367
|
+
sustain: 0.12,
|
|
368
|
+
release: 240,
|
|
369
|
+
waveType: WAVE.TRIANGLE,
|
|
370
|
+
volume: 0.62
|
|
371
|
+
})
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Kick drum
|
|
375
|
+
|
|
376
|
+
A sine that falls in pitch while its level decays reads as a drum. The pitch
|
|
377
|
+
sweep is linear over the whole event.
|
|
378
|
+
|
|
379
|
+
```javascript
|
|
380
|
+
synth.playTone({
|
|
381
|
+
frequency: 150,
|
|
382
|
+
frequencyEnd: 45,
|
|
383
|
+
duration: 320,
|
|
384
|
+
attack: 1,
|
|
385
|
+
decay: 140,
|
|
386
|
+
sustain: 0.35,
|
|
387
|
+
release: 140,
|
|
388
|
+
volume: 0.9
|
|
389
|
+
})
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Closed hi-hat
|
|
393
|
+
|
|
394
|
+
White noise through a high-pass filter, with a decay that drops to a quiet
|
|
395
|
+
tail. The filter fields only work together with a `filterType`.
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
synth.playTone({
|
|
399
|
+
waveType: WAVE.NOISE,
|
|
400
|
+
duration: 90,
|
|
401
|
+
attack: 1,
|
|
402
|
+
decay: 20,
|
|
403
|
+
sustain: 0.3,
|
|
404
|
+
release: 60,
|
|
405
|
+
filterType: 'highpass',
|
|
406
|
+
filterHz: 6500,
|
|
407
|
+
volume: 0.45
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Electric piano
|
|
412
|
+
|
|
413
|
+
Frequency modulation with a 1:1 ratio gives a tine-like tone. Letting the
|
|
414
|
+
modulation index fall through `fmDepthEnd` makes the note mellow as it rings,
|
|
415
|
+
which is what the ear expects from a struck tine. FM needs a sine carrier.
|
|
416
|
+
|
|
417
|
+
```javascript
|
|
418
|
+
synth.playTone({
|
|
419
|
+
note: 'E3',
|
|
420
|
+
duration: 1400,
|
|
421
|
+
attack: 3,
|
|
422
|
+
decay: 700,
|
|
423
|
+
sustain: 0.25,
|
|
424
|
+
release: 500,
|
|
425
|
+
fmRatio: 1,
|
|
426
|
+
fmDepth: 2.2,
|
|
427
|
+
fmDepthEnd: 0.3,
|
|
428
|
+
volume: 0.55
|
|
429
|
+
})
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Bell
|
|
433
|
+
|
|
434
|
+
A non-integer ratio produces an inharmonic spectrum, and an index that decays
|
|
435
|
+
to nothing leaves a pure ring. `fmDepthEnd: 0` is a real target, so it is
|
|
436
|
+
accepted; omit the key when no sweep is wanted.
|
|
437
|
+
|
|
438
|
+
```javascript
|
|
439
|
+
synth.playTone({
|
|
440
|
+
note: 'A5',
|
|
441
|
+
duration: 2400,
|
|
442
|
+
attack: 1,
|
|
443
|
+
decay: 1800,
|
|
444
|
+
sustain: 0,
|
|
445
|
+
release: 500,
|
|
446
|
+
fmRatio: 3.5,
|
|
447
|
+
fmDepth: 4,
|
|
448
|
+
fmDepthEnd: 0,
|
|
449
|
+
volume: 0.5
|
|
450
|
+
})
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Wah sweep
|
|
454
|
+
|
|
455
|
+
A resonant band-pass opening on a sawtooth. The cutoff moves exponentially, so
|
|
456
|
+
equal distances in pitch take equal time. Keep `filterQ` moderate: high values
|
|
457
|
+
make the resonance peak loud enough to engage the limiter.
|
|
458
|
+
|
|
459
|
+
```javascript
|
|
460
|
+
synth.playTone({
|
|
461
|
+
note: 'A2',
|
|
462
|
+
duration: 900,
|
|
463
|
+
attack: 20,
|
|
464
|
+
release: 200,
|
|
465
|
+
waveType: WAVE.SAWTOOTH,
|
|
466
|
+
filterType: 'bandpass',
|
|
467
|
+
filterHz: 350,
|
|
468
|
+
filterHzEnd: 2200,
|
|
469
|
+
filterQ: 4,
|
|
470
|
+
volume: 0.7
|
|
471
|
+
})
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### Thick unison pad
|
|
475
|
+
|
|
476
|
+
Repeat a pitch and spread the copies a few cents apart. The slow beating
|
|
477
|
+
between them is what makes the sound wide. `detunes` is aligned with the
|
|
478
|
+
pitches and can be shorter than the array.
|
|
479
|
+
|
|
480
|
+
```javascript
|
|
481
|
+
synth.playChord({
|
|
482
|
+
notes: ['A3', 'A3', 'A3', 'E4', 'E4', 'E4'],
|
|
483
|
+
detunes: [-9, 0, 9, -7, 0, 7],
|
|
484
|
+
pans: [-0.6, 0, 0.6, -0.4, 0, 0.4],
|
|
485
|
+
duration: 2400,
|
|
486
|
+
attack: 350,
|
|
487
|
+
release: 900,
|
|
488
|
+
waveType: WAVE.SAWTOOTH,
|
|
489
|
+
volume: 0.45
|
|
490
|
+
})
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Chaining after a sequence
|
|
494
|
+
|
|
495
|
+
`sequenceend` carries the accepted `sequenceId` and a reason of `completed` or `stopped`. Only the completed event for this owner should trigger the outro; a delayed event for another ID must not affect it.
|
|
496
|
+
|
|
497
|
+
```javascript
|
|
498
|
+
let cueSequenceId = 0
|
|
499
|
+
function onSequenceEnd (event) {
|
|
500
|
+
if (event.sequenceId !== cueSequenceId) return
|
|
501
|
+
cueSequenceId = 0
|
|
502
|
+
if (event.reason === 'completed') playOutroCue()
|
|
503
|
+
}
|
|
504
|
+
synth.addEventListener('sequenceend', onSequenceEnd)
|
|
505
|
+
cueSequenceId = synth.playSequence({timeUnit: 'beats', stepDuration: 0.5, loop: false,
|
|
506
|
+
voice: {waveType: WAVE.SQUARE, volume: 0.5},
|
|
507
|
+
steps: [{notes: [72]}, {notes: [76]}, {notes: [79]}, {notes: [84]}]
|
|
508
|
+
})
|
|
509
|
+
if (!cueSequenceId) throw new Error('Cue rejected')
|
|
510
|
+
function cleanupSequenceEvents () {
|
|
511
|
+
if (cueSequenceId) synth.stopSequence({sequenceId: cueSequenceId})
|
|
512
|
+
synth.removeEventListener('sequenceend', onSequenceEnd)
|
|
513
|
+
}
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
## Preset architecture for strict contracts
|
|
517
|
+
|
|
518
|
+
Keep descriptive metadata outside the native options object. Build a new
|
|
519
|
+
closed object at the call boundary so keys such as `label`, `category`, or
|
|
520
|
+
`description` never reach the module.
|
|
521
|
+
|
|
522
|
+
```javascript
|
|
523
|
+
const PRESETS = {
|
|
524
|
+
success: {
|
|
525
|
+
label: 'Success',
|
|
526
|
+
frequency: 760,
|
|
527
|
+
frequencyEnd: 1040,
|
|
528
|
+
waveType: WAVE.TRIANGLE
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function playPreset (name) {
|
|
533
|
+
const preset = PRESETS[name]
|
|
534
|
+
if (!preset) return false
|
|
535
|
+
|
|
536
|
+
return synth.playTone({
|
|
537
|
+
frequency: preset.frequency,
|
|
538
|
+
frequencyEnd: preset.frequencyEnd,
|
|
539
|
+
duration: 150,
|
|
540
|
+
attack: 3,
|
|
541
|
+
release: 90,
|
|
542
|
+
volume: 0.58,
|
|
543
|
+
waveType: preset.waveType
|
|
544
|
+
})
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Avoid `synth.playTone(PRESETS[name])`: the preset contains `label`, which makes
|
|
549
|
+
the native call reject the entire dictionary.
|