@maccesar/titools 4.16.2 → 4.17.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.
- package/AGENTS-VERCEL-RESEARCH.md +10 -6
- package/README.md +58 -31
- package/agents/ti-pro.md +16 -3
- package/commands/ti-audit.md +21 -6
- package/commands/ti-check.md +2 -1
- package/lib/config.js +1 -0
- package/lib/downloader.js +1 -0
- package/package.json +6 -3
- package/skills/ti-synthengine/SKILL.md +92 -0
- package/skills/ti-synthengine/references/api.md +236 -0
- package/skills/ti-synthengine/references/audio-engine.md +217 -0
- package/skills/ti-synthengine/references/examples.md +348 -0
- package/skills/ti-synthengine/references/guide.md +252 -0
- package/skills/ti-synthengine/references/recipes.md +610 -0
- package/skills/ti-synthengine/references/recommendations.md +150 -0
- package/skills/ti-synthengine/references/sound-design.md +428 -0
- package/skills/ti-synthengine/references/troubleshooting.md +162 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
> Source snapshot: official TiSynthEngine 1.0.0 documentation at commit `6685b81` (2026-08-29).
|
|
2
|
+
|
|
3
|
+
# Examples
|
|
4
|
+
|
|
5
|
+
Every snippet on this page uses the TiSynthEngine 1.0.0 contract. Unless a
|
|
6
|
+
snippet includes startup code, it assumes the engine is already running.
|
|
7
|
+
|
|
8
|
+
```javascript
|
|
9
|
+
var synth = require('ti.synthengine')
|
|
10
|
+
var contract = synth.getDefaults()
|
|
11
|
+
var WAVE = contract.waveTypes
|
|
12
|
+
|
|
13
|
+
if (contract.apiVersion !== '1.0.0') {
|
|
14
|
+
throw new Error('TiSynthEngine 1.0.0 is required')
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
<!-- TOC-START -->
|
|
19
|
+
## Contents
|
|
20
|
+
|
|
21
|
+
- [Start once after layout](#start-once-after-layout)
|
|
22
|
+
- [Five waveforms](#five-waveforms)
|
|
23
|
+
- [Pitch sweep and pan movement](#pitch-sweep-and-pan-movement)
|
|
24
|
+
- [Vibrato](#vibrato)
|
|
25
|
+
- [Chord with an even spread](#chord-with-an-even-spread)
|
|
26
|
+
- [Chord with explicit pans](#chord-with-explicit-pans)
|
|
27
|
+
- [Tempo, note values and swing](#tempo-note-values-and-swing)
|
|
28
|
+
- [Millisecond pattern](#millisecond-pattern)
|
|
29
|
+
- [Step overrides](#step-overrides)
|
|
30
|
+
- [Noise percussion](#noise-percussion)
|
|
31
|
+
- [Xylophone sketch](#xylophone-sketch)
|
|
32
|
+
- [Simon-style pads](#simon-style-pads)
|
|
33
|
+
- [Speaker-safe startup](#speaker-safe-startup)
|
|
34
|
+
- [Validation at an input boundary](#validation-at-an-input-boundary)
|
|
35
|
+
- [Stop and clean up](#stop-and-clean-up)
|
|
36
|
+
|
|
37
|
+
<!-- TOC-END -->
|
|
38
|
+
|
|
39
|
+
## Start once after layout
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
var started = false
|
|
43
|
+
var timer = null
|
|
44
|
+
|
|
45
|
+
function onPostLayout () {
|
|
46
|
+
if (timer !== null) clearTimeout(timer)
|
|
47
|
+
timer = setTimeout(function () {
|
|
48
|
+
timer = null
|
|
49
|
+
win.removeEventListener('postlayout', onPostLayout)
|
|
50
|
+
started = synth.startEngine({
|
|
51
|
+
maxVoices: 16,
|
|
52
|
+
mixingProfile: 'conservative'
|
|
53
|
+
})
|
|
54
|
+
}, 250)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cleanup () {
|
|
58
|
+
win.removeEventListener('postlayout', onPostLayout)
|
|
59
|
+
if (timer !== null) clearTimeout(timer)
|
|
60
|
+
if (started) synth.shutdown()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
win.addEventListener('postlayout', onPostLayout)
|
|
64
|
+
win.addEventListener('close', cleanup)
|
|
65
|
+
win.open()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Five waveforms
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
var names = ['SINE', 'SQUARE', 'SAWTOOTH', 'TRIANGLE', 'NOISE']
|
|
72
|
+
|
|
73
|
+
names.forEach(function (name, index) {
|
|
74
|
+
setTimeout(function () {
|
|
75
|
+
synth.playTone({
|
|
76
|
+
note: 'A4',
|
|
77
|
+
duration: 400,
|
|
78
|
+
attack: 8,
|
|
79
|
+
release: 100,
|
|
80
|
+
volume: 0.6,
|
|
81
|
+
waveType: WAVE[name]
|
|
82
|
+
})
|
|
83
|
+
}, index * 500)
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Noise ignores pitch, but `note` keeps the event shape valid.
|
|
88
|
+
|
|
89
|
+
## Pitch sweep and pan movement
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
synth.playTone({
|
|
93
|
+
frequency: 180,
|
|
94
|
+
frequencyEnd: 1200,
|
|
95
|
+
pan: -0.9,
|
|
96
|
+
panEnd: 0.9,
|
|
97
|
+
duration: 950,
|
|
98
|
+
attack: 5,
|
|
99
|
+
release: 160,
|
|
100
|
+
volume: 0.55,
|
|
101
|
+
waveType: WAVE.SAWTOOTH
|
|
102
|
+
})
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Set `frequencyEnd: 0` to keep the starting pitch. Set `panEnd` equal to `pan`
|
|
106
|
+
when no stereo movement is wanted.
|
|
107
|
+
|
|
108
|
+
## Vibrato
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
synth.playTone({
|
|
112
|
+
note: 'E4',
|
|
113
|
+
duration: 1600,
|
|
114
|
+
attack: 100,
|
|
115
|
+
release: 300,
|
|
116
|
+
volume: 0.7,
|
|
117
|
+
waveType: WAVE.SINE,
|
|
118
|
+
lfoFreq: 5.2,
|
|
119
|
+
lfoDepth: 6
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`lfoFreq` and `lfoDepth` are both in hertz. A depth of 6 Hz is gentle around
|
|
124
|
+
E4; use your ears when moving the same setting to a much lower note.
|
|
125
|
+
|
|
126
|
+
## Chord with an even spread
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
synth.playChord({
|
|
130
|
+
notes: ['C4', 'E4', 'G4', 'C5'],
|
|
131
|
+
duration: 1200,
|
|
132
|
+
attack: 120,
|
|
133
|
+
release: 360,
|
|
134
|
+
volume: 0.65,
|
|
135
|
+
waveType: WAVE.TRIANGLE,
|
|
136
|
+
panSpread: 0.85
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Chord with explicit pans
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
synth.playChord({
|
|
144
|
+
frequencies: [261.63, 329.63, 392, 523.25],
|
|
145
|
+
duration: 900,
|
|
146
|
+
attack: 20,
|
|
147
|
+
release: 240,
|
|
148
|
+
volume: 0.62,
|
|
149
|
+
waveType: WAVE.SINE,
|
|
150
|
+
pans: [-0.8, -0.25, 0.25, 0.8]
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Use `playChord` for a known simultaneous group. Launching four separate tones
|
|
155
|
+
is not atomic and gives each one full event gain.
|
|
156
|
+
|
|
157
|
+
## Tempo, note values and swing
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
synth.playPattern({
|
|
161
|
+
bpm: 132,
|
|
162
|
+
swing: 0.12,
|
|
163
|
+
noteValue: '8n',
|
|
164
|
+
attack: 4,
|
|
165
|
+
release: 55,
|
|
166
|
+
volume: 0.55,
|
|
167
|
+
waveType: WAVE.SQUARE,
|
|
168
|
+
steps: [
|
|
169
|
+
{ note: 'C4', duration: 150 },
|
|
170
|
+
{ note: 'E4', duration: 150 },
|
|
171
|
+
{ rest: true },
|
|
172
|
+
{ note: 'G4', duration: 150 },
|
|
173
|
+
{ note: 'C5', duration: 280, noteValue: '4n' }
|
|
174
|
+
]
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Positive swing lengthens one step and shortens the next. Negative swing flips
|
|
179
|
+
that relationship. Keep it subtle unless the lurch is the intended effect.
|
|
180
|
+
|
|
181
|
+
## Millisecond pattern
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
synth.playPattern({
|
|
185
|
+
stepDuration: 110,
|
|
186
|
+
pan: -0.4,
|
|
187
|
+
panEnd: 0.4,
|
|
188
|
+
waveType: WAVE.TRIANGLE,
|
|
189
|
+
steps: [
|
|
190
|
+
{ note: 'C5', duration: 90 },
|
|
191
|
+
{ note: 'D5', duration: 90 },
|
|
192
|
+
{ note: 'E5', duration: 90 },
|
|
193
|
+
{ note: 'G5', duration: 180 }
|
|
194
|
+
]
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
`stepDuration` controls spacing. Per-step `duration` controls how long the
|
|
199
|
+
sound itself lasts. The pattern root does not accept `duration`.
|
|
200
|
+
|
|
201
|
+
## Step overrides
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
synth.playPattern({
|
|
205
|
+
bpm: 100,
|
|
206
|
+
noteValue: '8n',
|
|
207
|
+
attack: 10,
|
|
208
|
+
release: 90,
|
|
209
|
+
volume: 0.5,
|
|
210
|
+
waveType: WAVE.SINE,
|
|
211
|
+
steps: [
|
|
212
|
+
{ note: 'C4' },
|
|
213
|
+
{
|
|
214
|
+
note: 'E4',
|
|
215
|
+
waveType: WAVE.SAWTOOTH,
|
|
216
|
+
pan: -0.7,
|
|
217
|
+
panEnd: 0.7,
|
|
218
|
+
lfoFreq: 7,
|
|
219
|
+
lfoDepth: 10
|
|
220
|
+
},
|
|
221
|
+
{ rest: true },
|
|
222
|
+
{ note: 'G4', volume: 0.75, noteValue: '4n' }
|
|
223
|
+
]
|
|
224
|
+
})
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Noise percussion
|
|
228
|
+
|
|
229
|
+
```javascript
|
|
230
|
+
synth.playPattern({
|
|
231
|
+
bpm: 116,
|
|
232
|
+
noteValue: '16n',
|
|
233
|
+
volume: 0.32,
|
|
234
|
+
waveType: WAVE.NOISE,
|
|
235
|
+
steps: [
|
|
236
|
+
{ frequency: 200, duration: 70, attack: 1, release: 60 },
|
|
237
|
+
{ rest: true },
|
|
238
|
+
{ frequency: 200, duration: 32, attack: 1, release: 25 },
|
|
239
|
+
{ frequency: 200, duration: 32, attack: 1, release: 25 },
|
|
240
|
+
{ rest: true },
|
|
241
|
+
{ frequency: 200, duration: 52, attack: 1, release: 44 }
|
|
242
|
+
]
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Every sounding step needs `frequency` or `note`, including a noise step. Do not
|
|
247
|
+
write a primitive step such as `200`; primitive steps return
|
|
248
|
+
`invalid_step_type`.
|
|
249
|
+
|
|
250
|
+
## Xylophone sketch
|
|
251
|
+
|
|
252
|
+
```javascript
|
|
253
|
+
function playBar (note, pan) {
|
|
254
|
+
return synth.playTone({
|
|
255
|
+
note: note,
|
|
256
|
+
duration: 520,
|
|
257
|
+
attack: 2,
|
|
258
|
+
release: 430,
|
|
259
|
+
volume: 0.62,
|
|
260
|
+
waveType: WAVE.TRIANGLE,
|
|
261
|
+
pan: pan
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
playBar('C5', -0.7)
|
|
266
|
+
setTimeout(function () { playBar('E5', -0.2) }, 130)
|
|
267
|
+
setTimeout(function () { playBar('G5', 0.3) }, 260)
|
|
268
|
+
setTimeout(function () { playBar('C6', 0.75) }, 390)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
These are deliberately independent strikes. Their releases overlap, so leave
|
|
272
|
+
headroom in `volume` and choose enough `maxVoices` for the tails.
|
|
273
|
+
|
|
274
|
+
## Simon-style pads
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
var simonPitches = [329.63, 440, 554.37, 659.25]
|
|
278
|
+
|
|
279
|
+
function playPad (index) {
|
|
280
|
+
return synth.playTone({
|
|
281
|
+
frequency: simonPitches[index],
|
|
282
|
+
duration: 340,
|
|
283
|
+
attack: 6,
|
|
284
|
+
release: 90,
|
|
285
|
+
volume: 0.62,
|
|
286
|
+
waveType: WAVE.SQUARE
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function playAllPads () {
|
|
291
|
+
return synth.playChord({
|
|
292
|
+
frequencies: simonPitches,
|
|
293
|
+
duration: 480,
|
|
294
|
+
attack: 8,
|
|
295
|
+
release: 120,
|
|
296
|
+
volume: 0.58,
|
|
297
|
+
waveType: WAVE.SQUARE,
|
|
298
|
+
pans: [-0.75, -0.25, 0.25, 0.75]
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Speaker-safe startup
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
var accepted = synth.startEngine({
|
|
307
|
+
maxVoices: 12,
|
|
308
|
+
mixingProfile: 'speakerSafe'
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
if (accepted) synth.setVolume({ volume: 0.85 })
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
This profile lowers source gain and the limiter ceiling. It cannot repair a
|
|
315
|
+
speaker that physically struggles with a very low pitch, so test the real
|
|
316
|
+
device and raise the pitch when needed.
|
|
317
|
+
|
|
318
|
+
## Validation at an input boundary
|
|
319
|
+
|
|
320
|
+
```javascript
|
|
321
|
+
function playUserFrequency (input) {
|
|
322
|
+
var limits = synth.getDefaults().limits.frequency
|
|
323
|
+
var frequency = Number(input)
|
|
324
|
+
|
|
325
|
+
if (!isFinite(frequency)) return false
|
|
326
|
+
if (frequency < limits.min || frequency > limits.max) return false
|
|
327
|
+
|
|
328
|
+
return synth.playTone({ frequency: frequency })
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The native contract rejects bad values rather than clamping them. Validation in
|
|
333
|
+
the app lets you give the user a specific message before the call reaches the
|
|
334
|
+
module.
|
|
335
|
+
|
|
336
|
+
## Stop and clean up
|
|
337
|
+
|
|
338
|
+
```javascript
|
|
339
|
+
synth.stopAll()
|
|
340
|
+
|
|
341
|
+
// Later, when the audio owner closes:
|
|
342
|
+
synth.shutdown()
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`stopAll()` fades current voices but leaves pending pattern scheduling alone.
|
|
346
|
+
`shutdown()` cancels the scheduler and releases the native stream.
|
|
347
|
+
|
|
348
|
+
The complete interactive demo is `example/app.js` in the upstream TiSynthEngine repository. For additional retro effects, playable xylophone interaction, memory pads, layered polyphony and cancellable repetition, continue with [Curated sound and interaction recipes](recipes.md).
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
> Source snapshot: official TiSynthEngine 1.0.0 documentation at commit `6685b81` (2026-08-29).
|
|
2
|
+
|
|
3
|
+
# Guide
|
|
4
|
+
|
|
5
|
+
This guide covers the normal path from installing TiSynthEngine to releasing
|
|
6
|
+
the native audio resources when the owning screen closes.
|
|
7
|
+
|
|
8
|
+
## Install the module
|
|
9
|
+
|
|
10
|
+
Use the archive for each target platform:
|
|
11
|
+
|
|
12
|
+
- `ti.synthengine-android-1.0.0.zip`
|
|
13
|
+
- `ti.synthengine-iphone-1.0.0.zip`
|
|
14
|
+
|
|
15
|
+
Put the archives in the Titanium project root or install them through the
|
|
16
|
+
Titanium CLI. Pin the same version for both platforms:
|
|
17
|
+
|
|
18
|
+
```xml
|
|
19
|
+
<modules>
|
|
20
|
+
<module platform="android" version="1.0.0">ti.synthengine</module>
|
|
21
|
+
<module platform="iphone" version="1.0.0">ti.synthengine</module>
|
|
22
|
+
</modules>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
TiSynthEngine requires Titanium SDK 13.3.1.GA or newer. The iOS renderer
|
|
26
|
+
requires iOS 13 or newer.
|
|
27
|
+
|
|
28
|
+
## Load and verify the contract
|
|
29
|
+
|
|
30
|
+
Require the module once in the code that owns audio. Check `apiVersion` before
|
|
31
|
+
starting the engine so a stale native archive fails with a useful message.
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
var synth = require('ti.synthengine')
|
|
35
|
+
var contract = synth.getDefaults()
|
|
36
|
+
|
|
37
|
+
if (contract.apiVersion !== '1.0.0') {
|
|
38
|
+
throw new Error('TiSynthEngine 1.0.0 is required; found ' + contract.apiVersion)
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`getDefaults()` works before the stream starts. Call it again after startup if
|
|
43
|
+
you need the frequency ceiling for input validation, because that ceiling
|
|
44
|
+
depends on the output sample rate.
|
|
45
|
+
|
|
46
|
+
## Lifecycle
|
|
47
|
+
|
|
48
|
+
Opening a low-latency stream while Titanium lays out the first window can cause
|
|
49
|
+
an underrun on some Android devices. Register `postlayout` before `open()`, wait
|
|
50
|
+
for a short quiet period, and cancel the timer if the window closes first.
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
var running = false
|
|
54
|
+
var startTimer = null
|
|
55
|
+
|
|
56
|
+
function startAudio () {
|
|
57
|
+
startTimer = null
|
|
58
|
+
win.removeEventListener('postlayout', onPostLayout)
|
|
59
|
+
running = synth.startEngine({
|
|
60
|
+
maxVoices: 16,
|
|
61
|
+
mixingProfile: 'conservative'
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function onPostLayout () {
|
|
66
|
+
if (startTimer !== null) clearTimeout(startTimer)
|
|
67
|
+
startTimer = setTimeout(startAudio, 250)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function closeAudio () {
|
|
71
|
+
win.removeEventListener('postlayout', onPostLayout)
|
|
72
|
+
win.removeEventListener('close', closeAudio)
|
|
73
|
+
|
|
74
|
+
if (startTimer !== null) {
|
|
75
|
+
clearTimeout(startTimer)
|
|
76
|
+
startTimer = null
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (running) {
|
|
80
|
+
synth.shutdown()
|
|
81
|
+
running = false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
win.addEventListener('postlayout', onPostLayout)
|
|
86
|
+
win.addEventListener('close', closeAudio)
|
|
87
|
+
win.open()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Use one owner for the engine. Start it once, pass a small audio service to the
|
|
91
|
+
screens that need it, and call `shutdown()` from the owner. A second
|
|
92
|
+
`startEngine()` call is harmless, but it makes ownership and cleanup unclear.
|
|
93
|
+
|
|
94
|
+
## Play the first sound
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
var accepted = synth.playTone({
|
|
98
|
+
note: 'A4',
|
|
99
|
+
duration: 300,
|
|
100
|
+
attack: 8,
|
|
101
|
+
release: 80,
|
|
102
|
+
volume: 0.8,
|
|
103
|
+
waveType: contract.waveTypes.SINE
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if (!accepted) Ti.API.warn('The tone was rejected')
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Calls that create or change sound return `true` when accepted and `false` when
|
|
110
|
+
rejected. During development, handle that result and inspect logcat or the
|
|
111
|
+
Xcode console for the structured validation warning.
|
|
112
|
+
|
|
113
|
+
## Choose a pitch
|
|
114
|
+
|
|
115
|
+
`playTone` accepts one of these fields:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
synth.playTone({ frequency: 440 })
|
|
119
|
+
synth.playTone({ note: 'A4' })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Do not send both. Notes accept English names (`C#4`, `Bb3`) and solfege names
|
|
123
|
+
(`DO4`, `FA#5`, `Sib3`). When the octave is omitted, octave 4 is used.
|
|
124
|
+
|
|
125
|
+
## Shape a tone
|
|
126
|
+
|
|
127
|
+
The oscillator runs for `duration` milliseconds. `attack` controls the fade in
|
|
128
|
+
and `release` controls the fade out; the time between them stays at full level.
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
synth.playTone({
|
|
132
|
+
note: 'C4',
|
|
133
|
+
duration: 1100,
|
|
134
|
+
attack: 180,
|
|
135
|
+
release: 420,
|
|
136
|
+
waveType: contract.waveTypes.TRIANGLE
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Explicit envelope values must fit inside `duration`. An attack equal to or
|
|
141
|
+
longer than the duration is rejected, as is a release longer than the time left
|
|
142
|
+
after attack.
|
|
143
|
+
|
|
144
|
+
Tone options can also move during the event:
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
synth.playTone({
|
|
148
|
+
frequency: 220,
|
|
149
|
+
frequencyEnd: 880,
|
|
150
|
+
pan: -0.8,
|
|
151
|
+
panEnd: 0.8,
|
|
152
|
+
lfoFreq: 5.5,
|
|
153
|
+
lfoDepth: 8,
|
|
154
|
+
duration: 1200,
|
|
155
|
+
release: 180,
|
|
156
|
+
waveType: contract.waveTypes.SAWTOOTH
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`frequencyEnd` creates a linear pitch sweep. `panEnd` moves the sound between
|
|
161
|
+
channels. `lfoFreq` is the vibrato rate in hertz and `lfoDepth` is its depth in
|
|
162
|
+
hertz.
|
|
163
|
+
|
|
164
|
+
## Play simultaneous notes
|
|
165
|
+
|
|
166
|
+
Use `playChord` when notes belong to one musical event:
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
synth.playChord({
|
|
170
|
+
notes: ['C4', 'E4', 'G4'],
|
|
171
|
+
duration: 900,
|
|
172
|
+
attack: 25,
|
|
173
|
+
release: 220,
|
|
174
|
+
panSpread: 0.7,
|
|
175
|
+
waveType: contract.waveTypes.SINE
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The chord is placed on the audio queue atomically. Its voices also share a
|
|
180
|
+
constant-power event gain. Several separate `playTone` calls are independent,
|
|
181
|
+
can begin in different callbacks, and are hotter when summed.
|
|
182
|
+
|
|
183
|
+
Use `pans` when every note needs a deliberate position. Missing entries remain
|
|
184
|
+
centered, and an array longer than the note array is rejected.
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
synth.playChord({
|
|
188
|
+
frequencies: [261.63, 329.63, 392],
|
|
189
|
+
pans: [-0.75, 0, 0.75],
|
|
190
|
+
duration: 700
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Play a pattern
|
|
195
|
+
|
|
196
|
+
A pattern contains 1 to 512 object steps. Each step must have exactly one of
|
|
197
|
+
`note`, `frequency` or `rest: true`.
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
synth.playPattern({
|
|
201
|
+
bpm: 126,
|
|
202
|
+
swing: 0.1,
|
|
203
|
+
noteValue: '8n',
|
|
204
|
+
waveType: contract.waveTypes.SQUARE,
|
|
205
|
+
steps: [
|
|
206
|
+
{ note: 'C4' },
|
|
207
|
+
{ note: 'E4' },
|
|
208
|
+
{ rest: true },
|
|
209
|
+
{ note: 'G4', noteValue: '4n', pan: 0.5 }
|
|
210
|
+
]
|
|
211
|
+
})
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Step timing is chosen in this order: step `duration`, step `noteValue`, pattern
|
|
215
|
+
`noteValue`, pattern `stepDuration`, then the 100 ms default. A step also
|
|
216
|
+
inherits the pattern's envelope, volume, waveform, pan, sweep and vibrato
|
|
217
|
+
options. Values set on the step override the inherited value.
|
|
218
|
+
|
|
219
|
+
Only one pattern scheduler is active. Starting another pattern cancels the
|
|
220
|
+
pending steps of the previous pattern. It does not stop voices that have
|
|
221
|
+
already started.
|
|
222
|
+
|
|
223
|
+
## Master level and stopping
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
synth.setVolume({ volume: 0.7 })
|
|
227
|
+
synth.stopAll()
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Master volume is ramped to avoid a step in the output. `stopAll()` fades active
|
|
231
|
+
voices, but it does not cancel future steps in the current pattern. Start a new
|
|
232
|
+
pattern or call `shutdown()` when future scheduling must stop as well.
|
|
233
|
+
|
|
234
|
+
## Output route changes
|
|
235
|
+
|
|
236
|
+
The native ports rebuild or resume their stream after headphones, Bluetooth or
|
|
237
|
+
other output routes change. The note crossing the transition may end early;
|
|
238
|
+
new notes can be played normally after the system settles. Do not restart the
|
|
239
|
+
whole app for a route change.
|
|
240
|
+
|
|
241
|
+
On iOS, `startEngine()` activates a `Playback` audio session. That ignores the
|
|
242
|
+
silent switch and normally interrupts other audio. If the app needs different
|
|
243
|
+
session behavior, read [coexisting with other audio](recommendations.md#coexisting-with-other-audio)
|
|
244
|
+
and test the final app on a device.
|
|
245
|
+
|
|
246
|
+
## Next steps
|
|
247
|
+
|
|
248
|
+
- [API reference](api.md) for every accepted key and limit.
|
|
249
|
+
- [Sound design](sound-design.md) for waveform and envelope recipes.
|
|
250
|
+
- [Examples](examples.md) for copy-ready sounds and patterns.
|
|
251
|
+
- [Troubleshooting](troubleshooting.md) when a call returns `false` or audio is
|
|
252
|
+
silent.
|