@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.
Files changed (98) hide show
  1. package/CONTRIBUTING.md +69 -0
  2. package/README.md +112 -72
  3. package/agents/ti-pro.md +5 -1
  4. package/commands/ti-audit.md +5 -1
  5. package/docs/examples.md +91 -0
  6. package/docs/knowledge-index.md +57 -0
  7. package/lib/config.js +2 -0
  8. package/lib/downloader.js +1 -1
  9. package/package.json +4 -2
  10. package/skills/purgetss/references/multi-density-images.md +1 -1
  11. package/skills/ti-admob-unified/SKILL.md +58 -0
  12. package/skills/ti-admob-unified/agents/openai.yaml +4 -0
  13. package/skills/ti-admob-unified/assets/example/app.js +1207 -0
  14. package/skills/ti-admob-unified/assets/example/lib/ads.js +646 -0
  15. package/skills/ti-admob-unified/assets/example/library-usage.js +136 -0
  16. package/skills/ti-admob-unified/references/api-reference.md +615 -0
  17. package/skills/ti-admob-unified/references/banners.md +435 -0
  18. package/skills/ti-admob-unified/references/contributing.md +233 -0
  19. package/skills/ti-admob-unified/references/dependencies.md +91 -0
  20. package/skills/ti-admob-unified/references/fullscreen.md +238 -0
  21. package/skills/ti-admob-unified/references/game-monetization.md +148 -0
  22. package/skills/ti-admob-unified/references/getting-started.md +319 -0
  23. package/skills/ti-admob-unified/references/index.md +33 -0
  24. package/skills/ti-admob-unified/references/integration.md +87 -0
  25. package/skills/ti-admob-unified/references/library.md +220 -0
  26. package/skills/ti-admob-unified/references/mediation-parity.md +17 -0
  27. package/skills/ti-admob-unified/references/mediation.md +145 -0
  28. package/skills/ti-admob-unified/references/migration.md +259 -0
  29. package/skills/ti-admob-unified/references/module-changelog.md +84 -0
  30. package/skills/ti-admob-unified/references/module-license.txt +21 -0
  31. package/skills/ti-admob-unified/references/privacy.md +281 -0
  32. package/skills/ti-admob-unified/references/production.md +67 -0
  33. package/skills/ti-admob-unified/references/rewarded-interstitial.md +101 -0
  34. package/skills/ti-admob-unified/references/roadmap.md +143 -0
  35. package/skills/ti-admob-unified/references/source-notes.md +80 -0
  36. package/skills/ti-admob-unified/references/source-snapshot.json +644 -0
  37. package/skills/ti-admob-unified/references/third-party-notices.md +22 -0
  38. package/skills/ti-game/SKILL.md +31 -5
  39. package/skills/ti-game/references/api.md +36 -24
  40. package/skills/ti-game/references/debugging-performance.md +22 -7
  41. package/skills/ti-game/references/examples.md +113 -20
  42. package/skills/ti-game/references/input.md +101 -0
  43. package/skills/ti-game/references/patterns.md +56 -10
  44. package/skills/ti-game/references/physics.md +530 -0
  45. package/skills/ti-game/references/project-setup.md +131 -47
  46. package/skills/ti-game/references/recipes-action.md +149 -18
  47. package/skills/ti-game/references/recipes-adventure-effects.md +47 -4
  48. package/skills/ti-game/references/roadmap.md +11 -4
  49. package/skills/ti-game/references/source-notes.md +81 -0
  50. package/skills/ti-game/references/tilemaps.md +13 -4
  51. package/skills/ti-game-assets/SKILL.md +53 -0
  52. package/skills/ti-game-assets/agents/openai.yaml +4 -0
  53. package/skills/ti-game-assets/references/alpha.md +26 -0
  54. package/skills/ti-game-assets/references/art-direction.md +37 -0
  55. package/skills/ti-game-assets/references/integration.md +49 -0
  56. package/skills/ti-game-assets/references/manifest.example.json +429 -0
  57. package/skills/ti-game-assets/references/manifest.md +39 -0
  58. package/skills/ti-game-assets/references/offline-audio.md +33 -0
  59. package/skills/ti-game-assets/references/prompts.md +33 -0
  60. package/skills/ti-game-assets/references/repeating-textures.md +17 -0
  61. package/skills/ti-game-assets/references/responsible-ads.md +31 -0
  62. package/skills/ti-game-assets/references/sound-assets.md +42 -0
  63. package/skills/ti-game-assets/references/vector-sources.md +21 -0
  64. package/skills/ti-game-assets/scripts/alpha_tools.py +160 -0
  65. package/skills/ti-game-assets/scripts/requirements.txt +1 -0
  66. package/skills/ti-game-assets/scripts/validate_manifest.py +267 -0
  67. package/skills/ti-synthengine/SKILL.md +77 -67
  68. package/skills/ti-synthengine/assets/example/README.md +84 -0
  69. package/skills/ti-synthengine/assets/example/app.js +1178 -0
  70. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +83 -0
  71. package/skills/ti-synthengine/assets/example/lib/sequenceScore.js +25 -0
  72. package/skills/ti-synthengine/references/advanced-examples.md +174 -0
  73. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  74. package/skills/ti-synthengine/references/android-sim80-correction.json +162 -0
  75. package/skills/ti-synthengine/references/api.md +573 -153
  76. package/skills/ti-synthengine/references/audio-engine.md +272 -124
  77. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  78. package/skills/ti-synthengine/references/examples.md +355 -106
  79. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  80. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  81. package/skills/ti-synthengine/references/frontier-physical-validation.json +1918 -0
  82. package/skills/ti-synthengine/references/guide.md +426 -75
  83. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  84. package/skills/ti-synthengine/references/performance-review.json +1146 -0
  85. package/skills/ti-synthengine/references/physical-validation.json +1457 -0
  86. package/skills/ti-synthengine/references/recipes-interaction.md +549 -0
  87. package/skills/ti-synthengine/references/recipes.md +434 -420
  88. package/skills/ti-synthengine/references/recommendations.md +72 -85
  89. package/skills/ti-synthengine/references/sound-design.md +375 -155
  90. package/skills/ti-synthengine/references/source-notes.md +109 -0
  91. package/skills/ti-synthengine/references/source-snapshot.json +452 -0
  92. package/skills/ti-synthengine/references/stats-performance.json +2992 -0
  93. package/skills/ti-synthengine/references/stats-performance.md +82 -0
  94. package/skills/ti-synthengine/references/studio-validation.json +6202 -0
  95. package/skills/ti-synthengine/references/troubleshooting.md +123 -78
  96. package/skills/ti-synthengine/references/unified-bypass-performance.json +22425 -0
  97. package/skills/ti-synthengine/references/unified-validation.json +314 -0
  98. 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()