@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,150 @@
|
|
|
1
|
+
> Source snapshot: official TiSynthEngine 1.0.0 documentation at commit `6685b81` (2026-08-29).
|
|
2
|
+
|
|
3
|
+
# Recommendations
|
|
4
|
+
|
|
5
|
+
The defaults work for ordinary tones and chords. The choices below matter when
|
|
6
|
+
an app has dense overlap, user-controlled values, demanding speakers or several
|
|
7
|
+
audio systems sharing one lifecycle.
|
|
8
|
+
|
|
9
|
+
## Group simultaneous notes
|
|
10
|
+
|
|
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()`
|
|
13
|
+
calls are appropriate for independent touches and effects, but their start
|
|
14
|
+
times and gain are independent too.
|
|
15
|
+
|
|
16
|
+
For keys or pads a user can trigger freely, keep each event around 0.4 to 0.7
|
|
17
|
+
until the worst overlap has been heard. Raising every event above 1 is usually
|
|
18
|
+
a sign that the session needs `balanced`, not that each note needs more gain.
|
|
19
|
+
|
|
20
|
+
## Choose the mixing profile once
|
|
21
|
+
|
|
22
|
+
| Situation | Profile |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| General app audio and chords | `conservative` |
|
|
25
|
+
| Mostly single notes that need more level | `balanced` |
|
|
26
|
+
| Low pitches or distortion from a small phone speaker | `speakerSafe` |
|
|
27
|
+
| The app performs its own strict gain staging | `raw` |
|
|
28
|
+
|
|
29
|
+
The profile is fixed for the stream. Changing it requires `shutdown()` and a
|
|
30
|
+
new `startEngine()` call, so it should not be a live sound-design control.
|
|
31
|
+
|
|
32
|
+
If a phone buzzes under `speakerSafe`, lower the event volume or raise the
|
|
33
|
+
pitch. Small drivers can distort acoustically even when the digital output is
|
|
34
|
+
well below its ceiling.
|
|
35
|
+
|
|
36
|
+
## Size `maxVoices` for real overlap
|
|
37
|
+
|
|
38
|
+
`maxVoices` controls capacity. It does not make the engine louder or quieter.
|
|
39
|
+
|
|
40
|
+
| App behavior | Starting value |
|
|
41
|
+
| --- | ---: |
|
|
42
|
+
| Occasional interface feedback | 4 to 8 |
|
|
43
|
+
| Game effects with overlap | 8 to 16 |
|
|
44
|
+
| Chords, keys or long release tails | 16 to 24 |
|
|
45
|
+
| Explicit stress testing | 32 |
|
|
46
|
+
|
|
47
|
+
Count release tails, not just buttons pressed at the same instant. A new voice
|
|
48
|
+
steals the slot with the least time remaining when the pool is full. The short
|
|
49
|
+
fade prevents a hard click but cannot preserve the stolen note.
|
|
50
|
+
|
|
51
|
+
## Keep one lifecycle owner
|
|
52
|
+
|
|
53
|
+
Require the module in one audio service or controller, start it after the first
|
|
54
|
+
layout settles, and shut it down from the same owner. This avoids an engine that
|
|
55
|
+
outlives its window or a timer that starts after cleanup.
|
|
56
|
+
|
|
57
|
+
A 250 ms debounce after `postlayout` is a practical baseline for a plain
|
|
58
|
+
screen. Heavy startup work, advertisements or entry animations may need more.
|
|
59
|
+
Measure the real app instead of increasing the delay indefinitely.
|
|
60
|
+
|
|
61
|
+
Route recovery is automatic. Do not call `shutdown()` just because headphones
|
|
62
|
+
were inserted or removed. The sound crossing the transition may end early;
|
|
63
|
+
play later sounds normally once the system has moved to the new route.
|
|
64
|
+
|
|
65
|
+
## Coexisting with other audio
|
|
66
|
+
|
|
67
|
+
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.
|
|
71
|
+
|
|
72
|
+
A game that should respect the silent switch may set Titanium's ambient
|
|
73
|
+
category after the last engine startup:
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
if (Ti.Platform.osname !== 'android') {
|
|
77
|
+
Ti.Media.audioSessionCategory = Ti.Media.AUDIO_SESSION_CATEGORY_AMBIENT
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
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.
|
|
85
|
+
|
|
86
|
+
Android mixes independent output streams at the system level, but another app
|
|
87
|
+
or stale test process can still consume low-latency resources. Force-stop old
|
|
88
|
+
test builds when diagnosing a glitch that does not reproduce after reboot.
|
|
89
|
+
|
|
90
|
+
## Validate values at the app boundary
|
|
91
|
+
|
|
92
|
+
The native API rejects invalid values instead of clamping them. Use
|
|
93
|
+
`getDefaults().limits` for a user-facing slider, remote configuration or saved
|
|
94
|
+
sound preset.
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
function acceptedFrequency (raw) {
|
|
98
|
+
var value = Number(raw)
|
|
99
|
+
var range = synth.getDefaults().limits.frequency
|
|
100
|
+
if (!isFinite(value)) return null
|
|
101
|
+
if (value < range.min || value > range.max) return null
|
|
102
|
+
return value
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Do not copy the frequency maximum into application code. It depends on the
|
|
107
|
+
active sample rate and should be read again after startup.
|
|
108
|
+
|
|
109
|
+
Check Boolean return values during development. A rejected call includes the
|
|
110
|
+
method, key and reason in the native log. Logging that result at the caller is
|
|
111
|
+
often enough to identify a stale preset or misspelled field.
|
|
112
|
+
|
|
113
|
+
## Test the intended sound
|
|
114
|
+
|
|
115
|
+
Listen on the output people will use. Headphones reveal noise and stereo detail;
|
|
116
|
+
the phone speaker reveals acoustic buzz, weak bass and mono collapse. Test at
|
|
117
|
+
low, normal and high system volume.
|
|
118
|
+
|
|
119
|
+
Use one sustained chord when judging polyphonic timbre. A loop that starts the
|
|
120
|
+
same chord before its predecessor finishes deliberately changes the number of
|
|
121
|
+
overlapping events. That can drive the limiter in a repeating cycle and sound
|
|
122
|
+
like pumping. Keep the loop as an extreme scheduling and recovery test, not as
|
|
123
|
+
the reference for how the chord should sound.
|
|
124
|
+
|
|
125
|
+
Exercise the real worst case:
|
|
126
|
+
|
|
127
|
+
- every pad the UI allows at once;
|
|
128
|
+
- the fastest pattern the app can request;
|
|
129
|
+
- a long release followed by unrelated feedback;
|
|
130
|
+
- a route change while a note is playing;
|
|
131
|
+
- screen off, interruption and background transitions if the product depends
|
|
132
|
+
on them.
|
|
133
|
+
|
|
134
|
+
An underrun counter only describes transport. A clean counter cannot tell you
|
|
135
|
+
whether a waveform, pitch or mix is pleasant.
|
|
136
|
+
|
|
137
|
+
## Known limits
|
|
138
|
+
|
|
139
|
+
- There are no per-voice handles. A single active note cannot be edited or
|
|
140
|
+
stopped; `stopAll()` affects the whole pool.
|
|
141
|
+
- Every event has a finite duration of at most 60 seconds. There is no looping.
|
|
142
|
+
- The module synthesizes waveforms and does not play audio files.
|
|
143
|
+
- 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.
|
|
146
|
+
- Voice capacity and mixing profile remain fixed until shutdown.
|
|
147
|
+
- Pattern scheduling is timer-based, not sample-accurate.
|
|
148
|
+
|
|
149
|
+
Use a sample player or a dedicated music engine when a product needs streaming,
|
|
150
|
+
loop points, per-voice control or a synchronized composition timeline.
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
> Source snapshot: official TiSynthEngine 1.0.0 documentation at commit `6685b81` (2026-08-29).
|
|
2
|
+
|
|
3
|
+
# Sound design with TiSynthEngine
|
|
4
|
+
|
|
5
|
+
TiSynthEngine builds sounds from oscillators. There is no recorded sample
|
|
6
|
+
underneath a tone: pitch, waveform, envelope, movement and level describe the
|
|
7
|
+
whole sound. That small set of controls is enough for interface feedback,
|
|
8
|
+
musical cues, alarms, retro effects and simple percussion.
|
|
9
|
+
|
|
10
|
+
The examples assume the engine is running and use these constants:
|
|
11
|
+
|
|
12
|
+
```javascript
|
|
13
|
+
var synth = require('ti.synthengine')
|
|
14
|
+
var WAVE = synth.getDefaults().waveTypes
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
<!-- TOC-START -->
|
|
18
|
+
## Contents
|
|
19
|
+
|
|
20
|
+
- [Notes, frequencies and octaves](#notes-frequencies-and-octaves)
|
|
21
|
+
- [Waveform and timbre](#waveform-and-timbre)
|
|
22
|
+
- [Envelope and total duration](#envelope-and-total-duration)
|
|
23
|
+
- [Pan and movement](#pan-and-movement)
|
|
24
|
+
- [Vibrato and the LFO](#vibrato-and-the-lfo)
|
|
25
|
+
- [Chords, polyphony and gain](#chords-polyphony-and-gain)
|
|
26
|
+
- [Mixing profiles and limiter](#mixing-profiles-and-limiter)
|
|
27
|
+
- [Speakers, headphones and devices](#speakers-headphones-and-devices)
|
|
28
|
+
- [Recipes](#recipes)
|
|
29
|
+
|
|
30
|
+
<!-- TOC-END -->
|
|
31
|
+
|
|
32
|
+
## Notes, frequencies and octaves
|
|
33
|
+
|
|
34
|
+
Frequency is the number of waveform cycles per second, measured in hertz. A
|
|
35
|
+
440 Hz oscillator repeats 440 times each second. TiSynthEngine accepts a
|
|
36
|
+
frequency directly or converts a note name for you:
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
synth.playTone({ frequency: 440 })
|
|
40
|
+
synth.playTone({ note: 'A4' })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Both calls have the same pitch. Note names use an octave number: `C4` is middle
|
|
44
|
+
C, `C5` is one octave higher, and `C3` is one octave lower. Moving up one
|
|
45
|
+
octave doubles frequency; moving down halves it.
|
|
46
|
+
|
|
47
|
+
The note parser accepts English letters and solfege. Sharps use `#`; flats use
|
|
48
|
+
a lowercase `b`.
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
C4 C#4 Db4 D4
|
|
52
|
+
DO4 DO#4 REb4 RE4
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
When no octave is present, octave 4 is used. The supported octave digit is 0
|
|
56
|
+
through 9, subject to the current frequency ceiling.
|
|
57
|
+
|
|
58
|
+
The tuning system is twelve-tone equal temperament with A4 at 440 Hz. Each
|
|
59
|
+
semitone multiplies frequency by the twelfth root of two:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
frequency = 440 * 2 ^ ((midiNote - 69) / 12)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Equal temperament keeps intervals consistent in every key. It also means a
|
|
66
|
+
major third is the practical keyboard interval, not a pure 5:4 frequency
|
|
67
|
+
ratio. That distinction rarely matters for game or interface sounds.
|
|
68
|
+
|
|
69
|
+
## Waveform and timbre
|
|
70
|
+
|
|
71
|
+
Pitch says how fast the oscillator repeats. Waveform says how it moves within
|
|
72
|
+
each cycle, which determines the harmonic content and perceived timbre.
|
|
73
|
+
|
|
74
|
+
### Sine
|
|
75
|
+
|
|
76
|
+
`SINE` contains the fundamental and no intentional overtones. It works for
|
|
77
|
+
clean beeps, soft bass notes and sounds that should stay out of the way. On a
|
|
78
|
+
small speaker, very low sines may be quiet or may excite the speaker's own
|
|
79
|
+
distortion because there are no upper harmonics to carry the pitch.
|
|
80
|
+
|
|
81
|
+
### Square
|
|
82
|
+
|
|
83
|
+
`SQUARE` contains strong odd harmonics. It sounds hollow, bright and electronic.
|
|
84
|
+
It is a good starting point for retro game cues and clear melodic prompts. The
|
|
85
|
+
extra high-frequency energy makes it more assertive than a sine at the same
|
|
86
|
+
pitch.
|
|
87
|
+
|
|
88
|
+
### Sawtooth
|
|
89
|
+
|
|
90
|
+
`SAWTOOTH` contains both even and odd harmonics. It is the densest pitched
|
|
91
|
+
waveform in the module and reads as buzzy or brassy. Use it for alarms, arcade
|
|
92
|
+
leads and short sweeps. Square and sawtooth are band-limited with PolyBLEP to
|
|
93
|
+
reduce aliasing at discontinuities.
|
|
94
|
+
|
|
95
|
+
### Triangle
|
|
96
|
+
|
|
97
|
+
`TRIANGLE` contains odd harmonics that fall off faster than a square wave. It
|
|
98
|
+
has more body than a sine without the hard edge of a square. It suits gentle
|
|
99
|
+
melodies, pads and xylophone-like sketches.
|
|
100
|
+
|
|
101
|
+
### Noise
|
|
102
|
+
|
|
103
|
+
`NOISE` produces white noise rather than a repeating pitched cycle. Its
|
|
104
|
+
`frequency` is accepted because every sound event uses the same contract, but
|
|
105
|
+
changing that value does not tune the noise. A short envelope turns it into a
|
|
106
|
+
click, hat or impact layer; a long envelope creates wind or static.
|
|
107
|
+
|
|
108
|
+
Try every waveform at a moderate level before choosing one:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
;[WAVE.SINE, WAVE.SQUARE, WAVE.SAWTOOTH, WAVE.TRIANGLE, WAVE.NOISE]
|
|
112
|
+
.forEach(function (waveType, index) {
|
|
113
|
+
setTimeout(function () {
|
|
114
|
+
synth.playTone({
|
|
115
|
+
note: 'A4',
|
|
116
|
+
waveType: waveType,
|
|
117
|
+
duration: 450,
|
|
118
|
+
attack: 8,
|
|
119
|
+
release: 120,
|
|
120
|
+
volume: 0.65
|
|
121
|
+
})
|
|
122
|
+
}, index * 550)
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Envelope and total duration
|
|
127
|
+
|
|
128
|
+
Every event uses an attack, an implicit sustain section and a release:
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
silence -> attack -> full level -> release -> silence
|
|
132
|
+
|<---------- duration ---------->|
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`duration` is the total time, not the sustain time. If duration is 500 ms,
|
|
136
|
+
attack is 50 ms and release is 100 ms, the full-level middle lasts 350 ms.
|
|
137
|
+
|
|
138
|
+
Short attacks feel immediate. A 0 or 1 ms attack can be useful for percussion,
|
|
139
|
+
but an abrupt start at an unlucky phase can sound like a click. Values between
|
|
140
|
+
5 and 15 ms keep most game feedback crisp without a hard edge.
|
|
141
|
+
|
|
142
|
+
Long attacks soften the onset. Use 100 to 500 ms for pads and slow warnings.
|
|
143
|
+
Release controls how the sound leaves: 20 to 80 ms feels tight, while several
|
|
144
|
+
hundred milliseconds leaves a tail.
|
|
145
|
+
|
|
146
|
+
Explicit attack and release values must fit. These relationships are required:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
attack < duration
|
|
150
|
+
release <= duration - attack
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Defaults are adjusted internally if a very short duration leaves no room, but
|
|
154
|
+
values supplied by the caller are never reshaped silently.
|
|
155
|
+
|
|
156
|
+
## Pan and movement
|
|
157
|
+
|
|
158
|
+
`pan` ranges from -1 on the left to 1 on the right. Zero is centered. Stereo
|
|
159
|
+
movement is a straight interpolation from `pan` to `panEnd` over the event.
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
synth.playTone({
|
|
163
|
+
note: 'E5',
|
|
164
|
+
pan: -0.9,
|
|
165
|
+
panEnd: 0.9,
|
|
166
|
+
duration: 900,
|
|
167
|
+
attack: 10,
|
|
168
|
+
release: 120,
|
|
169
|
+
waveType: WAVE.TRIANGLE
|
|
170
|
+
})
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Phone speakers are often mono or close together, so pan may be subtle. Treat it
|
|
174
|
+
as extra detail rather than the only cue that distinguishes two sounds.
|
|
175
|
+
|
|
176
|
+
`frequencyEnd` makes a linear pitch sweep. Use an upward sweep for activation
|
|
177
|
+
or reward and a downward sweep for dismissal, power-down or impact.
|
|
178
|
+
|
|
179
|
+
```javascript
|
|
180
|
+
synth.playTone({
|
|
181
|
+
frequency: 180,
|
|
182
|
+
frequencyEnd: 920,
|
|
183
|
+
duration: 420,
|
|
184
|
+
attack: 4,
|
|
185
|
+
release: 90,
|
|
186
|
+
waveType: WAVE.SAWTOOTH,
|
|
187
|
+
volume: 0.55
|
|
188
|
+
})
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Vibrato and the LFO
|
|
192
|
+
|
|
193
|
+
The low-frequency oscillator modulates pitch. `lfoFreq` is the modulation rate
|
|
194
|
+
in cycles per second, and `lfoDepth` is the maximum pitch movement in hertz.
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
synth.playTone({
|
|
198
|
+
note: 'A4',
|
|
199
|
+
duration: 1400,
|
|
200
|
+
attack: 80,
|
|
201
|
+
release: 240,
|
|
202
|
+
lfoFreq: 5.5,
|
|
203
|
+
lfoDepth: 7,
|
|
204
|
+
waveType: WAVE.SINE
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A rate around 4 to 7 Hz with a small depth sounds like musical vibrato. A fast
|
|
209
|
+
rate or large depth becomes an effect. Because depth is measured in hertz, 8 Hz
|
|
210
|
+
is a larger musical interval on a low note than on a high note.
|
|
211
|
+
|
|
212
|
+
## Chords, polyphony and gain
|
|
213
|
+
|
|
214
|
+
`playChord` is for notes that should start together. The call is placed in the
|
|
215
|
+
native queue as one event, so the audio callback sees the whole chord or none
|
|
216
|
+
of it. Its source gain is divided by the square root of the chord's voice count
|
|
217
|
+
to keep power stable.
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
synth.playChord({
|
|
221
|
+
notes: ['C4', 'E4', 'G4', 'B4'],
|
|
222
|
+
duration: 1500,
|
|
223
|
+
attack: 180,
|
|
224
|
+
release: 450,
|
|
225
|
+
volume: 0.65,
|
|
226
|
+
waveType: WAVE.TRIANGLE,
|
|
227
|
+
pans: [-0.75, -0.25, 0.25, 0.75]
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Four separate `playTone` calls are four independent full-gain events. They are
|
|
232
|
+
not atomic and may start in different audio callbacks. Their sum is also hotter
|
|
233
|
+
and can push the limiter. Separate calls are correct for unrelated taps or
|
|
234
|
+
effects; use `playChord` when the app already knows the pitches are one chord.
|
|
235
|
+
|
|
236
|
+
`maxVoices` is capacity, not a loudness control. When the pool is full, the
|
|
237
|
+
voice with the least time remaining is faded and reused. Leave a small margin for release tails, but do
|
|
238
|
+
not set 32 by habit when the app can only make four sounds at once.
|
|
239
|
+
|
|
240
|
+
## Mixing profiles and limiter
|
|
241
|
+
|
|
242
|
+
Choose the profile once in `startEngine()`:
|
|
243
|
+
|
|
244
|
+
| Profile | Practical use |
|
|
245
|
+
| --- | --- |
|
|
246
|
+
| `conservative` | Default for mixed effects, chords and unpredictable overlap. |
|
|
247
|
+
| `balanced` | More level for mostly monophonic sounds. |
|
|
248
|
+
| `speakerSafe` | Lower source gain and a 0.70 ceiling for small speakers. |
|
|
249
|
+
| `raw` | Full source gain; useful only when the app owns all gain staging. |
|
|
250
|
+
|
|
251
|
+
Protected profiles calibrate source gain by waveform and event voice count.
|
|
252
|
+
The final stereo-linked limiter catches exceptional peaks with 10 ms of
|
|
253
|
+
lookahead and a 100 ms release. Its ceiling is 0.85, or 0.70 for `speakerSafe`.
|
|
254
|
+
|
|
255
|
+
The limiter is protection, not a loudness effect. If it works on every note,
|
|
256
|
+
lower event volume or choose a more conservative profile. Repeatedly starting
|
|
257
|
+
the same long sound before its previous release ends raises and lowers the sum,
|
|
258
|
+
which can produce audible pumping. That overlap is useful as a stress test, but
|
|
259
|
+
it is a poor reference for the tone's normal quality.
|
|
260
|
+
|
|
261
|
+
## Speakers, headphones and devices
|
|
262
|
+
|
|
263
|
+
Headphones usually reproduce bass and stereo movement more cleanly than a phone
|
|
264
|
+
speaker. A sound that is smooth in headphones can buzz on a small driver even
|
|
265
|
+
when the digital signal never clips. Low pitches and square or sawtooth waves
|
|
266
|
+
are the most demanding.
|
|
267
|
+
|
|
268
|
+
For speaker-facing sounds:
|
|
269
|
+
|
|
270
|
+
- begin with `conservative` or `speakerSafe`;
|
|
271
|
+
- keep important pitches above roughly 200 Hz;
|
|
272
|
+
- use short attacks instead of an instantaneous full-scale edge;
|
|
273
|
+
- test at several system volume settings on the target device;
|
|
274
|
+
- keep critical meaning in rhythm and pitch, not pan alone.
|
|
275
|
+
|
|
276
|
+
Different phones have different sample rates, speaker protection and acoustic
|
|
277
|
+
resonances. Test the real device family. A desktop browser or simulator cannot
|
|
278
|
+
answer whether a small speaker will rattle.
|
|
279
|
+
|
|
280
|
+
## Recipes
|
|
281
|
+
|
|
282
|
+
These recipes use only the 1.0.0 contract and keep explicit envelopes valid.
|
|
283
|
+
Tune pitch and volume for the app's other audio rather than treating the values
|
|
284
|
+
as mastering targets.
|
|
285
|
+
|
|
286
|
+
### Interface click
|
|
287
|
+
|
|
288
|
+
```javascript
|
|
289
|
+
synth.playTone({
|
|
290
|
+
frequency: 1200,
|
|
291
|
+
duration: 32,
|
|
292
|
+
attack: 1,
|
|
293
|
+
release: 24,
|
|
294
|
+
volume: 0.35,
|
|
295
|
+
waveType: WAVE.SINE
|
|
296
|
+
})
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The short sine is cleaner than a full-band noise click. Raise the frequency for
|
|
300
|
+
a lighter control or lower it for a heavier one.
|
|
301
|
+
|
|
302
|
+
### Confirmation hit
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
synth.playTone({
|
|
306
|
+
frequency: 760,
|
|
307
|
+
frequencyEnd: 1040,
|
|
308
|
+
duration: 150,
|
|
309
|
+
attack: 3,
|
|
310
|
+
release: 90,
|
|
311
|
+
volume: 0.6,
|
|
312
|
+
waveType: WAVE.TRIANGLE
|
|
313
|
+
})
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### Soft pad
|
|
317
|
+
|
|
318
|
+
```javascript
|
|
319
|
+
synth.playChord({
|
|
320
|
+
notes: ['C4', 'G4', 'C5', 'E5'],
|
|
321
|
+
duration: 2600,
|
|
322
|
+
attack: 480,
|
|
323
|
+
release: 900,
|
|
324
|
+
volume: 0.55,
|
|
325
|
+
waveType: WAVE.TRIANGLE,
|
|
326
|
+
panSpread: 0.8
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Two-tone alarm
|
|
331
|
+
|
|
332
|
+
```javascript
|
|
333
|
+
synth.playPattern({
|
|
334
|
+
bpm: 150,
|
|
335
|
+
noteValue: '8n',
|
|
336
|
+
attack: 8,
|
|
337
|
+
release: 70,
|
|
338
|
+
volume: 0.55,
|
|
339
|
+
waveType: WAVE.SAWTOOTH,
|
|
340
|
+
steps: [
|
|
341
|
+
{ note: 'A4' },
|
|
342
|
+
{ note: 'E5' },
|
|
343
|
+
{ note: 'A4' },
|
|
344
|
+
{ note: 'E5' }
|
|
345
|
+
]
|
|
346
|
+
})
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Retro pickup
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
synth.playPattern({
|
|
353
|
+
bpm: 180,
|
|
354
|
+
swing: 0.06,
|
|
355
|
+
noteValue: '16n',
|
|
356
|
+
attack: 2,
|
|
357
|
+
release: 45,
|
|
358
|
+
volume: 0.5,
|
|
359
|
+
waveType: WAVE.SQUARE,
|
|
360
|
+
steps: [
|
|
361
|
+
{ note: 'C5', duration: 80 },
|
|
362
|
+
{ note: 'E5', duration: 80 },
|
|
363
|
+
{ note: 'G5', duration: 80 },
|
|
364
|
+
{ note: 'C6', duration: 160, noteValue: '8n' }
|
|
365
|
+
]
|
|
366
|
+
})
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The pattern root does not accept `duration`. Put it on each sounding step when
|
|
370
|
+
you want a gate shorter than its rhythmic step.
|
|
371
|
+
|
|
372
|
+
### Noise percussion
|
|
373
|
+
|
|
374
|
+
```javascript
|
|
375
|
+
synth.playPattern({
|
|
376
|
+
bpm: 112,
|
|
377
|
+
noteValue: '8n',
|
|
378
|
+
waveType: WAVE.NOISE,
|
|
379
|
+
volume: 0.34,
|
|
380
|
+
steps: [
|
|
381
|
+
{ frequency: 220, duration: 65, attack: 1, release: 55 },
|
|
382
|
+
{ rest: true },
|
|
383
|
+
{ frequency: 220, duration: 35, attack: 1, release: 28 },
|
|
384
|
+
{ frequency: 220, duration: 35, attack: 1, release: 28 }
|
|
385
|
+
]
|
|
386
|
+
})
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Noise steps still need `frequency` or `note` because every sounding pattern
|
|
390
|
+
step follows the same shape. The pitch value is not used by the noise source.
|
|
391
|
+
|
|
392
|
+
### Simon-style cues
|
|
393
|
+
|
|
394
|
+
Use short, distinct square-wave pitches. Play one tone for the sequence and
|
|
395
|
+
one atomic chord only when demonstrating all pads together.
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
var SIMON = [329.63, 440, 554.37, 659.25]
|
|
399
|
+
|
|
400
|
+
function playSimonPad (index) {
|
|
401
|
+
return synth.playTone({
|
|
402
|
+
frequency: SIMON[index],
|
|
403
|
+
duration: 340,
|
|
404
|
+
attack: 6,
|
|
405
|
+
release: 90,
|
|
406
|
+
volume: 0.62,
|
|
407
|
+
waveType: WAVE.SQUARE
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function playAllSimonPads () {
|
|
412
|
+
return synth.playChord({
|
|
413
|
+
frequencies: SIMON,
|
|
414
|
+
duration: 520,
|
|
415
|
+
attack: 8,
|
|
416
|
+
release: 130,
|
|
417
|
+
volume: 0.58,
|
|
418
|
+
waveType: WAVE.SQUARE,
|
|
419
|
+
pans: [-0.75, -0.25, 0.25, 0.75]
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
For more complete code, continue with [Examples](examples.md) and
|
|
425
|
+
[Curated sound and interaction recipes](recipes.md). The latter includes retro
|
|
426
|
+
effects, a playable xylophone, memory-pad timbres, layered chords and timer
|
|
427
|
+
ownership. For gain and device-testing advice, see
|
|
428
|
+
[Recommendations](recommendations.md).
|