@maccesar/titools 5.1.1 → 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 +112 -72
- package/agents/ti-pro.md +5 -1
- package/commands/ti-audit.md +5 -1
- package/docs/examples.md +91 -0
- package/docs/knowledge-index.md +57 -0
- package/lib/config.js +2 -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 +58 -0
- package/skills/ti-admob-unified/agents/openai.yaml +4 -0
- package/skills/ti-admob-unified/assets/example/app.js +1207 -0
- package/skills/ti-admob-unified/assets/example/lib/ads.js +646 -0
- package/skills/ti-admob-unified/assets/example/library-usage.js +136 -0
- package/skills/ti-admob-unified/references/api-reference.md +615 -0
- package/skills/ti-admob-unified/references/banners.md +435 -0
- package/skills/ti-admob-unified/references/contributing.md +233 -0
- package/skills/ti-admob-unified/references/dependencies.md +91 -0
- package/skills/ti-admob-unified/references/fullscreen.md +238 -0
- package/skills/ti-admob-unified/references/game-monetization.md +148 -0
- package/skills/ti-admob-unified/references/getting-started.md +319 -0
- package/skills/ti-admob-unified/references/index.md +33 -0
- package/skills/ti-admob-unified/references/integration.md +87 -0
- package/skills/ti-admob-unified/references/library.md +220 -0
- package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
- package/skills/ti-admob-unified/references/mediation.md +145 -0
- package/skills/ti-admob-unified/references/migration.md +259 -0
- package/skills/ti-admob-unified/references/module-changelog.md +84 -0
- package/skills/ti-admob-unified/references/module-license.txt +21 -0
- package/skills/ti-admob-unified/references/privacy.md +281 -0
- 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 +143 -0
- package/skills/ti-admob-unified/references/source-notes.md +80 -0
- package/skills/ti-admob-unified/references/source-snapshot.json +644 -0
- package/skills/ti-admob-unified/references/third-party-notices.md +22 -0
- package/skills/ti-game/SKILL.md +31 -5
- package/skills/ti-game/references/api.md +36 -24
- package/skills/ti-game/references/debugging-performance.md +22 -7
- package/skills/ti-game/references/examples.md +113 -20
- package/skills/ti-game/references/input.md +101 -0
- package/skills/ti-game/references/patterns.md +56 -10
- package/skills/ti-game/references/physics.md +530 -0
- package/skills/ti-game/references/project-setup.md +131 -47
- package/skills/ti-game/references/recipes-action.md +149 -18
- package/skills/ti-game/references/recipes-adventure-effects.md +47 -4
- package/skills/ti-game/references/roadmap.md +11 -4
- package/skills/ti-game/references/source-notes.md +81 -0
- package/skills/ti-game/references/tilemaps.md +13 -4
- package/skills/ti-game-assets/SKILL.md +53 -0
- package/skills/ti-game-assets/agents/openai.yaml +4 -0
- package/skills/ti-game-assets/references/alpha.md +26 -0
- package/skills/ti-game-assets/references/art-direction.md +37 -0
- package/skills/ti-game-assets/references/integration.md +49 -0
- package/skills/ti-game-assets/references/manifest.example.json +429 -0
- package/skills/ti-game-assets/references/manifest.md +39 -0
- package/skills/ti-game-assets/references/offline-audio.md +33 -0
- package/skills/ti-game-assets/references/prompts.md +33 -0
- package/skills/ti-game-assets/references/repeating-textures.md +17 -0
- package/skills/ti-game-assets/references/responsible-ads.md +31 -0
- package/skills/ti-game-assets/references/sound-assets.md +42 -0
- package/skills/ti-game-assets/references/vector-sources.md +21 -0
- package/skills/ti-game-assets/scripts/alpha_tools.py +160 -0
- package/skills/ti-game-assets/scripts/requirements.txt +1 -0
- package/skills/ti-game-assets/scripts/validate_manifest.py +267 -0
- package/skills/ti-synthengine/SKILL.md +77 -67
- 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 +573 -153
- package/skills/ti-synthengine/references/audio-engine.md +272 -124
- package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
- package/skills/ti-synthengine/references/examples.md +355 -106
- 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 +426 -75
- 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 +434 -420
- package/skills/ti-synthengine/references/recommendations.md +72 -85
- package/skills/ti-synthengine/references/sound-design.md +375 -155
- package/skills/ti-synthengine/references/source-notes.md +109 -0
- package/skills/ti-synthengine/references/source-snapshot.json +452 -0
- package/skills/ti-synthengine/references/stats-performance.json +2992 -0
- package/skills/ti-synthengine/references/stats-performance.md +82 -0
- package/skills/ti-synthengine/references/studio-validation.json +6202 -0
- package/skills/ti-synthengine/references/troubleshooting.md +123 -78
- 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,1178 @@
|
|
|
1
|
+
var sequenceScore = require('lib/sequenceScore')
|
|
2
|
+
var Synth = require('ti.synthengine')
|
|
3
|
+
|
|
4
|
+
var sequenceOwner = require('lib/sequenceOwner')(Synth, {
|
|
5
|
+
rejected: function() { setStatus('Request rejected · sequence publication', COLORS.red) }
|
|
6
|
+
})
|
|
7
|
+
function publishSequence(options) { return sequenceOwner.submit(options) }
|
|
8
|
+
|
|
9
|
+
var defaults = Synth.getDefaults()
|
|
10
|
+
if (!defaults || defaults.apiVersion !== '1.0.0') {
|
|
11
|
+
throw new Error('TiSynthEngine 1.0.0 is required')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!defaults.limits || !defaults.limits.spatialX || !defaults.limits.spatialY
|
|
15
|
+
|| typeof Synth.getStats !== 'function' || typeof Synth.resetStats !== 'function'
|
|
16
|
+
|| typeof Synth.setStatsEnabled !== 'function') {
|
|
17
|
+
throw new Error('Install the current TiSynthEngine 1.0.0 packages, then rebuild and reinstall this app')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
var WAVE = defaults.waveTypes
|
|
21
|
+
var STEPPED_WAVE = [-1, -0.78, -0.16, 0.84, 1, 0.42, -0.2, -0.62]
|
|
22
|
+
var engineRunning = false
|
|
23
|
+
var startTimer = null
|
|
24
|
+
var statsTimer = null
|
|
25
|
+
var diagnosticsEnabled = false
|
|
26
|
+
var selectedSession = 'playback'
|
|
27
|
+
var masterVolume = 1
|
|
28
|
+
var binauralEnabled = true
|
|
29
|
+
var interactiveControls = []
|
|
30
|
+
|
|
31
|
+
var COLORS = {
|
|
32
|
+
canvas: '#030712',
|
|
33
|
+
surface: '#111827',
|
|
34
|
+
surfaceRaised: '#1f2937',
|
|
35
|
+
surfaceMuted: '#374151',
|
|
36
|
+
text: '#ffffff',
|
|
37
|
+
textSecondary: '#9ca3af',
|
|
38
|
+
textMuted: '#6b7280',
|
|
39
|
+
green: '#16a34a',
|
|
40
|
+
greenPressed: '#15803d',
|
|
41
|
+
red: '#dc2626',
|
|
42
|
+
redDark: '#991b1b',
|
|
43
|
+
yellow: '#eab308',
|
|
44
|
+
yellowPressed: '#ca8a04',
|
|
45
|
+
blue: '#2563eb',
|
|
46
|
+
blueDark: '#1e40af',
|
|
47
|
+
indigo: '#4338ca',
|
|
48
|
+
indigoPressed: '#3730a3',
|
|
49
|
+
orange: '#c2410c',
|
|
50
|
+
orangePressed: '#9a3412',
|
|
51
|
+
teal: '#0f766e',
|
|
52
|
+
tealPressed: '#115e59',
|
|
53
|
+
pink: '#be185d',
|
|
54
|
+
pinkPressed: '#9d174d',
|
|
55
|
+
brown: '#78350f',
|
|
56
|
+
brownPressed: '#451a03'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function dp(value) {
|
|
60
|
+
return value + 'dp'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Window and reusable UI
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
var win = Ti.UI.createWindow({
|
|
68
|
+
backgroundColor: COLORS.canvas,
|
|
69
|
+
layout: 'vertical',
|
|
70
|
+
navBarHidden: true,
|
|
71
|
+
extendSafeArea: false
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
var scrollOptions = {
|
|
75
|
+
layout: 'vertical',
|
|
76
|
+
contentWidth: Ti.UI.FILL,
|
|
77
|
+
contentHeight: Ti.UI.SIZE,
|
|
78
|
+
showVerticalScrollIndicator: false,
|
|
79
|
+
width: Ti.UI.FILL,
|
|
80
|
+
height: Ti.UI.FILL,
|
|
81
|
+
touchEnabled: false
|
|
82
|
+
}
|
|
83
|
+
if (Ti.Platform.osname === 'android') {
|
|
84
|
+
scrollOptions.scrollType = 'vertical'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var scrollView = Ti.UI.createScrollView(scrollOptions)
|
|
88
|
+
|
|
89
|
+
function makeText(text, options) {
|
|
90
|
+
options = options || {}
|
|
91
|
+
return Ti.UI.createLabel({
|
|
92
|
+
text: text,
|
|
93
|
+
color: options.color || COLORS.text,
|
|
94
|
+
width: options.width || Ti.UI.FILL,
|
|
95
|
+
height: options.height || Ti.UI.SIZE,
|
|
96
|
+
textAlign: options.textAlign || Ti.UI.TEXT_ALIGNMENT_LEFT,
|
|
97
|
+
verticalAlign: options.verticalAlign,
|
|
98
|
+
includeFontPadding: options.includeFontPadding,
|
|
99
|
+
top: options.top,
|
|
100
|
+
right: options.right,
|
|
101
|
+
bottom: options.bottom,
|
|
102
|
+
left: options.left,
|
|
103
|
+
opacity: options.opacity === undefined ? 1 : options.opacity,
|
|
104
|
+
font: {
|
|
105
|
+
fontSize: options.fontSize || 14,
|
|
106
|
+
fontWeight: options.fontWeight || 'normal'
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function makeBadge(text, backgroundColor) {
|
|
112
|
+
var badge = Ti.UI.createView({
|
|
113
|
+
backgroundColor: backgroundColor,
|
|
114
|
+
borderRadius: dp(12),
|
|
115
|
+
width: Ti.UI.SIZE,
|
|
116
|
+
height: dp(24),
|
|
117
|
+
right: dp(8)
|
|
118
|
+
})
|
|
119
|
+
badge.add(makeText(text, {
|
|
120
|
+
color: COLORS.text,
|
|
121
|
+
width: Ti.UI.SIZE,
|
|
122
|
+
left: dp(10),
|
|
123
|
+
right: dp(10),
|
|
124
|
+
fontSize: 11,
|
|
125
|
+
fontWeight: 'bold',
|
|
126
|
+
textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER
|
|
127
|
+
}))
|
|
128
|
+
return badge
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
var statusDot = Ti.UI.createView({
|
|
132
|
+
backgroundColor: COLORS.yellow,
|
|
133
|
+
width: dp(8),
|
|
134
|
+
height: dp(8),
|
|
135
|
+
borderRadius: dp(4),
|
|
136
|
+
top: dp(12),
|
|
137
|
+
left: dp(12)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
var statusLabel = makeText('Preparing audio after layout…', {
|
|
141
|
+
color: COLORS.textSecondary,
|
|
142
|
+
width: Ti.UI.SIZE,
|
|
143
|
+
height: Ti.UI.FILL,
|
|
144
|
+
left: dp(8),
|
|
145
|
+
right: dp(12),
|
|
146
|
+
fontSize: 12,
|
|
147
|
+
fontWeight: 'bold',
|
|
148
|
+
verticalAlign: Ti.UI.TEXT_VERTICAL_ALIGNMENT_CENTER,
|
|
149
|
+
includeFontPadding: false
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
var statusPill = Ti.UI.createView({
|
|
153
|
+
layout: 'horizontal',
|
|
154
|
+
backgroundColor: COLORS.surfaceRaised,
|
|
155
|
+
borderRadius: dp(16),
|
|
156
|
+
height: dp(32),
|
|
157
|
+
width: Ti.UI.SIZE,
|
|
158
|
+
top: dp(18)
|
|
159
|
+
})
|
|
160
|
+
statusPill.add(statusDot)
|
|
161
|
+
statusPill.add(statusLabel)
|
|
162
|
+
|
|
163
|
+
function setStatus(text, tone) {
|
|
164
|
+
statusLabel.text = text
|
|
165
|
+
statusDot.backgroundColor = tone || COLORS.green
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function makeHero() {
|
|
169
|
+
var hero = Ti.UI.createView({
|
|
170
|
+
layout: 'vertical',
|
|
171
|
+
height: Ti.UI.SIZE,
|
|
172
|
+
left: dp(20),
|
|
173
|
+
right: dp(20),
|
|
174
|
+
top: Ti.Platform.osname === 'android' ? dp(30) : dp(20),
|
|
175
|
+
bottom: dp(8)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
hero.add(makeText('NATIVE AUDIO PLAYGROUND', {
|
|
179
|
+
color: COLORS.textMuted,
|
|
180
|
+
fontSize: 11,
|
|
181
|
+
fontWeight: 'bold',
|
|
182
|
+
textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER
|
|
183
|
+
}))
|
|
184
|
+
hero.add(makeText('TiSynthEngine', {
|
|
185
|
+
top: dp(6),
|
|
186
|
+
fontSize: 30,
|
|
187
|
+
fontWeight: 'bold',
|
|
188
|
+
textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER
|
|
189
|
+
}))
|
|
190
|
+
hero.add(makeText('Explore synthesis, sequencing, headphone positioning and engine diagnostics.', {
|
|
191
|
+
top: dp(8),
|
|
192
|
+
color: COLORS.textSecondary,
|
|
193
|
+
fontSize: 14,
|
|
194
|
+
textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER
|
|
195
|
+
}))
|
|
196
|
+
|
|
197
|
+
var badgeRow = Ti.UI.createView({
|
|
198
|
+
layout: 'horizontal',
|
|
199
|
+
width: Ti.UI.SIZE,
|
|
200
|
+
height: Ti.UI.SIZE,
|
|
201
|
+
top: dp(16)
|
|
202
|
+
})
|
|
203
|
+
badgeRow.add(makeBadge('API 1.0.0', COLORS.indigo))
|
|
204
|
+
badgeRow.add(makeBadge('ANDROID + iOS', COLORS.surfaceMuted))
|
|
205
|
+
hero.add(badgeRow)
|
|
206
|
+
hero.add(statusPill)
|
|
207
|
+
|
|
208
|
+
return hero
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function makeSection(title, description, accentColor) {
|
|
212
|
+
var section = Ti.UI.createView({
|
|
213
|
+
layout: 'vertical',
|
|
214
|
+
height: Ti.UI.SIZE,
|
|
215
|
+
left: dp(16),
|
|
216
|
+
right: dp(16),
|
|
217
|
+
top: dp(20),
|
|
218
|
+
backgroundColor: COLORS.surface,
|
|
219
|
+
borderRadius: dp(12)
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
section.add(Ti.UI.createView({
|
|
223
|
+
height: dp(4),
|
|
224
|
+
left: 0,
|
|
225
|
+
right: 0,
|
|
226
|
+
top: 0,
|
|
227
|
+
backgroundColor: accentColor
|
|
228
|
+
}))
|
|
229
|
+
section.add(makeText(title.toUpperCase(), {
|
|
230
|
+
top: dp(16),
|
|
231
|
+
left: dp(16),
|
|
232
|
+
right: dp(16),
|
|
233
|
+
color: COLORS.textSecondary,
|
|
234
|
+
fontSize: 12,
|
|
235
|
+
fontWeight: 'bold'
|
|
236
|
+
}))
|
|
237
|
+
section.add(makeText(description, {
|
|
238
|
+
top: dp(5),
|
|
239
|
+
left: dp(16),
|
|
240
|
+
right: dp(16),
|
|
241
|
+
bottom: dp(4),
|
|
242
|
+
color: COLORS.text,
|
|
243
|
+
fontSize: 16,
|
|
244
|
+
fontWeight: 'bold'
|
|
245
|
+
}))
|
|
246
|
+
|
|
247
|
+
return section
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function makeRow() {
|
|
251
|
+
return Ti.UI.createView({
|
|
252
|
+
layout: 'horizontal',
|
|
253
|
+
width: Ti.UI.FILL,
|
|
254
|
+
height: Ti.UI.SIZE
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function makeAction(options) {
|
|
259
|
+
var action = Ti.UI.createView({
|
|
260
|
+
backgroundColor: options.color,
|
|
261
|
+
borderRadius: dp(options.radius || 8),
|
|
262
|
+
width: options.width || Ti.UI.FILL,
|
|
263
|
+
height: dp(options.height || 52),
|
|
264
|
+
top: dp(options.top === undefined ? 6 : options.top),
|
|
265
|
+
right: options.right === undefined ? dp(16) : options.right,
|
|
266
|
+
bottom: options.bottom,
|
|
267
|
+
left: options.left === undefined ? dp(16) : options.left,
|
|
268
|
+
opacity: 0.58,
|
|
269
|
+
touchEnabled: false,
|
|
270
|
+
bubbleParent: false,
|
|
271
|
+
accessibilityLabel: options.accessibilityLabel || options.title,
|
|
272
|
+
accessibilityHint: options.accessibilityHint || 'Plays this sound demonstration'
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
var label = makeText(options.title, {
|
|
276
|
+
color: options.textColor || COLORS.text,
|
|
277
|
+
left: dp(8),
|
|
278
|
+
right: dp(8),
|
|
279
|
+
fontSize: options.fontSize || 13,
|
|
280
|
+
fontWeight: 'bold',
|
|
281
|
+
textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER
|
|
282
|
+
})
|
|
283
|
+
label.accessibilityHidden = true
|
|
284
|
+
action.add(label)
|
|
285
|
+
|
|
286
|
+
function restoreColor() {
|
|
287
|
+
action.backgroundColor = options.color
|
|
288
|
+
action.opacity = engineRunning || options.allowStopped ? 1 : 0.58
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
action.addEventListener('touchstart', function () {
|
|
292
|
+
if (!engineRunning && !options.allowStopped) return
|
|
293
|
+
action.backgroundColor = options.pressedColor || COLORS.surfaceMuted
|
|
294
|
+
action.opacity = 0.9
|
|
295
|
+
})
|
|
296
|
+
action.addEventListener('touchend', restoreColor)
|
|
297
|
+
action.addEventListener('touchcancel', restoreColor)
|
|
298
|
+
action.addEventListener('click', function () {
|
|
299
|
+
if (!engineRunning && !options.allowStopped) return
|
|
300
|
+
restoreColor()
|
|
301
|
+
setStatus(options.status || ('Playing ' + options.title), options.statusColor || options.color)
|
|
302
|
+
var accepted = options.onPress()
|
|
303
|
+
if (accepted === false || accepted === 0) {
|
|
304
|
+
setStatus('Request rejected · inspect the native log', COLORS.red)
|
|
305
|
+
}
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
interactiveControls.push({ view: action, allowStopped: !!options.allowStopped })
|
|
309
|
+
return action
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function makeGridAction(options) {
|
|
313
|
+
options.width = '46%'
|
|
314
|
+
options.left = '2%'
|
|
315
|
+
options.right = '2%'
|
|
316
|
+
return makeAction(options)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function makeCompactAction(options) {
|
|
320
|
+
options.width = '29%'
|
|
321
|
+
options.left = '2%'
|
|
322
|
+
options.right = '2%'
|
|
323
|
+
options.height = 48
|
|
324
|
+
return makeAction(options)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function playMatchedSine(note) {
|
|
328
|
+
return Synth.playTone({
|
|
329
|
+
note: note,
|
|
330
|
+
duration: 600,
|
|
331
|
+
attack: 10,
|
|
332
|
+
release: 80,
|
|
333
|
+
volume: 0.7,
|
|
334
|
+
waveType: WAVE.SINE
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function playMatchedArpeggio(pitchKey, pitches) {
|
|
339
|
+
return publishSequence(sequenceScore({
|
|
340
|
+
attack: 5,
|
|
341
|
+
release: 30,
|
|
342
|
+
volume: 0.7,
|
|
343
|
+
waveType: WAVE.SQUARE,
|
|
344
|
+
steps: pitches.map(function (pitch) {
|
|
345
|
+
var step = { duration: 140 }
|
|
346
|
+
step[pitchKey] = pitch
|
|
347
|
+
return step
|
|
348
|
+
})
|
|
349
|
+
}))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
scrollView.add(makeHero())
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
// 1. Waveform demos
|
|
356
|
+
// ---------------------------------------------------------------------------
|
|
357
|
+
|
|
358
|
+
var waveSection = makeSection(
|
|
359
|
+
'Waveforms · 440 Hz',
|
|
360
|
+
'Same 70% gain; harmonics still change perceived loudness.',
|
|
361
|
+
COLORS.indigo
|
|
362
|
+
)
|
|
363
|
+
var waveNames = ['Sine', 'Square', 'Sawtooth', 'Triangle', 'Noise', 'Wavetable']
|
|
364
|
+
var waveTypes = [WAVE.SINE, WAVE.SQUARE, WAVE.SAWTOOTH, WAVE.TRIANGLE, WAVE.NOISE, WAVE.WAVETABLE]
|
|
365
|
+
var waveStyles = [
|
|
366
|
+
{ color: COLORS.green, pressed: COLORS.greenPressed },
|
|
367
|
+
{ color: COLORS.blue, pressed: COLORS.blueDark },
|
|
368
|
+
{ color: COLORS.orange, pressed: COLORS.orangePressed },
|
|
369
|
+
{ color: COLORS.indigo, pressed: COLORS.indigoPressed },
|
|
370
|
+
{ color: COLORS.surfaceMuted, pressed: COLORS.surfaceRaised },
|
|
371
|
+
{ color: COLORS.teal, pressed: COLORS.tealPressed }
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
var waveRow = null
|
|
375
|
+
waveNames.forEach(function (name, index) {
|
|
376
|
+
if (index % 2 === 0) {
|
|
377
|
+
waveRow = makeRow()
|
|
378
|
+
waveSection.add(waveRow)
|
|
379
|
+
}
|
|
380
|
+
waveRow.add(makeGridAction({
|
|
381
|
+
title: name === 'Noise' ? 'Noise\nUnpitched' : name + '\n440 Hz',
|
|
382
|
+
color: waveStyles[index].color,
|
|
383
|
+
pressedColor: waveStyles[index].pressed,
|
|
384
|
+
status: name === 'Noise' ? 'Noise · unpitched' : name + ' · 440 Hz',
|
|
385
|
+
onPress: function () {
|
|
386
|
+
var options = {
|
|
387
|
+
frequency: 440,
|
|
388
|
+
duration: 600,
|
|
389
|
+
attack: 10,
|
|
390
|
+
release: 80,
|
|
391
|
+
volume: 0.7,
|
|
392
|
+
waveType: waveTypes[index]
|
|
393
|
+
}
|
|
394
|
+
if (waveTypes[index] === WAVE.WAVETABLE) options.waveTable = STEPPED_WAVE
|
|
395
|
+
return Synth.playTone(options)
|
|
396
|
+
}
|
|
397
|
+
}))
|
|
398
|
+
})
|
|
399
|
+
waveSection.add(Ti.UI.createView({ height: dp(12) }))
|
|
400
|
+
scrollView.add(waveSection)
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// 2. Note names
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
|
|
406
|
+
var noteSection = makeSection(
|
|
407
|
+
'Note names · matched A/B',
|
|
408
|
+
'Same sine, envelope and gain as above. A4 and La4 resolve to 440 Hz.',
|
|
409
|
+
COLORS.green
|
|
410
|
+
)
|
|
411
|
+
var noteDemos = [
|
|
412
|
+
{ note: 'A4', frequency: '440 Hz' },
|
|
413
|
+
{ note: 'La4', frequency: '440 Hz' },
|
|
414
|
+
{ note: 'C4', frequency: '261.63 Hz' },
|
|
415
|
+
{ note: 'Do4', frequency: '261.63 Hz' },
|
|
416
|
+
{ note: 'F#4', frequency: '369.99 Hz' },
|
|
417
|
+
{ note: 'Fa#4', frequency: '369.99 Hz' },
|
|
418
|
+
{ note: 'Bb3', frequency: '233.08 Hz' },
|
|
419
|
+
{ note: 'Sib3', frequency: '233.08 Hz' }
|
|
420
|
+
]
|
|
421
|
+
var noteRow = null
|
|
422
|
+
noteDemos.forEach(function (demo, index) {
|
|
423
|
+
if (index % 2 === 0) {
|
|
424
|
+
noteRow = makeRow()
|
|
425
|
+
noteSection.add(noteRow)
|
|
426
|
+
}
|
|
427
|
+
noteRow.add(makeGridAction({
|
|
428
|
+
title: demo.note + '\n' + demo.frequency,
|
|
429
|
+
color: COLORS.surfaceMuted,
|
|
430
|
+
pressedColor: COLORS.surfaceRaised,
|
|
431
|
+
status: 'Note ' + demo.note + ' · sine at 70%',
|
|
432
|
+
onPress: function () {
|
|
433
|
+
return playMatchedSine(demo.note)
|
|
434
|
+
}
|
|
435
|
+
}))
|
|
436
|
+
})
|
|
437
|
+
noteSection.add(Ti.UI.createView({ height: dp(12) }))
|
|
438
|
+
scrollView.add(noteSection)
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// 3. Motion and modulation
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
var motionSection = makeSection(
|
|
445
|
+
'Motion and modulation',
|
|
446
|
+
'Move a sound through stereo space, pitch and vibrato.',
|
|
447
|
+
COLORS.pink
|
|
448
|
+
)
|
|
449
|
+
motionSection.add(makeAction({
|
|
450
|
+
title: 'PAN SWEEP\nLeft → Right · 2 seconds',
|
|
451
|
+
color: COLORS.teal,
|
|
452
|
+
pressedColor: COLORS.tealPressed,
|
|
453
|
+
status: 'Pan sweep · left to right',
|
|
454
|
+
onPress: function () {
|
|
455
|
+
return Synth.playTone({
|
|
456
|
+
frequency: 440,
|
|
457
|
+
duration: 2000,
|
|
458
|
+
attack: 20,
|
|
459
|
+
release: 200,
|
|
460
|
+
volume: 0.8,
|
|
461
|
+
pan: -1.0,
|
|
462
|
+
panEnd: 1.0
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
}))
|
|
466
|
+
motionSection.add(makeAction({
|
|
467
|
+
title: 'PITCH SWEEP\n200 → 900 Hz · sawtooth',
|
|
468
|
+
color: COLORS.pink,
|
|
469
|
+
pressedColor: COLORS.pinkPressed,
|
|
470
|
+
status: 'Pitch sweep · 200 to 900 Hz',
|
|
471
|
+
onPress: function () {
|
|
472
|
+
return Synth.playTone({
|
|
473
|
+
frequency: 200,
|
|
474
|
+
frequencyEnd: 900,
|
|
475
|
+
duration: 400,
|
|
476
|
+
attack: 5,
|
|
477
|
+
release: 40,
|
|
478
|
+
volume: 0.7,
|
|
479
|
+
waveType: WAVE.SAWTOOTH
|
|
480
|
+
})
|
|
481
|
+
}
|
|
482
|
+
}))
|
|
483
|
+
motionSection.add(makeAction({
|
|
484
|
+
title: 'LFO VIBRATO\n5 Hz · depth 30',
|
|
485
|
+
color: COLORS.orange,
|
|
486
|
+
pressedColor: COLORS.orangePressed,
|
|
487
|
+
status: 'LFO vibrato · 5 Hz',
|
|
488
|
+
onPress: function () {
|
|
489
|
+
return Synth.playTone({
|
|
490
|
+
frequency: 440,
|
|
491
|
+
duration: 2000,
|
|
492
|
+
attack: 20,
|
|
493
|
+
release: 200,
|
|
494
|
+
volume: 0.7,
|
|
495
|
+
lfoFreq: 5,
|
|
496
|
+
lfoDepth: 30
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
}))
|
|
500
|
+
motionSection.add(Ti.UI.createView({ height: dp(16) }))
|
|
501
|
+
scrollView.add(motionSection)
|
|
502
|
+
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// 4. Wave shaping
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
|
|
507
|
+
var retroSection = makeSection(
|
|
508
|
+
'Wave shaping',
|
|
509
|
+
'Hear variable-rate sample-and-hold noise, low-pass filtering, variable pulse width, a custom wavetable and 8-bit quantization.',
|
|
510
|
+
COLORS.orange
|
|
511
|
+
)
|
|
512
|
+
retroSection.add(makeAction({
|
|
513
|
+
title: 'FILTERED NOISE DECAY\n12 kHz → 650 Hz · 8-bit',
|
|
514
|
+
color: COLORS.brown,
|
|
515
|
+
pressedColor: COLORS.brownPressed,
|
|
516
|
+
status: 'Filtered sample-and-hold noise sweep',
|
|
517
|
+
onPress: function () {
|
|
518
|
+
return Synth.playTone({
|
|
519
|
+
frequency: 120,
|
|
520
|
+
duration: 760,
|
|
521
|
+
attack: 1,
|
|
522
|
+
release: 580,
|
|
523
|
+
volume: 0.68,
|
|
524
|
+
waveType: WAVE.NOISE,
|
|
525
|
+
noiseRate: 12000,
|
|
526
|
+
noiseRateEnd: 650,
|
|
527
|
+
lowPassHz: 2500,
|
|
528
|
+
bitDepth: 8
|
|
529
|
+
})
|
|
530
|
+
}
|
|
531
|
+
}))
|
|
532
|
+
retroSection.add(makeAction({
|
|
533
|
+
title: 'PULSE-WIDTH SWEEP\n10% → 86% · A3',
|
|
534
|
+
color: COLORS.orange,
|
|
535
|
+
pressedColor: COLORS.orangePressed,
|
|
536
|
+
status: 'Square pulse-width sweep',
|
|
537
|
+
onPress: function () {
|
|
538
|
+
return Synth.playTone({
|
|
539
|
+
note: 'A3',
|
|
540
|
+
duration: 700,
|
|
541
|
+
attack: 3,
|
|
542
|
+
release: 180,
|
|
543
|
+
volume: 0.58,
|
|
544
|
+
waveType: WAVE.SQUARE,
|
|
545
|
+
pulseWidth: 0.1,
|
|
546
|
+
pulseWidthEnd: 0.86
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
}))
|
|
550
|
+
retroSection.add(makeAction({
|
|
551
|
+
title: 'STEPPED WAVETABLE\n8 samples · D4 · 8-bit',
|
|
552
|
+
color: COLORS.teal,
|
|
553
|
+
pressedColor: COLORS.tealPressed,
|
|
554
|
+
status: 'Custom stepped wavetable',
|
|
555
|
+
onPress: function () {
|
|
556
|
+
return Synth.playTone({
|
|
557
|
+
note: 'D4',
|
|
558
|
+
duration: 680,
|
|
559
|
+
attack: 4,
|
|
560
|
+
release: 220,
|
|
561
|
+
volume: 0.6,
|
|
562
|
+
waveType: WAVE.WAVETABLE,
|
|
563
|
+
waveTable: STEPPED_WAVE,
|
|
564
|
+
bitDepth: 8
|
|
565
|
+
})
|
|
566
|
+
}
|
|
567
|
+
}))
|
|
568
|
+
retroSection.add(makeAction({
|
|
569
|
+
title: 'SHAPING PATTERN\nNoise → pulse → wavetable',
|
|
570
|
+
color: COLORS.indigo,
|
|
571
|
+
pressedColor: COLORS.indigoPressed,
|
|
572
|
+
status: 'Pattern-level shaping with per-step overrides',
|
|
573
|
+
onPress: function () {
|
|
574
|
+
return publishSequence(sequenceScore({
|
|
575
|
+
stepDuration: 210,
|
|
576
|
+
attack: 2,
|
|
577
|
+
release: 80,
|
|
578
|
+
volume: 0.48,
|
|
579
|
+
waveType: WAVE.NOISE,
|
|
580
|
+
noiseRate: 12000,
|
|
581
|
+
lowPassHz: 2600,
|
|
582
|
+
bitDepth: 8,
|
|
583
|
+
steps: [
|
|
584
|
+
{ frequency: 120, duration: 180, release: 150, noiseRateEnd: 800 },
|
|
585
|
+
{ rest: true, duration: 100 },
|
|
586
|
+
{
|
|
587
|
+
note: 'A3',
|
|
588
|
+
duration: 300,
|
|
589
|
+
release: 120,
|
|
590
|
+
waveType: WAVE.SQUARE,
|
|
591
|
+
pulseWidth: 0.1,
|
|
592
|
+
pulseWidthEnd: 0.82,
|
|
593
|
+
lowPassHz: 0,
|
|
594
|
+
bitDepth: 0
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
note: 'D4',
|
|
598
|
+
duration: 360,
|
|
599
|
+
release: 160,
|
|
600
|
+
waveType: WAVE.WAVETABLE,
|
|
601
|
+
waveTable: STEPPED_WAVE
|
|
602
|
+
}
|
|
603
|
+
]
|
|
604
|
+
}))
|
|
605
|
+
}
|
|
606
|
+
}))
|
|
607
|
+
retroSection.add(Ti.UI.createView({ height: dp(16) }))
|
|
608
|
+
scrollView.add(retroSection)
|
|
609
|
+
|
|
610
|
+
// ---------------------------------------------------------------------------
|
|
611
|
+
// 5. Harmony
|
|
612
|
+
// ---------------------------------------------------------------------------
|
|
613
|
+
|
|
614
|
+
var harmonySection = makeSection(
|
|
615
|
+
'Harmony',
|
|
616
|
+
'Atomic chords keep every voice aligned at the same onset.',
|
|
617
|
+
COLORS.blue
|
|
618
|
+
)
|
|
619
|
+
harmonySection.add(makeAction({
|
|
620
|
+
title: 'C MAJOR TRIAD\nSine · automatic stereo spread',
|
|
621
|
+
color: COLORS.blueDark,
|
|
622
|
+
pressedColor: COLORS.blue,
|
|
623
|
+
status: 'C major triad · sine',
|
|
624
|
+
onPress: function () {
|
|
625
|
+
return Synth.playChord({
|
|
626
|
+
frequencies: [261.63, 329.63, 392.0],
|
|
627
|
+
duration: 1200,
|
|
628
|
+
attack: 20,
|
|
629
|
+
release: 300,
|
|
630
|
+
volume: 0.7,
|
|
631
|
+
panSpread: 0.8
|
|
632
|
+
})
|
|
633
|
+
}
|
|
634
|
+
}))
|
|
635
|
+
harmonySection.add(makeAction({
|
|
636
|
+
title: 'C MAJOR · WIDE\nSquare · per-voice pans',
|
|
637
|
+
color: COLORS.indigo,
|
|
638
|
+
pressedColor: COLORS.indigoPressed,
|
|
639
|
+
status: 'C major chord · square and wide',
|
|
640
|
+
onPress: function () {
|
|
641
|
+
return Synth.playChord({
|
|
642
|
+
notes: ['C4', 'E4', 'G4', 'C5'],
|
|
643
|
+
duration: 1000,
|
|
644
|
+
attack: 10,
|
|
645
|
+
release: 200,
|
|
646
|
+
volume: 0.6,
|
|
647
|
+
waveType: WAVE.SQUARE,
|
|
648
|
+
pans: [-0.9, -0.3, 0.3, 0.9]
|
|
649
|
+
})
|
|
650
|
+
}
|
|
651
|
+
}))
|
|
652
|
+
harmonySection.add(Ti.UI.createView({ height: dp(16) }))
|
|
653
|
+
scrollView.add(harmonySection)
|
|
654
|
+
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
// 6. Sequencing
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
var sequenceSection = makeSection(
|
|
660
|
+
'Native sequencing',
|
|
661
|
+
'The first two demos are matched A/B tests; only the pitch input format changes.',
|
|
662
|
+
COLORS.yellow
|
|
663
|
+
)
|
|
664
|
+
sequenceSection.add(makeAction({
|
|
665
|
+
title: 'ARPEGGIO A/B · FREQUENCIES\nC major · square · 70% gain',
|
|
666
|
+
color: COLORS.green,
|
|
667
|
+
pressedColor: COLORS.greenPressed,
|
|
668
|
+
status: 'Matched arpeggio · frequencies',
|
|
669
|
+
onPress: function () {
|
|
670
|
+
return playMatchedArpeggio('frequency', [261.63, 329.63, 392.0, 523.25, 392.0, 329.63])
|
|
671
|
+
}
|
|
672
|
+
}))
|
|
673
|
+
sequenceSection.add(makeAction({
|
|
674
|
+
title: 'ARPEGGIO A/B · NOTE NAMES\nC4 · E4 · G4 · C5 · G4 · E4',
|
|
675
|
+
color: COLORS.green,
|
|
676
|
+
pressedColor: COLORS.greenPressed,
|
|
677
|
+
status: 'Matched arpeggio · note names',
|
|
678
|
+
onPress: function () {
|
|
679
|
+
return playMatchedArpeggio('note', ['C4', 'E4', 'G4', 'C5', 'G4', 'E4'])
|
|
680
|
+
}
|
|
681
|
+
}))
|
|
682
|
+
sequenceSection.add(makeAction({
|
|
683
|
+
title: 'MELODY PATTERN\n140 BPM · swing 0.1',
|
|
684
|
+
color: COLORS.yellow,
|
|
685
|
+
pressedColor: COLORS.yellowPressed,
|
|
686
|
+
textColor: COLORS.canvas,
|
|
687
|
+
status: 'Melody pattern · 140 BPM',
|
|
688
|
+
onPress: function () {
|
|
689
|
+
return publishSequence(sequenceScore({
|
|
690
|
+
bpm: 140,
|
|
691
|
+
swing: 0.1,
|
|
692
|
+
steps: [
|
|
693
|
+
{ note: 'C4', noteValue: '8n', volume: 0.8, waveType: WAVE.SQUARE },
|
|
694
|
+
{ note: 'E4', noteValue: '8n', volume: 0.7, waveType: WAVE.SQUARE },
|
|
695
|
+
{ note: 'G4', noteValue: '8n', volume: 0.7, waveType: WAVE.SQUARE },
|
|
696
|
+
{ note: 'C5', noteValue: '4n', volume: 0.9, waveType: WAVE.SQUARE },
|
|
697
|
+
{ rest: true, noteValue: '8n' },
|
|
698
|
+
{ note: 'Bb3', noteValue: '8n', volume: 0.7, waveType: WAVE.SQUARE },
|
|
699
|
+
{ note: 'G3', noteValue: '4n', volume: 0.8, waveType: WAVE.SQUARE }
|
|
700
|
+
]
|
|
701
|
+
}))
|
|
702
|
+
}
|
|
703
|
+
}))
|
|
704
|
+
sequenceSection.add(makeAction({
|
|
705
|
+
title: 'NOISE PERCUSSION\n120 BPM · eighths and sixteenths',
|
|
706
|
+
color: COLORS.brown,
|
|
707
|
+
pressedColor: COLORS.brownPressed,
|
|
708
|
+
status: 'Noise percussion · 120 BPM',
|
|
709
|
+
onPress: function () {
|
|
710
|
+
return publishSequence(sequenceScore({
|
|
711
|
+
bpm: 120,
|
|
712
|
+
steps: [
|
|
713
|
+
{ frequency: 80, noteValue: '8n', volume: 0.9, waveType: WAVE.NOISE, attack: 2, release: 60 },
|
|
714
|
+
{ rest: true, noteValue: '8n' },
|
|
715
|
+
{ frequency: 80, noteValue: '8n', volume: 0.7, waveType: WAVE.NOISE, attack: 2, release: 40 },
|
|
716
|
+
{ rest: true, noteValue: '8n' },
|
|
717
|
+
{ frequency: 80, noteValue: '8n', volume: 0.9, waveType: WAVE.NOISE, attack: 2, release: 60 },
|
|
718
|
+
{ rest: true, noteValue: '8n' },
|
|
719
|
+
{ frequency: 80, noteValue: '16n', volume: 0.7, waveType: WAVE.NOISE, attack: 2, release: 30 },
|
|
720
|
+
{ frequency: 80, noteValue: '16n', volume: 0.7, waveType: WAVE.NOISE, attack: 2, release: 30 }
|
|
721
|
+
]
|
|
722
|
+
}))
|
|
723
|
+
}
|
|
724
|
+
}))
|
|
725
|
+
sequenceSection.add(Ti.UI.createView({ height: dp(16) }))
|
|
726
|
+
scrollView.add(sequenceSection)
|
|
727
|
+
|
|
728
|
+
// ---------------------------------------------------------------------------
|
|
729
|
+
// 7. Expressive synthesis
|
|
730
|
+
// ---------------------------------------------------------------------------
|
|
731
|
+
|
|
732
|
+
var expressiveSection = makeSection(
|
|
733
|
+
'Expressive synthesis',
|
|
734
|
+
'Decay and sustain, a resonant filter, FM and detune.',
|
|
735
|
+
COLORS.teal
|
|
736
|
+
)
|
|
737
|
+
expressiveSection.add(makeAction({
|
|
738
|
+
title: 'PLUCK\nTriangle · decay 260 ms · sustain 0.18',
|
|
739
|
+
color: COLORS.green,
|
|
740
|
+
pressedColor: COLORS.greenPressed,
|
|
741
|
+
status: 'Pluck · decay and sustain',
|
|
742
|
+
onPress: function () {
|
|
743
|
+
return Synth.playTone({
|
|
744
|
+
note: 'E4',
|
|
745
|
+
duration: 900,
|
|
746
|
+
attack: 2,
|
|
747
|
+
decay: 260,
|
|
748
|
+
sustain: 0.18,
|
|
749
|
+
release: 380,
|
|
750
|
+
volume: 0.6,
|
|
751
|
+
waveType: WAVE.TRIANGLE
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
}))
|
|
755
|
+
expressiveSection.add(makeAction({
|
|
756
|
+
title: 'WAH\nSawtooth · band-pass 350 → 2400 Hz',
|
|
757
|
+
color: COLORS.orange,
|
|
758
|
+
pressedColor: COLORS.orangePressed,
|
|
759
|
+
status: 'Wah · band-pass cutoff sweep',
|
|
760
|
+
onPress: function () {
|
|
761
|
+
return Synth.playTone({
|
|
762
|
+
note: 'A2',
|
|
763
|
+
duration: 1000,
|
|
764
|
+
attack: 10,
|
|
765
|
+
release: 200,
|
|
766
|
+
volume: 0.5,
|
|
767
|
+
waveType: WAVE.SAWTOOTH,
|
|
768
|
+
filterType: 'bandpass',
|
|
769
|
+
filterHz: 350,
|
|
770
|
+
filterHzEnd: 2400,
|
|
771
|
+
filterQ: 5
|
|
772
|
+
})
|
|
773
|
+
}
|
|
774
|
+
}))
|
|
775
|
+
expressiveSection.add(makeAction({
|
|
776
|
+
title: 'BELL\nFM ratio 3.5 · index 4 → 0',
|
|
777
|
+
color: COLORS.indigo,
|
|
778
|
+
pressedColor: COLORS.indigoPressed,
|
|
779
|
+
status: 'Bell · FM index decays',
|
|
780
|
+
onPress: function () {
|
|
781
|
+
return Synth.playTone({
|
|
782
|
+
note: 'C5',
|
|
783
|
+
duration: 2600,
|
|
784
|
+
attack: 1,
|
|
785
|
+
decay: 2000,
|
|
786
|
+
sustain: 0.02,
|
|
787
|
+
release: 500,
|
|
788
|
+
volume: 0.5,
|
|
789
|
+
waveType: WAVE.SINE,
|
|
790
|
+
fmRatio: 3.5,
|
|
791
|
+
fmDepth: 4,
|
|
792
|
+
fmDepthEnd: 0
|
|
793
|
+
})
|
|
794
|
+
}
|
|
795
|
+
}))
|
|
796
|
+
expressiveSection.add(makeAction({
|
|
797
|
+
title: 'ELECTRIC PIANO\nFM ratio 1 · index 2.2 → 0.25',
|
|
798
|
+
color: COLORS.blueDark,
|
|
799
|
+
pressedColor: COLORS.blue,
|
|
800
|
+
status: 'Electric piano · FM index mellows',
|
|
801
|
+
onPress: function () {
|
|
802
|
+
return Synth.playTone({
|
|
803
|
+
note: 'E3',
|
|
804
|
+
duration: 1800,
|
|
805
|
+
attack: 3,
|
|
806
|
+
decay: 700,
|
|
807
|
+
sustain: 0.3,
|
|
808
|
+
release: 600,
|
|
809
|
+
volume: 0.55,
|
|
810
|
+
waveType: WAVE.SINE,
|
|
811
|
+
fmRatio: 1,
|
|
812
|
+
fmDepth: 2.2,
|
|
813
|
+
fmDepthEnd: 0.25
|
|
814
|
+
})
|
|
815
|
+
}
|
|
816
|
+
}))
|
|
817
|
+
expressiveSection.add(makeAction({
|
|
818
|
+
title: 'THICK CHORD\nSix sawtooth voices · detunes ±9 cents',
|
|
819
|
+
color: COLORS.pink,
|
|
820
|
+
pressedColor: COLORS.pinkPressed,
|
|
821
|
+
status: 'Thick chord · detuned voices',
|
|
822
|
+
onPress: function () {
|
|
823
|
+
return Synth.playChord({
|
|
824
|
+
notes: ['A3', 'E4', 'A3', 'E4', 'A3', 'E4'],
|
|
825
|
+
detunes: [-9, -9, 0, 0, 9, 9],
|
|
826
|
+
duration: 3200,
|
|
827
|
+
attack: 700,
|
|
828
|
+
release: 1300,
|
|
829
|
+
volume: 0.5,
|
|
830
|
+
waveType: WAVE.SAWTOOTH,
|
|
831
|
+
filterType: 'lowpass',
|
|
832
|
+
filterHz: 1500,
|
|
833
|
+
panSpread: 0.8
|
|
834
|
+
})
|
|
835
|
+
}
|
|
836
|
+
}))
|
|
837
|
+
|
|
838
|
+
var patternEndLabel = makeText('sequenceend: no pattern yet', {
|
|
839
|
+
top: dp(8),
|
|
840
|
+
left: dp(16),
|
|
841
|
+
right: dp(16),
|
|
842
|
+
color: COLORS.textSecondary,
|
|
843
|
+
fontSize: 12
|
|
844
|
+
})
|
|
845
|
+
|
|
846
|
+
function onSequenceEnd(event) {
|
|
847
|
+
patternEndLabel.text = event.reason === 'completed'
|
|
848
|
+
? 'sequenceend: completed'
|
|
849
|
+
: 'sequenceend: stopped'
|
|
850
|
+
setStatus(patternEndLabel.text, event.reason === 'completed' ? COLORS.green : COLORS.yellow)
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
Synth.addEventListener('sequenceend', onSequenceEnd)
|
|
854
|
+
|
|
855
|
+
expressiveSection.add(makeAction({
|
|
856
|
+
title: 'PATTERN END\nTap once to finish; tap again quickly to replace',
|
|
857
|
+
color: COLORS.yellow,
|
|
858
|
+
pressedColor: COLORS.yellowPressed,
|
|
859
|
+
textColor: COLORS.canvas,
|
|
860
|
+
status: 'Pattern running · waiting for sequenceend',
|
|
861
|
+
onPress: function () {
|
|
862
|
+
patternEndLabel.text = 'sequenceend: pattern running'
|
|
863
|
+
return publishSequence(sequenceScore({
|
|
864
|
+
bpm: 132,
|
|
865
|
+
noteValue: '8n',
|
|
866
|
+
attack: 3,
|
|
867
|
+
decay: 90,
|
|
868
|
+
sustain: 0.4,
|
|
869
|
+
release: 60,
|
|
870
|
+
volume: 0.55,
|
|
871
|
+
waveType: WAVE.SQUARE,
|
|
872
|
+
steps: [
|
|
873
|
+
{ note: 'C5' },
|
|
874
|
+
{ note: 'E5' },
|
|
875
|
+
{ note: 'G5' },
|
|
876
|
+
{ rest: true },
|
|
877
|
+
{ note: 'E5' },
|
|
878
|
+
{ note: 'C5', noteValue: '4n' }
|
|
879
|
+
]
|
|
880
|
+
}))
|
|
881
|
+
}
|
|
882
|
+
}))
|
|
883
|
+
expressiveSection.add(patternEndLabel)
|
|
884
|
+
expressiveSection.add(Ti.UI.createView({ height: dp(16) }))
|
|
885
|
+
scrollView.add(expressiveSection)
|
|
886
|
+
|
|
887
|
+
// ---------------------------------------------------------------------------
|
|
888
|
+
// 8. Headphone positioning
|
|
889
|
+
// ---------------------------------------------------------------------------
|
|
890
|
+
|
|
891
|
+
var spatialSection = makeSection(
|
|
892
|
+
'Binaural headphone positioning',
|
|
893
|
+
'Use stereo headphones. Compare one noise burst at a time; elevation varies by listener. These controls affect this section only.',
|
|
894
|
+
COLORS.teal
|
|
895
|
+
)
|
|
896
|
+
var spatialModeLabel = makeText('Mode: binaural (front plane)', {
|
|
897
|
+
left: dp(16), right: dp(16), top: dp(8), fontSize: 12
|
|
898
|
+
})
|
|
899
|
+
var spatialModes = makeRow()
|
|
900
|
+
;[true, false].forEach(function (enabled) {
|
|
901
|
+
spatialModes.add(makeGridAction({
|
|
902
|
+
title: enabled ? 'BINAURAL MODE' : 'STEREO MODE',
|
|
903
|
+
color: COLORS.teal, pressedColor: COLORS.tealPressed,
|
|
904
|
+
accessibilityHint: 'Changes placement for new sounds in this section',
|
|
905
|
+
onPress: function () {
|
|
906
|
+
binauralEnabled = enabled
|
|
907
|
+
spatialModeLabel.text = enabled ? 'Mode: binaural (front plane)' : 'Mode: stereo (horizontal only)'
|
|
908
|
+
}
|
|
909
|
+
}))
|
|
910
|
+
})
|
|
911
|
+
spatialSection.add(spatialModes)
|
|
912
|
+
spatialSection.add(spatialModeLabel)
|
|
913
|
+
|
|
914
|
+
function placement(x, y) {
|
|
915
|
+
// Never mix spatial coordinates with pan, panEnd, pans or panSpread.
|
|
916
|
+
return binauralEnabled ? { spatialX: x, spatialY: y } : { pan: x, panEnd: x }
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
var spatialPoints = [
|
|
920
|
+
['TOP LEFT', -1, 1], ['TOP', 0, 1], ['TOP RIGHT', 1, 1],
|
|
921
|
+
['LEFT', -1, 0], ['CENTER', 0, 0], ['RIGHT', 1, 0],
|
|
922
|
+
['BOTTOM LEFT', -1, -1], ['BOTTOM', 0, -1], ['BOTTOM RIGHT', 1, -1]
|
|
923
|
+
]
|
|
924
|
+
var spatialRow
|
|
925
|
+
spatialPoints.forEach(function (point, index) {
|
|
926
|
+
if (index % 3 === 0) { spatialRow = makeRow(); spatialSection.add(spatialRow) }
|
|
927
|
+
spatialRow.add(makeCompactAction({
|
|
928
|
+
title: point[0] + '\nNoise burst',
|
|
929
|
+
fontSize: 11, color: COLORS.teal, pressedColor: COLORS.tealPressed,
|
|
930
|
+
onPress: function () {
|
|
931
|
+
return Synth.playTone(Object.assign({
|
|
932
|
+
frequency: 200, waveType: WAVE.NOISE, duration: 550,
|
|
933
|
+
attack: 5, release: 200, volume: 0.3
|
|
934
|
+
}, placement(point[1], point[2])))
|
|
935
|
+
}
|
|
936
|
+
}))
|
|
937
|
+
})
|
|
938
|
+
spatialSection.add(makeAction({
|
|
939
|
+
title: 'SPATIAL CHORD\nOne fixed position above and right',
|
|
940
|
+
color: COLORS.blueDark, pressedColor: COLORS.blue,
|
|
941
|
+
onPress: function () {
|
|
942
|
+
var position = binauralEnabled ? { spatialX: 0.7, spatialY: 1 } : { panSpread: 0, pans: [0.7, 0.7, 0.7] }
|
|
943
|
+
return Synth.playChord(Object.assign({
|
|
944
|
+
notes: ['C4', 'E4', 'G4'], duration: 1200, attack: 15, release: 300,
|
|
945
|
+
volume: 0.4, waveType: WAVE.SAWTOOTH
|
|
946
|
+
}, position))
|
|
947
|
+
}
|
|
948
|
+
}))
|
|
949
|
+
spatialSection.add(makeAction({
|
|
950
|
+
title: 'SPATIAL PATTERN\nAbove left, below center, above right',
|
|
951
|
+
color: COLORS.indigo, pressedColor: COLORS.indigoPressed,
|
|
952
|
+
onPress: function () {
|
|
953
|
+
return publishSequence(sequenceScore({
|
|
954
|
+
stepDuration: 550, attack: 5, decay: 180, sustain: 0, release: 200,
|
|
955
|
+
volume: 0.3, waveType: WAVE.NOISE,
|
|
956
|
+
steps: [[-1, 1], [0, -1], [1, 1]].map(function (point) {
|
|
957
|
+
return Object.assign({ frequency: 200 }, placement(point[0], point[1]))
|
|
958
|
+
})
|
|
959
|
+
}))
|
|
960
|
+
}
|
|
961
|
+
}))
|
|
962
|
+
spatialSection.add(Ti.UI.createView({ height: dp(16) }))
|
|
963
|
+
scrollView.add(spatialSection)
|
|
964
|
+
|
|
965
|
+
// ---------------------------------------------------------------------------
|
|
966
|
+
// 9. Session policy and statistics
|
|
967
|
+
// ---------------------------------------------------------------------------
|
|
968
|
+
|
|
969
|
+
var sessionSection = makeSection(
|
|
970
|
+
'Audio session',
|
|
971
|
+
'On iOS, playback ignores silent mode; ambient respects it and mixes with other apps. Changing policy stops sounds and patterns, then restarts. Android accepts both without changing its output.',
|
|
972
|
+
COLORS.blue
|
|
973
|
+
)
|
|
974
|
+
var sessionLabel = makeText('Session: playback', { left: dp(16), right: dp(16), top: dp(8), fontSize: 12 })
|
|
975
|
+
;['playback', 'ambient'].forEach(function (session) {
|
|
976
|
+
sessionSection.add(makeAction({
|
|
977
|
+
title: session.toUpperCase() + ' SESSION',
|
|
978
|
+
allowStopped: true,
|
|
979
|
+
color: COLORS.blueDark, pressedColor: COLORS.blue,
|
|
980
|
+
accessibilityHint: 'Restarts the engine with this session policy',
|
|
981
|
+
onPress: function () {
|
|
982
|
+
sequenceOwner.reset(); Synth.shutdown()
|
|
983
|
+
engineRunning = false
|
|
984
|
+
selectedSession = session
|
|
985
|
+
patternEndLabel.text = 'sequenceend: cancelled by shutdown (no event)'
|
|
986
|
+
return startAppCycles()
|
|
987
|
+
}
|
|
988
|
+
}))
|
|
989
|
+
})
|
|
990
|
+
sessionSection.add(sessionLabel)
|
|
991
|
+
sessionSection.add(Ti.UI.createView({ height: dp(16) }))
|
|
992
|
+
scrollView.add(sessionSection)
|
|
993
|
+
|
|
994
|
+
var statsSection = makeSection(
|
|
995
|
+
'Engine statistics',
|
|
996
|
+
'Read counters without changing them. Optional diagnostics poll at 5 Hz; n/a means unmeasured or unavailable, not zero. Render load is not total app CPU.',
|
|
997
|
+
COLORS.orange
|
|
998
|
+
)
|
|
999
|
+
var statsLabel = makeText('Diagnostics: off. Tap READ STATS for basic counters.', {
|
|
1000
|
+
left: dp(16), right: dp(16), top: dp(8), fontSize: 12
|
|
1001
|
+
})
|
|
1002
|
+
function measured(value, suffix) {
|
|
1003
|
+
return typeof value === 'number' && isFinite(value) ? value.toFixed(2) + (suffix || '') : 'n/a'
|
|
1004
|
+
}
|
|
1005
|
+
function refreshStats() {
|
|
1006
|
+
var stats = Synth.getStats()
|
|
1007
|
+
statsLabel.text = 'Diagnostics: ' + (diagnosticsEnabled ? 'on' : 'off')
|
|
1008
|
+
+ '\nVoices: ' + stats.activeVoices + '/' + stats.maxVoices + ' (peak ' + stats.peakActiveVoices + ')'
|
|
1009
|
+
+ '\nSpatial: ' + stats.spatialVoices + ' | tails: ' + stats.tailVoices + ' | queued: ' + stats.pendingCommands
|
|
1010
|
+
+ '\nReplaced: ' + stats.voiceStealCount + ' | requests rejected: ' + stats.rejectedRequestCount
|
|
1011
|
+
+ '\nPublications rejected: ' + stats.rejectedPublicationCount + ' | priority drops: ' + stats.priorityDropCount
|
|
1012
|
+
+ '\nRender load: ' + (stats.renderLoad == null ? 'n/a' : measured(stats.renderLoad * 100, '%'))
|
|
1013
|
+
+ '\nPeak L/R: ' + measured(stats.outputPeakDbFS && stats.outputPeakDbFS.left) + '/' + measured(stats.outputPeakDbFS && stats.outputPeakDbFS.right) + ' dBFS'
|
|
1014
|
+
+ '\nBackend underruns: ' + measured(stats.underrunCount)
|
|
1015
|
+
}
|
|
1016
|
+
function stopStatsPolling() {
|
|
1017
|
+
if (statsTimer !== null) clearInterval(statsTimer)
|
|
1018
|
+
statsTimer = null
|
|
1019
|
+
}
|
|
1020
|
+
statsSection.add(makeAction({
|
|
1021
|
+
title: 'READ STATS', color: COLORS.surfaceMuted, pressedColor: COLORS.surfaceRaised,
|
|
1022
|
+
accessibilityHint: 'Reads statistics without resetting them',
|
|
1023
|
+
onPress: refreshStats
|
|
1024
|
+
}))
|
|
1025
|
+
statsSection.add(makeAction({
|
|
1026
|
+
title: 'TOGGLE DIAGNOSTICS', color: COLORS.orange, pressedColor: COLORS.orangePressed,
|
|
1027
|
+
accessibilityHint: 'Enables or disables timing and signal measurements',
|
|
1028
|
+
onPress: function () {
|
|
1029
|
+
if (!Synth.setStatsEnabled(!diagnosticsEnabled)) return false
|
|
1030
|
+
diagnosticsEnabled = !diagnosticsEnabled
|
|
1031
|
+
stopStatsPolling()
|
|
1032
|
+
if (diagnosticsEnabled) statsTimer = setInterval(refreshStats, 200)
|
|
1033
|
+
refreshStats()
|
|
1034
|
+
return true
|
|
1035
|
+
}
|
|
1036
|
+
}))
|
|
1037
|
+
statsSection.add(makeAction({
|
|
1038
|
+
title: 'RESET STATS', color: COLORS.surfaceMuted, pressedColor: COLORS.surfaceRaised,
|
|
1039
|
+
accessibilityHint: 'Resets counters without stopping sounds or changing diagnostics',
|
|
1040
|
+
onPress: function () { Synth.resetStats(); refreshStats() }
|
|
1041
|
+
}))
|
|
1042
|
+
statsSection.add(statsLabel)
|
|
1043
|
+
statsSection.add(Ti.UI.createView({ height: dp(16) }))
|
|
1044
|
+
scrollView.add(statsSection)
|
|
1045
|
+
|
|
1046
|
+
// ---------------------------------------------------------------------------
|
|
1047
|
+
// 10. Output controls
|
|
1048
|
+
// ---------------------------------------------------------------------------
|
|
1049
|
+
|
|
1050
|
+
var controlSection = makeSection(
|
|
1051
|
+
'Output',
|
|
1052
|
+
'Set the master gain or stop every active voice.',
|
|
1053
|
+
COLORS.red
|
|
1054
|
+
)
|
|
1055
|
+
var volumeRow = makeRow()
|
|
1056
|
+
var volumeValues = [0.2, 0.5, 1.0]
|
|
1057
|
+
volumeValues.forEach(function (volume) {
|
|
1058
|
+
volumeRow.add(makeCompactAction({
|
|
1059
|
+
title: Math.round(volume * 100) + '%',
|
|
1060
|
+
color: volume === 1.0 ? COLORS.blueDark : COLORS.surfaceMuted,
|
|
1061
|
+
pressedColor: volume === 1.0 ? COLORS.blue : COLORS.surfaceRaised,
|
|
1062
|
+
status: 'Master volume · ' + Math.round(volume * 100) + '%',
|
|
1063
|
+
accessibilityLabel: 'Set master volume to ' + Math.round(volume * 100) + ' percent',
|
|
1064
|
+
onPress: function () {
|
|
1065
|
+
return setMasterVolume(volume)
|
|
1066
|
+
}
|
|
1067
|
+
}))
|
|
1068
|
+
})
|
|
1069
|
+
controlSection.add(volumeRow)
|
|
1070
|
+
|
|
1071
|
+
var boostRow = makeRow()
|
|
1072
|
+
boostRow.add(makeGridAction({
|
|
1073
|
+
title: 'BOOST\n150%',
|
|
1074
|
+
color: COLORS.orange,
|
|
1075
|
+
pressedColor: COLORS.orangePressed,
|
|
1076
|
+
status: 'Master volume · 150%',
|
|
1077
|
+
accessibilityLabel: 'Set master volume to 150 percent',
|
|
1078
|
+
onPress: function () {
|
|
1079
|
+
return setMasterVolume(1.5)
|
|
1080
|
+
}
|
|
1081
|
+
}))
|
|
1082
|
+
boostRow.add(makeGridAction({
|
|
1083
|
+
title: 'STOP ALL\nRelease active voices',
|
|
1084
|
+
color: COLORS.redDark,
|
|
1085
|
+
pressedColor: COLORS.red,
|
|
1086
|
+
status: 'Active voices fading; pending pattern steps remain',
|
|
1087
|
+
accessibilityHint: 'Fades current voices without cancelling future pattern steps',
|
|
1088
|
+
onPress: function () {
|
|
1089
|
+
sequenceOwner.stop(); Synth.stopAll()
|
|
1090
|
+
}
|
|
1091
|
+
}))
|
|
1092
|
+
controlSection.add(boostRow)
|
|
1093
|
+
controlSection.add(Ti.UI.createView({ height: dp(12) }))
|
|
1094
|
+
scrollView.add(controlSection)
|
|
1095
|
+
|
|
1096
|
+
scrollView.add(makeText('Built with the same public API on Android and iOS.', {
|
|
1097
|
+
top: dp(24),
|
|
1098
|
+
bottom: dp(32),
|
|
1099
|
+
color: COLORS.textMuted,
|
|
1100
|
+
fontSize: 12,
|
|
1101
|
+
textAlign: Ti.UI.TEXT_ALIGNMENT_CENTER
|
|
1102
|
+
}))
|
|
1103
|
+
|
|
1104
|
+
win.add(scrollView)
|
|
1105
|
+
|
|
1106
|
+
// ---------------------------------------------------------------------------
|
|
1107
|
+
// Engine lifecycle
|
|
1108
|
+
// ---------------------------------------------------------------------------
|
|
1109
|
+
|
|
1110
|
+
function setMasterVolume(volume) {
|
|
1111
|
+
if (!Synth.setVolume({ volume: volume })) return false
|
|
1112
|
+
masterVolume = volume
|
|
1113
|
+
return true
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function startAppCycles() {
|
|
1117
|
+
startTimer = null
|
|
1118
|
+
win.removeEventListener('postlayout', onFirstPostLayout)
|
|
1119
|
+
engineRunning = Synth.start({ maxVoices: 16, mixingProfile: 'conservative', audioSession: selectedSession })
|
|
1120
|
+
// Keep session controls available for retry if a native stream cannot open.
|
|
1121
|
+
scrollView.touchEnabled = true
|
|
1122
|
+
interactiveControls.forEach(function (control) {
|
|
1123
|
+
var enabled = engineRunning || control.allowStopped
|
|
1124
|
+
control.view.applyProperties({ opacity: enabled ? 1 : 0.58, touchEnabled: enabled })
|
|
1125
|
+
})
|
|
1126
|
+
sessionLabel.text = 'Session: ' + selectedSession + (engineRunning ? '' : ' (start failed; tap a session to retry)')
|
|
1127
|
+
if (!engineRunning) {
|
|
1128
|
+
setStatus('Engine start failed; select a session to retry', COLORS.red)
|
|
1129
|
+
return false
|
|
1130
|
+
}
|
|
1131
|
+
if (!setMasterVolume(masterVolume)) {
|
|
1132
|
+
setStatus('Request rejected · master volume was not restored', COLORS.red)
|
|
1133
|
+
return false
|
|
1134
|
+
}
|
|
1135
|
+
setStatus('Audio ready · tap a demo', COLORS.green)
|
|
1136
|
+
refreshStats()
|
|
1137
|
+
return true
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function onFirstPostLayout() {
|
|
1141
|
+
if (startTimer !== null) clearTimeout(startTimer)
|
|
1142
|
+
startTimer = setTimeout(startAppCycles, 250)
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function hideAndroidActionBar() {
|
|
1146
|
+
if (Ti.Platform.osname !== 'android') return
|
|
1147
|
+
|
|
1148
|
+
var activity = win.activity || Ti.Android.currentActivity
|
|
1149
|
+
if (activity && activity.actionBar) activity.actionBar.hide()
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function cleanup() {
|
|
1153
|
+
stopStatsPolling()
|
|
1154
|
+
Synth.setStatsEnabled(false)
|
|
1155
|
+
diagnosticsEnabled = false
|
|
1156
|
+
Synth.removeEventListener('sequenceend', onSequenceEnd)
|
|
1157
|
+
win.removeEventListener('postlayout', onFirstPostLayout)
|
|
1158
|
+
win.removeEventListener('open', hideAndroidActionBar)
|
|
1159
|
+
win.removeEventListener('focus', hideAndroidActionBar)
|
|
1160
|
+
win.removeEventListener('close', cleanup)
|
|
1161
|
+
|
|
1162
|
+
if (startTimer !== null) {
|
|
1163
|
+
clearTimeout(startTimer)
|
|
1164
|
+
startTimer = null
|
|
1165
|
+
}
|
|
1166
|
+
if (engineRunning) {
|
|
1167
|
+
sequenceOwner.reset(); Synth.shutdown()
|
|
1168
|
+
engineRunning = false
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
win.addEventListener('postlayout', onFirstPostLayout)
|
|
1173
|
+
if (Ti.Platform.osname === 'android') {
|
|
1174
|
+
win.addEventListener('open', hideAndroidActionBar)
|
|
1175
|
+
win.addEventListener('focus', hideAndroidActionBar)
|
|
1176
|
+
}
|
|
1177
|
+
win.addEventListener('close', cleanup)
|
|
1178
|
+
win.open()
|