@maccesar/titools 5.2.4 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +27 -32
  2. package/agents/ti-pro.md +1 -1
  3. package/commands/ti-audit.md +1 -1
  4. package/docs/examples.md +15 -8
  5. package/package.json +1 -1
  6. package/skills/ti-synthengine/SKILL.md +62 -92
  7. package/skills/ti-synthengine/assets/example/README.md +51 -54
  8. package/skills/ti-synthengine/assets/example/app.js +96 -57
  9. package/skills/ti-synthengine/assets/example/lib/sequenceOwner.js +39 -40
  10. package/skills/ti-synthengine/assets/example/lib/soundPreset.js +34 -0
  11. package/skills/ti-synthengine/assets/index.d.ts +226 -0
  12. package/skills/ti-synthengine/references/advanced-examples.md +12 -12
  13. package/skills/ti-synthengine/references/api.md +125 -33
  14. package/skills/ti-synthengine/references/audio-engine.md +24 -59
  15. package/skills/ti-synthengine/references/examples.md +64 -7
  16. package/skills/ti-synthengine/references/guide.md +69 -63
  17. package/skills/ti-synthengine/references/instruments-and-effects.md +34 -27
  18. package/skills/ti-synthengine/references/native-variation.md +92 -51
  19. package/skills/ti-synthengine/references/recipes-interaction.md +26 -19
  20. package/skills/ti-synthengine/references/recipes.md +19 -15
  21. package/skills/ti-synthengine/references/recommendations.md +14 -14
  22. package/skills/ti-synthengine/references/sound-api.md +197 -0
  23. package/skills/ti-synthengine/references/sound-design.md +11 -11
  24. package/skills/ti-synthengine/references/source-notes.md +62 -147
  25. package/skills/ti-synthengine/references/troubleshooting.md +22 -24
  26. package/skills/ti-synthengine/references/upgrade-2.0.0.md +40 -0
  27. package/skills/ti-synthengine/references/android-sim80-correction-evidence.zip +0 -0
  28. package/skills/ti-synthengine/references/android-sim80-correction.json +0 -162
  29. package/skills/ti-synthengine/references/audio-pipeline-validation.md +0 -19
  30. package/skills/ti-synthengine/references/bypass-physical-recheck-evidence.zip +0 -0
  31. package/skills/ti-synthengine/references/frontier-android-evidence.zip +0 -0
  32. package/skills/ti-synthengine/references/frontier-ipad-evidence.zip +0 -0
  33. package/skills/ti-synthengine/references/frontier-physical-validation.json +0 -1918
  34. package/skills/ti-synthengine/references/native-variation-proposal.md +0 -182
  35. package/skills/ti-synthengine/references/native-variation-validation.md +0 -63
  36. package/skills/ti-synthengine/references/performance-review-evidence.zip +0 -0
  37. package/skills/ti-synthengine/references/performance-review.json +0 -1146
  38. package/skills/ti-synthengine/references/physical-validation.json +0 -1457
  39. package/skills/ti-synthengine/references/source-snapshot.json +0 -463
  40. package/skills/ti-synthengine/references/stats-performance.json +0 -2992
  41. package/skills/ti-synthengine/references/stats-performance.md +0 -82
  42. package/skills/ti-synthengine/references/studio-validation.json +0 -6202
  43. package/skills/ti-synthengine/references/unified-bypass-performance.json +0 -22425
  44. package/skills/ti-synthengine/references/unified-validation.json +0 -314
@@ -1,91 +1,88 @@
1
- > Source snapshot: official TiSynthEngine documentation at commit `b07c97c762be5905f132cecf33e9d60ea0c21316` (2026-11-02). Published 1.0.2; local 1.1.0 candidates; public API 1.0.0. Upstream `example/README.md` SHA-256: `7906f208b287d31ac400f407f4f3509b7a12041274a4629ab287eea235954518`.
1
+ > Source snapshot: official TiSynthEngine documentation at commit `9d74aa45f84d3f2822d4aab76874efe37d81257f` (2026-11-03). Published module 2.0.0. Upstream `example/README.md` SHA-256: `db0f5921699859a1dd6918f215a4d30b008dd933cae458f21033216d7f807dd0`.
2
2
 
3
3
  # Interactive example
4
4
 
5
- The Classic Titanium app in [app.js](app.js) demonstrates the timed sounds, native sequences and statistics with the `sequenceend` event on Android and iOS. It runs with the published 1.0.2 module packages, including binaural and statistics support, or the unpublished local 1.1.0 candidates for native attack variation. Held voices, master processing and voice LFOs have copyable examples in the [advanced documentation](../../references/advanced-examples.md#sustained-game-motor); this reference app does not expose those newer controls. The default engine configuration is 16 voices, `conservative` mixing and the `playback` session; master volume starts at 100% and optional diagnostics start off.
5
+ The Classic Titanium app in [app.js](app.js) demonstrates the Sound object API on Android and iOS. It requires TiSynth 2.0.0, reported by `getContract().version`. `getDefaults().version` reports the same installed module version.
6
+
7
+ The default configuration is 16 voices, `conservative` mixing and the `playback` session. Master volume starts at 100% and optional diagnostics start off. Creating a Sound does not reserve a voice.
6
8
 
7
9
  ## Run it
8
10
 
9
- 1. Create a Classic Titanium application with Android and/or iOS support. If the app uses Alloy, create a separate Classic test project and leave the Alloy-generated files in place.
10
- 2. Download the official [Android 1.0.2](https://github.com/macCesar/TiSynthEngine/releases/tag/android-1.0.2) and [iOS 1.0.2](https://github.com/macCesar/TiSynthEngine/releases/tag/ios-1.0.2) packages, or build the current module source. Source builds now produce local `android/dist/ti.synthengine-android-1.1.0.zip` and `ios/dist/ti.synthengine-iphone-1.1.0.zip` candidates; select version `1.1.0` in both module entries when using them.
11
- 3. Put the archive for each target platform in the test app root. For the published packages, add these entries inside its `tiapp.xml`:
11
+ 1. Create a Classic Titanium application with Android and/or iOS support. For Alloy projects, keep the example in a separate Classic test app so the Alloy-generated files remain intact.
12
+ 2. Build and install the current module source for the target platforms. Select the version declared by that build in `tiapp.xml`; rebuilding and reinstalling the test app is necessary after changing module packages.
13
+ 3. Copy the entire `example/` directory into the app's `Resources/`, including `app.js` and every file under `lib/`. No images or audio files are needed.
14
+ 4. Run with Titanium SDK 13.2.0.GA or newer, Android compile API 35 or newer, and iOS 15 or newer. See the [installation guide](../../references/guide.md#install-the-module).
12
15
 
13
- ```xml
14
- <modules>
15
- <module platform="android" version="1.0.2">ti.synthengine</module>
16
- <module platform="iphone" version="1.0.2">ti.synthengine</module>
17
- </modules>
18
- ```
16
+ The example checks the Sound contract, spatial limits and enable/disable statistics methods before constructing the screen. Audio starts after layout settles. If startup fails, session buttons remain available to retry. Closing the window retires its pattern listener, clears startup/statistics timers, disables diagnostics and shuts down the engine.
19
17
 
20
- For the local candidates, replace both `version="1.0.2"` declarations above with `version="1.1.0"`. They still report public API `1.0.0`; detect variation through both capability/schema announcements. Rebuild and reinstall after switching packages.
18
+ This example plays foreground finite sounds and patterns. Sustained notes, master processing and voice LFOs have separate examples in the [advanced documentation](../../references/advanced-examples.md#sustained-game-motor).
21
19
 
22
- 4. Copy the entire `example/` folder into the test app's `Resources/`, including [app.js](app.js), [sequenceScore.js](lib/sequenceScore.js) and [sequenceOwner.js](lib/sequenceOwner.js). It needs no images or audio files.
23
- 5. Build and run the test app with the Titanium CLI or your existing Titanium launch configuration. Use Titanium SDK 13.2.0.GA or newer, Android compile API 35 or newer, and iOS 15 or newer. See the [module installation guide](../../references/guide.md#install-the-module).
20
+ ## Synthesis and sequencing
24
21
 
25
- The example checks the installed API version, spatial limits and all three statistics methods before constructing the screen. If it requests current packages, replace the native module, then rebuild and reinstall the app; copying module files alone does not update an app already on a device.
22
+ `createSound({ sound, playback })` separates synthesis properties from interpretation and timing. `lib/soundPreset.js` converts the example's editable recipes to this schema without playing audio. Finite sounds call `Sound.play()` and preserve their Boolean acceptance result. Chords declare `type: 'chord'`, including a chord containing just one note.
23
+
24
+ ## Native finite-tone repetition
25
+
26
+ ```js
27
+ var synth = require('ti.synthengine')
28
+ var shot = synth.createSound({
29
+ sound: { waveType: synth.getContract().waveTypes.SQUARE, attack: 2, release: 40, volume: 0.1 },
30
+ playback: { type: 'repeat', frequencies: [1380], frequencyEnd: 340, duration: 105, interval: 200 }
31
+ })
32
+ if (synth.start({ maxVoices: 16, mixingProfile: 'conservative' })) {
33
+ synth.setVolume(1)
34
+ var repetition = shot.play()
35
+ if (repetition) {
36
+ repetition.update({ interval: 150, volume: 0.08 })
37
+ // Later: repetition.stop()
38
+ }
39
+ }
40
+ ```
26
41
 
27
- Audio starts after the first layout settles. Sound controls report native rejections. If startup fails, the session buttons remain available to retry. Closing the window removes its sequence listener, clears startup/statistics timers, disables diagnostics and shuts down the engine. The example plays finite sounds in the foreground and does not demonstrate background audio. Turn diagnostics off before leaving it in the background.
42
+ Finite `duration` includes release; envelope times and repetition `interval` are milliseconds. Per-call overrides do not change the Sound definition. Repetition controls keep private execution identity. An update preserves the next deadline and changes subsequent attacks; stopping cancels future attacks while admitted shots finish. There is no JavaScript attack timer. Close, session changes and STOP ALL also release or invalidate repetition ownership.
28
43
 
29
- ## Synthesis and sequencing
44
+ The screen retains its waveform, English/solfege pitch, stereo motion, vibrato, variable-rate noise, pulse-width, wavetable, quantization, chord, arpeggio, tempo/swing, percussion, decay/sustain, filter, FM and detune demonstrations. Master BOOST intentionally raises the bus above unity; 100% restores unity. Repeated taps can overlap voices and cause limiting or replacement.
30
45
 
31
- - Compare all six waveforms and equivalent English/solfege note names.
32
- - Try stereo pan, pitch sweep, vibrato, variable-rate noise, pulse width, custom wavetable and quantization.
33
- - Compare atomic chords with explicit pans, matched arpeggios, tempo/swing and noise percussion.
34
- - Hear decay/sustain, resonant filtering, FM bell/electric piano and detuned voices.
46
+ ## Native attack variation
35
47
 
36
- Keep system volume moderate while comparing sounds. Master BOOST intentionally raises the bus above unity; use the 100% control to restore it. Repeated taps overlap events and may cause limiting or voice replacement.
48
+ ATTACK VARIATION retains its metal-strike recipe. When both `capabilities.variation === true` and `variationSchemaVersion === 1` are announced, the renderer chooses pitch, FM depth and level once per admitted attack. Otherwise the recipe omits variation. See the [variation guide](../../references/native-variation.md).
37
49
 
38
- ## Headphone positioning
50
+ ## Sequence completion and cancellation
39
51
 
40
- Use stereo headphones. In the binaural section, compare each of the nine noise positions one at a time: left/right runs from -1 to 1, above/below from 1 to -1. The center still uses binaural processing. Elevation perception varies with the listener and headphones, so the controls cannot guarantee localization for each person.
52
+ `lib/sequenceScore.js` compiles note-name scores to millisecond onsets and explicit release onsets. `soundPreset.js` maps step spacing to `playback.interval` or `steps[].interval`, keeps `length`, and places synthesis in `sound` or `steps[].sound`. The native sample clock schedules every attack.
41
53
 
42
- Select STEREO MODE and repeat points with the same horizontal position. Their height no longer affects placement because both spatial keys are omitted. BINAURAL MODE restores per-event coordinates. These two modes affect this section only; the ordinary stereo demos remain unchanged. An active voice keeps its original position.
54
+ `lib/sequenceOwner.js` retains the pattern control returned by `Sound.play()`. Tap PATTERN END once to see `end: completed`. Tap repeatedly to request a stop, coalesce pending scores and publish only the newest complete score after the matching control emits `end`. Its callback receives `{ reason: 'completed' | 'stopped' }`, without native IDs. Late events from retired controls cannot change application ownership.
43
55
 
44
- SPATIAL CHORD places the whole chord above/right. SPATIAL PATTERN places three broadband hits above/left, below/center and above/right using per-step coordinates. Neither sends stereo pan fields while binaural is active. The output remains two channels on a front plane, without rear positioning, distance attenuation or head tracking. See the [app integration guide](../../references/guide.md#integrate-binaural-in-a-2d-app) for source mapping and amplitude sliders.
56
+ A completed pattern can leave audible releases or shared effect tails. STOP ALL cancels future attacks and fades active voices. Changing sessions or closing the example shuts down its engine and cancels pending application publication.
45
57
 
46
- ## Audio session
58
+ ## Headphone positioning
47
59
 
48
- On iOS, compare PLAYBACK SESSION and AMBIENT SESSION. Changing the selection shuts down the engine, silently cancels any pending pattern, starts the new policy and restores the selected master volume. It does not resume the previous sound. The policy is fixed while the stream runs; a second `start()` alone cannot change it.
60
+ Use stereo headphones to compare the nine noise positions individually. Horizontal coordinates run from -1 to 1; vertical coordinates run from 1 above to -1 below. Center still uses binaural processing. Elevation perception varies by listener and headphones.
49
61
 
50
- On an iOS device, play music from another app and try a short sound in ambient mode; compare with playback. Also compare the device's silent mode where available. Ambient should respect silent mode and mix with other apps; playback ignores silent mode and normally interrupts their audio. Android validates both strings but does not change its output. Simulator behavior does not establish the physical silent-mode, interruption or route behavior.
62
+ STEREO MODE omits both spatial coordinates and uses horizontal pan alone. BINAURAL MODE restores coordinates for new events; an active voice retains its original position. These modes affect only this section. SPATIAL CHORD places a whole chord above/right, and SPATIAL PATTERN places three broadband hits using per-step coordinates. Binaural events omit stereo pan fields.
51
63
 
52
- ## Sequence completion and cancellation
64
+ Output remains two channels on a front plane, without rear positioning, distance attenuation or head tracking. See the [integration guide](../../references/guide.md#integrate-binaural-in-a-2d-app).
53
65
 
54
- 1. Tap PATTERN END and wait for `sequenceend: completed`.
55
- 2. Tap it twice quickly. The example requests stop for its accepted sequence, coalesces repeated taps to the latest complete score, and publishes it only after the matching retirement event frees capacity. Native `sequenceend` events report `{ sequenceId, reason: 'completed' | 'stopped' }`.
56
- 3. Selecting a session shuts down the engine and restarts it with the selected policy. Events already queued for JavaScript can still arrive.
57
- 4. STOP ALL releases active voices and cancels future attacks. Closing the example shuts down its engine.
66
+ ## Audio session
58
67
 
59
- All successive attacks run on the native sample clock. The example compiles its note-name scores to millisecond sequences with explicit `duration` and `length`, preserving the original interval and release onset. Each sequence has one of `frequencies`, MIDI `notes`, or `rest: true` per step, with sound shaping in `voice`/`overrides`. Copy the entire `example/` folder, including `lib/sequenceScore.js` and `lib/sequenceOwner.js`, into a Classic app's Resources folder.
68
+ PLAYBACK SESSION and AMBIENT SESSION shut down the existing stream, cancel pending patterns, start the selected policy and restore master volume. They do not resume old sounds; a second `start()` alone cannot change a running session's policy.
60
69
 
61
- Completion marks the final scheduled step; releases and shared effect tails can remain audible. See the [event contract](../../references/api.md#sequenceend-event).
70
+ On physical iOS hardware, compare another app's music, silent mode and an output-route change. Ambient respects silent mode and mixes with other apps; playback ignores silent mode and normally interrupts them. Android accepts both strings without changing its output. Simulator results do not establish physical interruption or route behavior.
62
71
 
63
72
  ## Statistics
64
73
 
65
- - READ STATS takes a fresh snapshot without resetting counters or enabling diagnostics.
66
- - TOGGLE DIAGNOSTICS enables callback timing and signal meters, refreshing at 5 Hz. Toggle it off to stop the timer and optional measurements; basic counters remain available.
67
- - RESET STATS clears accumulated statistics without stopping voices, cancelling queued work or changing the diagnostic switch. The next callback applies render-owned resets.
68
-
69
- The panel shows active/peak/spatial/tail voices, pending commands, replacements, request/publication rejections, priority drops, callback load, stereo output peak and backend underruns. It displays `n/a` for unmeasured or unavailable values, keeping them distinct from zero. Signal windows need about 200 ms of output after enabling. Native snapshots also include RMS, limiter reduction, sample rate, buffer and latency fields; consult the [complete field reference](../../references/api.md#getstats) when integrating a full diagnostic view.
70
-
71
- Render load divides callback elapsed time by the audio budget for that block; it does not measure total app CPU. The panel multiplies the native fraction by 100, so a reading of 1.20% means that fraction of the block budget, not 120%.
74
+ READ STATS takes a snapshot without resetting counters or enabling diagnostics. TOGGLE DIAGNOSTICS enables callback timing and signal meters, refreshing at 5 Hz; disabling it stops the timer and optional measurements while retaining basic counters. RESET STATS clears accumulated statistics without stopping voices or cancelling scheduled work.
72
75
 
73
- The shared final output stage retains zero samples. Master volume changes use a linear 10 ms ramp; overload can audibly change the mix. Even with zero voices, the stream renders silent blocks and runs output protection and any enabled diagnostics. Expect a small, changing baseline. The two decimal places are display formatting and do not guarantee that precision.
76
+ The panel shows active/peak/spatial/tail voices, pending commands, replacements, request/publication rejections, priority drops, callback load, stereo peak and backend underruns. Unavailable measurements remain `n/a`, distinct from zero. Signal windows need about 200 ms after enabling diagnostics. See the [field reference](../../references/api.md#getstats).
74
77
 
75
- Compare readings on the same device with the same configuration and load; simulator and phone readings are not equivalent. Android backend metrics are best-effort. This iOS backend has no underrun/buffer/latency measurement, and a clean counter cannot establish sound quality.
78
+ Render load is callback elapsed time divided by the budget for that block. It is not total app CPU. The stream continues rendering silence with zero voices. Compare measurements on the same device, route, configuration and load; a clean counter does not establish audio quality. Turn diagnostics off before leaving this foreground example in the background.
76
79
 
77
80
  ## Checks and limits
78
81
 
79
- Run these focused checks from the module repository root:
82
+ From the module repository root:
80
83
 
81
84
  ```sh
82
- node tests/documentation_regression.js
83
- node --test tests/example_app_regression.js
85
+ node --test tests/example_app_regression.js tests/sequence_owner_regression.js
84
86
  ```
85
87
 
86
- The example regression executes its real action callbacks with simulated Titanium UI/audio boundaries. It covers rejection feedback, placement/bypass, session restart and gain preservation, diagnostic cleanup and close-before-start. It does not render native audio or establish layout, perceived elevation, silent-mode behavior or sound quality. Use the device checklist above, including headphones and an output-route change, before claiming those results.
87
-
88
-
89
- ## Native attack variation
90
-
91
- The **ATTACK VARIATION** button submits the same metal-strike recipe on each press. With `capabilities.variation === true` and `variationSchemaVersion === 1`, the renderer chooses pitch, FM depth and level once per admitted attack. Older packages receive the original recipe without the dictionary. This button uses independent timed events; the [variation guide](../../references/native-variation.md) separately demonstrates sustained ownership and native sequence retriggers. Physical listening acceptance remains separate from host correctness checks.
88
+ These checks execute the real interactive callbacks and the current public adapter over native payload spies. They cover preserved synthesis parameters, rejection feedback, spatial bypass, repeated finite shots, session/gain restoration, diagnostics, native retirement coalescing, active updates, late events and close-before-start. Native validator replay and platform listening acceptance remain separate. Host checks do not establish layout, perceived elevation, silent-mode behavior or sound quality.
@@ -1,20 +1,23 @@
1
+ var SoundPreset = require('lib/soundPreset')
1
2
  var sequenceScore = require('lib/sequenceScore')
2
3
  var Synth = require('ti.synthengine')
3
4
 
4
5
  var sequenceOwner = require('lib/sequenceOwner')(Synth, {
5
- rejected: function() { setStatus('Request rejected · sequence publication', COLORS.red) }
6
+ rejected: function() { setStatus('Request rejected · sequence publication', COLORS.red) },
7
+ ended: onSequenceEnd
6
8
  })
7
9
  function publishSequence(options) { return sequenceOwner.submit(options) }
8
10
 
9
- var defaults = Synth.getDefaults()
10
- if (!defaults || defaults.apiVersion !== '1.0.0') {
11
- throw new Error('TiSynthEngine 1.0.0 is required')
11
+ var defaults = Synth.getContract()
12
+ if (!defaults || defaults.version !== '2.0.0') {
13
+ throw new Error('TiSynthEngine 2.0.0 is required')
12
14
  }
13
15
 
14
16
  if (!defaults.limits || !defaults.limits.spatialX || !defaults.limits.spatialY
15
17
  || 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
+ || typeof Synth.enableStats !== 'function' || typeof Synth.disableStats !== 'function'
19
+ || typeof Synth.createSound !== 'function') {
20
+ throw new Error('Install the current TiSynthEngine Sound API packages, then rebuild and reinstall this app')
18
21
  }
19
22
 
20
23
  var WAVE = defaults.waveTypes
@@ -23,6 +26,11 @@ var engineRunning = false
23
26
  var startTimer = null
24
27
  var statsTimer = null
25
28
  var diagnosticsEnabled = false
29
+ var repeatingTone = null
30
+ var repeatingSound = Synth.createSound({
31
+ sound: { attack: 2, release: 40, waveType: WAVE.SQUARE, volume: 0.1 },
32
+ playback: { type: 'repeat', frequencies: [1380], frequencyEnd: 340, duration: 105, interval: 200 }
33
+ })
26
34
  var selectedSession = 'playback'
27
35
  var masterVolume = 1
28
36
  var binauralEnabled = true
@@ -200,7 +208,7 @@ function makeHero() {
200
208
  height: Ti.UI.SIZE,
201
209
  top: dp(16)
202
210
  })
203
- badgeRow.add(makeBadge('API 1.0.0', COLORS.indigo))
211
+ badgeRow.add(makeBadge('API 2.0.0', COLORS.indigo))
204
212
  badgeRow.add(makeBadge('ANDROID + iOS', COLORS.surfaceMuted))
205
213
  hero.add(badgeRow)
206
214
  hero.add(statusPill)
@@ -325,14 +333,14 @@ function makeCompactAction(options) {
325
333
  }
326
334
 
327
335
  function playMatchedSine(note) {
328
- return Synth.playTone({
336
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
329
337
  note: note,
330
338
  duration: 600,
331
339
  attack: 10,
332
340
  release: 80,
333
341
  volume: 0.7,
334
342
  waveType: WAVE.SINE
335
- })
343
+ })).play()
336
344
  }
337
345
 
338
346
  function playMatchedArpeggio(pitchKey, pitches) {
@@ -392,13 +400,45 @@ waveNames.forEach(function (name, index) {
392
400
  waveType: waveTypes[index]
393
401
  }
394
402
  if (waveTypes[index] === WAVE.WAVETABLE) options.waveTable = STEPPED_WAVE
395
- return Synth.playTone(options)
403
+ return Synth.createSound(SoundPreset.fromRecipe('tone', options)).play()
396
404
  }
397
405
  }))
398
406
  })
399
407
  waveSection.add(Ti.UI.createView({ height: dp(12) }))
400
408
  scrollView.add(waveSection)
401
409
 
410
+ // A repeated finite recipe retains its own control; there is no attack timer.
411
+ function stopRepeatingTone() {
412
+ if (!repeatingTone) return true
413
+ if (!repeatingTone.stop()) return false
414
+ repeatingTone = null
415
+ return true
416
+ }
417
+ var repeatSection = makeSection('Finite-tone repetition',
418
+ 'The same complete 105 ms shot repeats every 200 ms. Edit its future attacks; stop lets current shots finish.',
419
+ COLORS.teal)
420
+ repeatSection.add(makeAction({
421
+ title: 'START REPEATING', color: COLORS.teal, pressedColor: COLORS.tealPressed,
422
+ onPress: function () {
423
+ if (repeatingTone) return true
424
+ var control = repeatingSound.play()
425
+ if (!control) return false
426
+ repeatingTone = control
427
+ return true
428
+ }
429
+ }))
430
+ repeatSection.add(makeAction({
431
+ title: 'EDIT REPEATING', color: COLORS.teal, pressedColor: COLORS.tealPressed,
432
+ onPress: function () {
433
+ return !!repeatingTone && repeatingTone.update({ interval: 150, volume: 0.08 })
434
+ }
435
+ }))
436
+ repeatSection.add(makeAction({
437
+ title: 'STOP REPEATING', color: COLORS.surfaceMuted, pressedColor: COLORS.surfaceRaised,
438
+ onPress: stopRepeatingTone
439
+ }))
440
+ scrollView.add(repeatSection)
441
+
402
442
  // ---------------------------------------------------------------------------
403
443
  // 2. Note names
404
444
  // ---------------------------------------------------------------------------
@@ -452,7 +492,7 @@ motionSection.add(makeAction({
452
492
  pressedColor: COLORS.tealPressed,
453
493
  status: 'Pan sweep · left to right',
454
494
  onPress: function () {
455
- return Synth.playTone({
495
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
456
496
  frequency: 440,
457
497
  duration: 2000,
458
498
  attack: 20,
@@ -460,7 +500,7 @@ motionSection.add(makeAction({
460
500
  volume: 0.8,
461
501
  pan: -1.0,
462
502
  panEnd: 1.0
463
- })
503
+ })).play()
464
504
  }
465
505
  }))
466
506
  motionSection.add(makeAction({
@@ -469,7 +509,7 @@ motionSection.add(makeAction({
469
509
  pressedColor: COLORS.pinkPressed,
470
510
  status: 'Pitch sweep · 200 to 900 Hz',
471
511
  onPress: function () {
472
- return Synth.playTone({
512
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
473
513
  frequency: 200,
474
514
  frequencyEnd: 900,
475
515
  duration: 400,
@@ -477,7 +517,7 @@ motionSection.add(makeAction({
477
517
  release: 40,
478
518
  volume: 0.7,
479
519
  waveType: WAVE.SAWTOOTH
480
- })
520
+ })).play()
481
521
  }
482
522
  }))
483
523
  motionSection.add(makeAction({
@@ -486,7 +526,7 @@ motionSection.add(makeAction({
486
526
  pressedColor: COLORS.orangePressed,
487
527
  status: 'LFO vibrato · 5 Hz',
488
528
  onPress: function () {
489
- return Synth.playTone({
529
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
490
530
  frequency: 440,
491
531
  duration: 2000,
492
532
  attack: 20,
@@ -494,7 +534,7 @@ motionSection.add(makeAction({
494
534
  volume: 0.7,
495
535
  lfoFreq: 5,
496
536
  lfoDepth: 30
497
- })
537
+ })).play()
498
538
  }
499
539
  }))
500
540
  motionSection.add(Ti.UI.createView({ height: dp(16) }))
@@ -515,7 +555,7 @@ retroSection.add(makeAction({
515
555
  pressedColor: COLORS.brownPressed,
516
556
  status: 'Filtered sample-and-hold noise sweep',
517
557
  onPress: function () {
518
- return Synth.playTone({
558
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
519
559
  frequency: 120,
520
560
  duration: 760,
521
561
  attack: 1,
@@ -526,7 +566,7 @@ retroSection.add(makeAction({
526
566
  noiseRateEnd: 650,
527
567
  lowPassHz: 2500,
528
568
  bitDepth: 8
529
- })
569
+ })).play()
530
570
  }
531
571
  }))
532
572
  retroSection.add(makeAction({
@@ -535,7 +575,7 @@ retroSection.add(makeAction({
535
575
  pressedColor: COLORS.orangePressed,
536
576
  status: 'Square pulse-width sweep',
537
577
  onPress: function () {
538
- return Synth.playTone({
578
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
539
579
  note: 'A3',
540
580
  duration: 700,
541
581
  attack: 3,
@@ -544,7 +584,7 @@ retroSection.add(makeAction({
544
584
  waveType: WAVE.SQUARE,
545
585
  pulseWidth: 0.1,
546
586
  pulseWidthEnd: 0.86
547
- })
587
+ })).play()
548
588
  }
549
589
  }))
550
590
  retroSection.add(makeAction({
@@ -553,7 +593,7 @@ retroSection.add(makeAction({
553
593
  pressedColor: COLORS.tealPressed,
554
594
  status: 'Custom stepped wavetable',
555
595
  onPress: function () {
556
- return Synth.playTone({
596
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
557
597
  note: 'D4',
558
598
  duration: 680,
559
599
  attack: 4,
@@ -562,7 +602,7 @@ retroSection.add(makeAction({
562
602
  waveType: WAVE.WAVETABLE,
563
603
  waveTable: STEPPED_WAVE,
564
604
  bitDepth: 8
565
- })
605
+ })).play()
566
606
  }
567
607
  }))
568
608
  retroSection.add(makeAction({
@@ -622,14 +662,14 @@ harmonySection.add(makeAction({
622
662
  pressedColor: COLORS.blue,
623
663
  status: 'C major triad · sine',
624
664
  onPress: function () {
625
- return Synth.playChord({
665
+ return Synth.createSound(SoundPreset.fromRecipe('chord', {
626
666
  frequencies: [261.63, 329.63, 392.0],
627
667
  duration: 1200,
628
668
  attack: 20,
629
669
  release: 300,
630
670
  volume: 0.7,
631
671
  panSpread: 0.8
632
- })
672
+ })).play()
633
673
  }
634
674
  }))
635
675
  harmonySection.add(makeAction({
@@ -638,7 +678,7 @@ harmonySection.add(makeAction({
638
678
  pressedColor: COLORS.indigoPressed,
639
679
  status: 'C major chord · square and wide',
640
680
  onPress: function () {
641
- return Synth.playChord({
681
+ return Synth.createSound(SoundPreset.fromRecipe('chord', {
642
682
  notes: ['C4', 'E4', 'G4', 'C5'],
643
683
  duration: 1000,
644
684
  attack: 10,
@@ -646,7 +686,7 @@ harmonySection.add(makeAction({
646
686
  volume: 0.6,
647
687
  waveType: WAVE.SQUARE,
648
688
  pans: [-0.9, -0.3, 0.3, 0.9]
649
- })
689
+ })).play()
650
690
  }
651
691
  }))
652
692
  harmonySection.add(Ti.UI.createView({ height: dp(16) }))
@@ -740,7 +780,7 @@ expressiveSection.add(makeAction({
740
780
  pressedColor: COLORS.greenPressed,
741
781
  status: 'Pluck · decay and sustain',
742
782
  onPress: function () {
743
- return Synth.playTone({
783
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
744
784
  note: 'E4',
745
785
  duration: 900,
746
786
  attack: 2,
@@ -749,7 +789,7 @@ expressiveSection.add(makeAction({
749
789
  release: 380,
750
790
  volume: 0.6,
751
791
  waveType: WAVE.TRIANGLE
752
- })
792
+ })).play()
753
793
  }
754
794
  }))
755
795
  expressiveSection.add(makeAction({
@@ -758,7 +798,7 @@ expressiveSection.add(makeAction({
758
798
  pressedColor: COLORS.orangePressed,
759
799
  status: 'Wah · band-pass cutoff sweep',
760
800
  onPress: function () {
761
- return Synth.playTone({
801
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
762
802
  note: 'A2',
763
803
  duration: 1000,
764
804
  attack: 10,
@@ -769,7 +809,7 @@ expressiveSection.add(makeAction({
769
809
  filterHz: 350,
770
810
  filterHzEnd: 2400,
771
811
  filterQ: 5
772
- })
812
+ })).play()
773
813
  }
774
814
  }))
775
815
  expressiveSection.add(makeAction({
@@ -778,7 +818,7 @@ expressiveSection.add(makeAction({
778
818
  pressedColor: COLORS.indigoPressed,
779
819
  status: 'Bell · FM index decays',
780
820
  onPress: function () {
781
- return Synth.playTone({
821
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
782
822
  note: 'C5',
783
823
  duration: 2600,
784
824
  attack: 1,
@@ -790,7 +830,7 @@ expressiveSection.add(makeAction({
790
830
  fmRatio: 3.5,
791
831
  fmDepth: 4,
792
832
  fmDepthEnd: 0
793
- })
833
+ })).play()
794
834
  }
795
835
  }))
796
836
  expressiveSection.add(makeAction({
@@ -799,7 +839,7 @@ expressiveSection.add(makeAction({
799
839
  pressedColor: COLORS.blue,
800
840
  status: 'Electric piano · FM index mellows',
801
841
  onPress: function () {
802
- return Synth.playTone({
842
+ return Synth.createSound(SoundPreset.fromRecipe('tone', {
803
843
  note: 'E3',
804
844
  duration: 1800,
805
845
  attack: 3,
@@ -811,7 +851,7 @@ expressiveSection.add(makeAction({
811
851
  fmRatio: 1,
812
852
  fmDepth: 2.2,
813
853
  fmDepthEnd: 0.25
814
- })
854
+ })).play()
815
855
  }
816
856
  }))
817
857
  expressiveSection.add(makeAction({
@@ -820,7 +860,7 @@ expressiveSection.add(makeAction({
820
860
  pressedColor: COLORS.pinkPressed,
821
861
  status: 'Thick chord · detuned voices',
822
862
  onPress: function () {
823
- return Synth.playChord({
863
+ return Synth.createSound(SoundPreset.fromRecipe('chord', {
824
864
  notes: ['A3', 'E4', 'A3', 'E4', 'A3', 'E4'],
825
865
  detunes: [-9, -9, 0, 0, 9, 9],
826
866
  duration: 3200,
@@ -831,7 +871,7 @@ expressiveSection.add(makeAction({
831
871
  filterType: 'lowpass',
832
872
  filterHz: 1500,
833
873
  panSpread: 0.8
834
- })
874
+ })).play()
835
875
  }
836
876
  }))
837
877
 
@@ -851,11 +891,11 @@ expressiveSection.add(makeAction({
851
891
  volume: [0.291, 0.3], fmDepth: [2.944, 3.456], pitchCents: [-12, 12]
852
892
  } }
853
893
  }
854
- return Synth.playTone(options)
894
+ return Synth.createSound(SoundPreset.fromRecipe('tone', options)).play()
855
895
  }
856
896
  }))
857
897
 
858
- var patternEndLabel = makeText('sequenceend: no pattern yet', {
898
+ var patternEndLabel = makeText('end: no pattern yet', {
859
899
  top: dp(8),
860
900
  left: dp(16),
861
901
  right: dp(16),
@@ -865,21 +905,20 @@ var patternEndLabel = makeText('sequenceend: no pattern yet', {
865
905
 
866
906
  function onSequenceEnd(event) {
867
907
  patternEndLabel.text = event.reason === 'completed'
868
- ? 'sequenceend: completed'
869
- : 'sequenceend: stopped'
908
+ ? 'end: completed'
909
+ : 'end: stopped'
870
910
  setStatus(patternEndLabel.text, event.reason === 'completed' ? COLORS.green : COLORS.yellow)
871
911
  }
872
912
 
873
- Synth.addEventListener('sequenceend', onSequenceEnd)
874
913
 
875
914
  expressiveSection.add(makeAction({
876
915
  title: 'PATTERN END\nTap once to finish; tap again quickly to replace',
877
916
  color: COLORS.yellow,
878
917
  pressedColor: COLORS.yellowPressed,
879
918
  textColor: COLORS.canvas,
880
- status: 'Pattern running · waiting for sequenceend',
919
+ status: 'Pattern running · waiting for end',
881
920
  onPress: function () {
882
- patternEndLabel.text = 'sequenceend: pattern running'
921
+ patternEndLabel.text = 'end: pattern running'
883
922
  return publishSequence(sequenceScore({
884
923
  bpm: 132,
885
924
  noteValue: '8n',
@@ -948,10 +987,10 @@ spatialPoints.forEach(function (point, index) {
948
987
  title: point[0] + '\nNoise burst',
949
988
  fontSize: 11, color: COLORS.teal, pressedColor: COLORS.tealPressed,
950
989
  onPress: function () {
951
- return Synth.playTone(Object.assign({
990
+ return Synth.createSound(SoundPreset.fromRecipe('tone', Object.assign({
952
991
  frequency: 200, waveType: WAVE.NOISE, duration: 550,
953
992
  attack: 5, release: 200, volume: 0.3
954
- }, placement(point[1], point[2])))
993
+ }, placement(point[1], point[2])))).play()
955
994
  }
956
995
  }))
957
996
  })
@@ -960,10 +999,10 @@ spatialSection.add(makeAction({
960
999
  color: COLORS.blueDark, pressedColor: COLORS.blue,
961
1000
  onPress: function () {
962
1001
  var position = binauralEnabled ? { spatialX: 0.7, spatialY: 1 } : { panSpread: 0, pans: [0.7, 0.7, 0.7] }
963
- return Synth.playChord(Object.assign({
1002
+ return Synth.createSound(SoundPreset.fromRecipe('chord', Object.assign({
964
1003
  notes: ['C4', 'E4', 'G4'], duration: 1200, attack: 15, release: 300,
965
1004
  volume: 0.4, waveType: WAVE.SAWTOOTH
966
- }, position))
1005
+ }, position))).play()
967
1006
  }
968
1007
  }))
969
1008
  spatialSection.add(makeAction({
@@ -999,10 +1038,10 @@ var sessionLabel = makeText('Session: playback', { left: dp(16), right: dp(16),
999
1038
  color: COLORS.blueDark, pressedColor: COLORS.blue,
1000
1039
  accessibilityHint: 'Restarts the engine with this session policy',
1001
1040
  onPress: function () {
1002
- sequenceOwner.reset(); Synth.shutdown()
1041
+ repeatingTone = null; sequenceOwner.reset(); Synth.shutdown()
1003
1042
  engineRunning = false
1004
1043
  selectedSession = session
1005
- patternEndLabel.text = 'sequenceend: cancelled by shutdown (no event)'
1044
+ patternEndLabel.text = 'end: cancelled by shutdown (no event)'
1006
1045
  return startAppCycles()
1007
1046
  }
1008
1047
  }))
@@ -1046,7 +1085,7 @@ statsSection.add(makeAction({
1046
1085
  title: 'TOGGLE DIAGNOSTICS', color: COLORS.orange, pressedColor: COLORS.orangePressed,
1047
1086
  accessibilityHint: 'Enables or disables timing and signal measurements',
1048
1087
  onPress: function () {
1049
- if (!Synth.setStatsEnabled(!diagnosticsEnabled)) return false
1088
+ if (!(diagnosticsEnabled ? Synth.disableStats() : Synth.enableStats())) return false
1050
1089
  diagnosticsEnabled = !diagnosticsEnabled
1051
1090
  stopStatsPolling()
1052
1091
  if (diagnosticsEnabled) statsTimer = setInterval(refreshStats, 200)
@@ -1103,10 +1142,10 @@ boostRow.add(makeGridAction({
1103
1142
  title: 'STOP ALL\nRelease active voices',
1104
1143
  color: COLORS.redDark,
1105
1144
  pressedColor: COLORS.red,
1106
- status: 'Active voices fading; pending pattern steps remain',
1107
- accessibilityHint: 'Fades current voices without cancelling future pattern steps',
1145
+ status: 'Future attacks cancelled; active voices fading',
1146
+ accessibilityHint: 'Cancels future attacks and fades current voices',
1108
1147
  onPress: function () {
1109
- sequenceOwner.stop(); Synth.stopAll()
1148
+ repeatingTone = null; sequenceOwner.stop(); Synth.stopAll()
1110
1149
  }
1111
1150
  }))
1112
1151
  controlSection.add(boostRow)
@@ -1128,7 +1167,7 @@ win.add(scrollView)
1128
1167
  // ---------------------------------------------------------------------------
1129
1168
 
1130
1169
  function setMasterVolume(volume) {
1131
- if (!Synth.setVolume({ volume: volume })) return false
1170
+ if (!Synth.setVolume(volume)) return false
1132
1171
  masterVolume = volume
1133
1172
  return true
1134
1173
  }
@@ -1170,10 +1209,10 @@ function hideAndroidActionBar() {
1170
1209
  }
1171
1210
 
1172
1211
  function cleanup() {
1212
+ if (!stopRepeatingTone()) Ti.API.warn('Repeating tone stop rejected; shutting down owner')
1173
1213
  stopStatsPolling()
1174
- Synth.setStatsEnabled(false)
1214
+ Synth.disableStats()
1175
1215
  diagnosticsEnabled = false
1176
- Synth.removeEventListener('sequenceend', onSequenceEnd)
1177
1216
  win.removeEventListener('postlayout', onFirstPostLayout)
1178
1217
  win.removeEventListener('open', hideAndroidActionBar)
1179
1218
  win.removeEventListener('focus', hideAndroidActionBar)
@@ -1184,7 +1223,7 @@ function cleanup() {
1184
1223
  startTimer = null
1185
1224
  }
1186
1225
  if (engineRunning) {
1187
- sequenceOwner.reset(); Synth.shutdown()
1226
+ repeatingTone = null; sequenceOwner.reset(); Synth.shutdown()
1188
1227
  engineRunning = false
1189
1228
  }
1190
1229
  }