expo-juce 0.5.2-rc.0 → 0.5.2-rc.2

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/README.md CHANGED
@@ -1,244 +1,240 @@
1
- # expo-juce
2
-
3
- Realtime DSP w/C++ & JUCE
4
-
5
- expo-juce embeds [JUCE](https://juce.com) 8 in an Expo / React Native app as an Expo
6
- module: a JUCE-owned audio device drives an `AudioProcessorGraph` with an 8-voice
7
- polyphonic synth and a transport, controlled from JS through synchronous JSI calls. It is
8
- the sound engine of the Beeper app ([BeeperNative](https://github.com/Beeper-Buzz/BeeperNative)).
9
- iOS only for now; the Android and web modules are scaffolds.
10
-
11
- Terms used below are explained in the
12
- [BeeperNative glossary](https://github.com/Beeper-Buzz/BeeperNative/blob/@aaron.smulktis/develop/GLOSSARY.md)
13
- (lands on that branch when `debug/audio-diagnostics` is merged).
14
-
15
- > **Branches.** `main` is a stale scaffold. Current work is on `@aaron.smulktis/develop`;
16
- > the diagnostics and latency work is on `debug/audio-diagnostics`. The GitHub repo is
17
- > `1instinct/expo-juce` (`package.json` still names `Beeper-buzz/expo-juce`).
18
-
19
- ## Architecture
20
-
21
- ```
22
- JS (src/index.ts) setup / noteOn / params / transport / getDiagnostics
23
- │ JSI (Expo Modules API)
24
- ExpoJuceModule.swift Function (sync) / AsyncFunction (setup, shutdown), onBeat events
25
- │
26
- ExpoJuceBridge (ObjC) plain ObjC surface for Swift
27
- │
28
- BeeperAudioEngine (ObjC++) AVAudioSession Playback + MixWithOthers; setup steps 0–11 on main thread
29
- │
30
- juce::AudioDeviceManager ─► GraphPlayer (AudioIODeviceCallback: graph, peaks, NaNs, test tone)
31
- │
32
- juce::AudioProcessorGraph PolySynthProcessor ─► audio output node
33
- ├─ SPSC note queue (256) drained at block start
34
- ├─ 8 × Voice: dsp::Oscillator → StateVariableTPTFilter (LP) → ADSR
35
- └─ TransportEngine (tempo / position / beat crossings)
36
- ```
37
-
38
- ## Roadmap: Full Audio Workstation
39
-
40
- expo-juce is being built toward full audio workstation capabilities. The work is decomposed into independent sub-projects, each building on the previous:
41
-
42
- ### Sub-project 1: Polyphonic Synth Engine
43
- **Status:** Done — working on device (iPhone, 2026-10-22) — [spec](docs/superpowers/specs/2026-05-11-polyphonic-synth-engine-design.md), [plan](docs/superpowers/plans/2026-05-11-polyphonic-synth-engine.md)
44
-
45
- Replaced the mono SynthEngine + AVAudioEngine with a JUCE-owned audio stack built on `AudioProcessorGraph`. 8-voice polyphonic synth with per-voice oscillator, SVF filter, and full ADSR envelope. MIDI-style API (`noteOn(note, velocity)` / `noteOff(note)`). JUCE owns device management and output for cross-platform support (iOS, macOS, Windows, Linux).
46
-
47
- ### Diagnostics, recovery and latency
48
- **Status:** Diagnostics done (0.5.2-diag.x); recovery and latency margin in progress (0.5.2+) — [silence spec](docs/superpowers/specs/2026-10-21-audio-silence-diagnostics.md), [latency spec](docs/superpowers/specs/2026-10-22-latency-margin.md), [latency guide](docs/latency.md)
49
-
50
- `setup()` rejects with the failing step, `getDiagnostics()` exposes every layer from device callbacks to synth peak, `playTestTone()` bypasses the graph, an offline harness reproduces the device callback on Linux. Root causes of the first on-device silence (fixed 2026-10-22): a stale Metro bundle running the app on stubs, infinite recursion in `-setBeatCallback:`, `setup()` always resolving, and expo-av switching the shared session to Ambient. In progress: 128-sample buffer, no allocation on the audio thread, native `JucePadView` touch path, 2 ms default attack, session re-assert and interruption / media-reset recovery (restoring what was lost with `JuceToneGenerator`, see the [salvage inventory](docs/superpowers/2026-10-22-salvage-inventory.md)).
51
-
52
- ### Sub-project 2: Effects Chain
53
- **Status:** Planned
54
-
55
- Add `AudioProcessor` nodes for standard effects: reverb, delay, distortion, EQ, compressor. Inserted into the graph after the synth node on a master effects bus. Each effect is an independent processor — can be bypassed, reordered, or removed.
56
-
57
- ### Sub-project 3: Sample Playback / Drum Machine
58
- **Status:** Planned — the old app's `BeeperSampler` and its four WAVs are the starting point (salvage inventory)
59
-
60
- `SamplerProcessor` node that loads audio files and triggers playback via `noteOn`. Supports multi-sample mapping (e.g., different samples per MIDI note). Integrates with the transport for sequenced playback.
61
-
62
- ### Sub-project 4: Audio Input Processing
63
- **Status:** Planned
64
-
65
- Route mic/line input through the `AudioProcessorGraph` for realtime effects processing (guitar pedal board style). The session is currently `Playback` + `MixWithOthers` with no input channels (`initialise(0, 2)`); this sub-project switches to `PlayAndRecord`, opens input channels and adds an input node routed through effects processors.
66
-
67
- ### Sub-project 5: Modular Routing
68
- **Status:** Planned
69
-
70
- Expose the `AudioProcessorGraph` topology to TypeScript — add, remove, and connect nodes from JS. Enables user-configurable signal chains, parallel processing, send/return buses, and sidechain routing.
71
-
72
- ### Sub-project 6: Electron / Desktop
73
- **Status:** Planned
74
-
75
- Compile the same C++ engine for macOS, Windows, and Linux. JUCE abstracts platform audio (CoreAudio, WASAPI, ALSA). Package as an Electron native addon. Same graph, same processors, same API — different platform.
76
-
77
- ## Quick Start
78
-
79
- ### Installation
80
-
81
- Requires an Expo dev client or EAS build (not Expo Go). Pin an exact version; prereleases are
82
- published under a dist-tag:
83
-
84
- ```bash
85
- npm install expo-juce@0.5.2-diag.1 --save-exact # dist-tag "diag"; "latest" is 0.5.1
86
- ```
87
-
88
- The native code is compiled by CocoaPods during prebuild / EAS build (`ios/ExpoJuce.podspec`,
89
- JUCE vendored in `ios/juce_modules`).
90
-
91
- ### Usage
92
-
93
- ```tsx
94
- import * as ExpoJuce from 'expo-juce';
95
-
96
- // Once, e.g. on first mount. Rejects with code ERR_AUDIO_SETUP, message "step N: <error>".
97
- await ExpoJuce.setup();
98
- // 0.5.2+: optional device request (defaults 128 samples / 48 kHz; actual values may differ)
99
- // await ExpoJuce.setup({ bufferSize: 128, sampleRate: 48000 });
100
-
101
- // Notes — synchronous; pushed onto a lock-free single-producer queue read by the audio thread
102
- ExpoJuce.noteOn(60, 127); // note 0–127, velocity 0–127
103
- ExpoJuce.noteOff(60);
104
- ExpoJuce.allNotesOff();
105
-
106
- // Global synth parameters — synchronous, std::atomic
107
- ExpoJuce.setWaveform('saw'); // 'sine' | 'square' | 'saw' | 'triangle'
108
- ExpoJuce.setFilterCutoff(2000); // Hz, 20–20000 (default 20000)
109
- ExpoJuce.setFilterResonance(0.3); // 0–1 → Q 0.707–10
110
- ExpoJuce.setAttack(10); // ms, 1–10000 (default 10; 2 from 0.5.2+)
111
- ExpoJuce.setDecay(1); // ms (default 1)
112
- ExpoJuce.setSustain(1); // 0–1 (default 1)
113
- ExpoJuce.setRelease(200); // ms (default 200)
114
- ExpoJuce.setDetune(0); // cents, −1200…1200
115
- ExpoJuce.setMasterLevel(0.5); // 0–1 (default 0.5)
116
-
117
- // Transport
118
- ExpoJuce.setTempo(120); // BPM, 20–400
119
- ExpoJuce.startTransport();
120
- const sub = ExpoJuce.addBeatListener(({ beat, bpm }) => { /* … */ });
121
- ExpoJuce.getTransportPosition(); ExpoJuce.getTempo(); ExpoJuce.isTransportPlaying();
122
- ExpoJuce.setTransportPosition(0);
123
- ExpoJuce.stopTransport();
124
- sub.remove();
125
-
126
- // Diagnostics
127
- const d = ExpoJuce.getDiagnostics(); // AudioDiagnostics snapshot, see src/ExpoJuce.types.ts
128
- ExpoJuce.playTestTone(500); // 440 Hz into the device output, bypasses graph + synth;
129
- // false if the engine/device is not running
130
-
131
- await ExpoJuce.shutdown();
132
- ```
133
-
134
- ### `JucePadView` (0.5.2+)
135
-
136
- A native view that plays a note on touch from the iOS main thread, without the JS thread,
137
- and reports to JS afterwards for visuals and haptics. Multi-touch safe: note off is sent when
138
- the last touch on the view lifts.
139
-
140
- ```tsx
141
- import { JucePadView } from 'expo-juce';
142
-
143
- <JucePadView
144
- note={60}
145
- velocity={127}
146
- enabled
147
- onNoteOn={() => {/* light the pad, haptic */}}
148
- onNoteOff={() => {/* unlight */}}
149
- style={{ width: 80, height: 80 }}
150
- />
151
- ```
152
-
153
- Check the shipped type definitions for the exact event payloads once 0.5.2 is published.
154
-
155
- ### Diagnostics
156
-
157
- `getDiagnostics()` returns setup state (`isReady`, `setupStep`, `lastError`), device
158
- (`deviceName`, `sampleRate`, `bufferSize`, `outputLatencySamples`), counters
159
- (`callbackCount`, `graphProcessCount`, `synthProcessCount`, `notesQueued/Consumed/Dropped`,
160
- `nanCount`, `toneSamplesMixed`), peaks (`outputPeak`, `synthPeak`), timing
161
- (`lastNoteToRenderMs`) and the AVAudioSession state (`sessionCategory`, `sessionSampleRate`,
162
- `sessionIOBufferMs`, `sessionOutputLatencyMs`, `otherAudioPlaying`, `routeOutputs`). 0.5.2+ adds
163
- `lastTouchToRenderMs`, `preferredBufferSize`, `preferredSampleRate`, `sessionReassertCount`, `interruptionCount`,
164
- `mediaResetCount`, `lastRecoveryError`. BeeperNative's diagnostics panel shows them as a
165
- pass/fail ladder — see [docs/latency.md](docs/latency.md#5-reading-the-diagnostics-panel).
166
-
167
- ## Latency
168
-
169
- Budget < 40 ms touch-to-sound, target ≤ 25 ms. Baseline on iPhone 16 Pro speaker: 48 kHz,
170
- 257-sample buffer, panel estimate 24.2 ms (before the touch path). The signal path, every
171
- change, how to read the panel and the **slow-motion measurement procedure** are in
172
- [docs/latency.md](docs/latency.md).
173
-
174
- ## Development
175
-
176
- ### Repository structure
177
-
178
- ```
179
- expo-juce/
180
- ├── src/ # TypeScript API (index.ts, ExpoJuce.types.ts), web stub, JUCE vendoring scripts
181
- ├── ios/ # Swift module, ObjC bridge, ObjC++ engine, JuceModule_*.mm, podspec
182
- │ └── juce_modules/ # Vendored JUCE 8.0.1 modules
183
- ├── android/ # Kotlin scaffold (no audio engine yet)
184
- ├── test/harness/ # Offline JUCE graph harness (WSL, CMake)
185
- ├── example/ # Old example app (Expo 51, pinned expo-juce 0.1.1) — stale; BeeperNative is the reference app
186
- ├── docs/
187
- │ ├── latency.md # Latency guide and measurement procedure
188
- │ └── superpowers/ # Specs, plans and the salvage inventory
189
- └── package.json
190
- ```
191
-
192
- ### Build and publish (Windows)
193
-
194
- There is no Mac or Xcode on the dev machine. The `expo-module-scripts` lifecycle scripts are
195
- bash and fail under npm on Windows, so build and publish by hand from Git Bash:
196
-
197
- ```bash
198
- npm install
199
- # 1. bump "version" in package.json, e.g. 0.5.2-diag.2
200
- CI=1 bash node_modules/expo-module-scripts/bin/expo-module-build # tsc → build/
201
- npm publish --tag diag --ignore-scripts # npm 2FA code required
202
- ```
203
-
204
- `--ignore-scripts` skips `prepublishOnly`, so step 1's build is mandatory. Prereleases go
205
- under a dist-tag (`diag`) so `latest` is not moved.
206
-
207
- Then in BeeperNative: `npm install expo-juce@<version> --save-exact`, check that
208
- `package-lock.json` still has the `@sentry/cli-darwin` entry, and run a new EAS build (EAS
209
- cannot resolve `file:` dependencies, which is why every native change goes through npm).
210
-
211
- ### Verifying without Xcode
212
-
213
- 1. **TypeScript:** `npx tsc --noEmit` here and in BeeperNative.
214
- 2. **Engine C++:** the offline harness compiles the vendored JUCE headless with `Voice`,
215
- `VoiceAllocator`, `TransportEngine`, `PolySynthProcessor` and a copy of `GraphPlayer`, replays the
216
- `doSetup` order and drives the callback with iOS-like block sizes:
217
- ```sh
218
- wsl -e bash -lc "cd /mnt/c/Users/aaron/Internal/Beeper/code/expo-juce/test/harness && cmake -S . -B build && cmake --build build -j16 && ./build/harness"
219
- ```
220
- See [test/harness/README.md](test/harness/README.md) (case `g2` is an expected-FAIL negative
221
- control). Extend it for every new graph node.
222
- 3. **ObjC++ / Swift:** careful review (no compiler locally); the EAS build is the first compile.
223
- 4. **Device:** BeeperNative's diagnostics panel (long-press BEEPER STUDIO), then the slow-motion
224
- measurement in [docs/latency.md](docs/latency.md#6-slow-motion-measurement-procedure).
225
-
226
- ### JUCE vendoring
227
-
228
- `npm run download-juce` fetches JUCE 8.0.1 into `JUCE/` and `npm run vendor-juce` copies the needed modules into
229
- `ios/juce_modules` (stripping tests, demos and non-iOS code). Each JUCE module is compiled as
230
- its own translation unit (`ios/JuceModule_*.mm`); `ios/JuceConfig.h` holds the module flags and
231
- `JUCE_STANDALONE_APPLICATION 0`.
232
-
233
- ## Documentation
234
-
235
- - [docs/latency.md](docs/latency.md) — latency budget, signal path, changes, panel fields, slow-motion measurement
236
- - [docs/superpowers/specs/2026-05-11-polyphonic-synth-engine-design.md](docs/superpowers/specs/2026-05-11-polyphonic-synth-engine-design.md) and its [plan](docs/superpowers/plans/2026-05-11-polyphonic-synth-engine.md)
237
- - [docs/superpowers/specs/2026-10-21-audio-silence-diagnostics.md](docs/superpowers/specs/2026-10-21-audio-silence-diagnostics.md) — silence investigation, diagnostics design
238
- - [docs/superpowers/specs/2026-10-22-latency-margin.md](docs/superpowers/specs/2026-10-22-latency-margin.md) — latency work spec and plan
239
- - [docs/superpowers/2026-10-22-salvage-inventory.md](docs/superpowers/2026-10-22-salvage-inventory.md) — what is reusable from earlier audio code
240
- - [BeeperNative README](https://github.com/Beeper-Buzz/BeeperNative/blob/@aaron.smulktis/develop/README.md) and [GLOSSARY](https://github.com/Beeper-Buzz/BeeperNative/blob/@aaron.smulktis/develop/GLOSSARY.md) — the app and its vocabulary (land on that branch when `debug/audio-diagnostics` is merged)
241
-
242
- ## License
243
-
244
- MIT
1
+ # expo-juce
2
+
3
+ Realtime DSP w/C++ & JUCE
4
+
5
+ expo-juce embeds [JUCE](https://juce.com) 8 in an Expo / React Native app as an Expo
6
+ module: a JUCE-owned audio device drives an `AudioProcessorGraph` with an 8-voice
7
+ polyphonic synth and a transport, controlled from JS through synchronous JSI calls.
8
+ iOS only for now; the Android and web modules are scaffolds.
9
+
10
+ > **Branches.** `main` is a stale scaffold. Current work is on `@aaron.smulktis/develop`;
11
+ > the diagnostics and latency work is on `debug/audio-diagnostics`.
12
+
13
+ ## Architecture
14
+
15
+ ```
16
+ JS (src/index.ts) setup / noteOn / params / transport / getDiagnostics
17
+ │ JSI (Expo Modules API)
18
+ ExpoJuceModule.swift Function (sync) / AsyncFunction (setup, shutdown), onBeat events
19
+ │
20
+ ExpoJuceBridge (ObjC) plain ObjC surface for Swift
21
+ │
22
+ ExpoJuceAudioEngine (ObjC++) AVAudioSession Playback + MixWithOthers; setup steps 0–11 on main thread
23
+ │
24
+ juce::AudioDeviceManager ─► GraphPlayer (AudioIODeviceCallback: graph, peaks, NaNs, test tone)
25
+ │
26
+ juce::AudioProcessorGraph PolySynthProcessor ─► audio output node
27
+ ├─ NoteEventQueue: note-on ring (256) drained at block start; releases as marks
28
+ ├─ 8 × Voice: dsp::Oscillator → StateVariableTPTFilter (LP) → ADSR
29
+ └─ TransportEngine (tempo / position / beat crossings)
30
+ ```
31
+
32
+ ## Roadmap: Full Audio Workstation
33
+
34
+ expo-juce is being built toward full audio workstation capabilities. The work is decomposed into independent sub-projects, each building on the previous:
35
+
36
+ ### Sub-project 1: Polyphonic Synth Engine
37
+ **Status:** Done — working on device (iPhone, 2026-10-22)
38
+
39
+ Replaced the mono SynthEngine + AVAudioEngine with a JUCE-owned audio stack built on `AudioProcessorGraph`. 8-voice polyphonic synth with per-voice oscillator, SVF filter, and full ADSR envelope. MIDI-style API (`noteOn(note, velocity)` / `noteOff(note)`). JUCE owns device management and output for cross-platform support (iOS, macOS, Windows, Linux).
40
+
41
+ ### Diagnostics, recovery and latency
42
+ **Status:** Diagnostics done (0.5.2-diag.x); recovery and latency margin in 0.5.2 — see the [latency guide](docs/latency.md)
43
+
44
+ `setup()` rejects with the failing step, `getDiagnostics()` exposes every layer from device callbacks to synth peak, `playTestTone()` bypasses the graph, an offline harness reproduces the device callback on Linux. Root causes of the first on-device silence (fixed 2026-10-22): a stale Metro bundle running the app on stubs, infinite recursion in `-setBeatCallback:`, `setup()` always resolving, and expo-av switching the shared session to Ambient. 0.5.2 adds: 128-sample buffer, no allocation on the audio thread, native `JucePadView` touch path, 2 ms default attack, session re-assert and interruption / media-reset recovery (keeping synth settings and transport state across rebuilds).
45
+
46
+ ### Sub-project 2: Effects Chain
47
+ **Status:** Planned
48
+
49
+ Add `AudioProcessor` nodes for standard effects: reverb, delay, distortion, EQ, compressor. Inserted into the graph after the synth node on a master effects bus. Each effect is an independent processor — can be bypassed, reordered, or removed.
50
+
51
+ ### Sub-project 3: Sample Playback / Drum Machine
52
+ **Status:** Planned — an earlier JUCE sampler prototype is the starting point
53
+
54
+ `SamplerProcessor` node that loads audio files and triggers playback via `noteOn`. Supports multi-sample mapping (e.g., different samples per MIDI note). Integrates with the transport for sequenced playback.
55
+
56
+ ### Sub-project 4: Audio Input Processing
57
+ **Status:** Planned
58
+
59
+ Route mic/line input through the `AudioProcessorGraph` for realtime effects processing (guitar pedal board style). The session is currently `Playback` + `MixWithOthers` with no input channels (`initialise(0, 2)`); this sub-project switches to `PlayAndRecord`, opens input channels and adds an input node routed through effects processors.
60
+
61
+ ### Sub-project 5: Modular Routing
62
+ **Status:** Planned
63
+
64
+ Expose the `AudioProcessorGraph` topology to TypeScript — add, remove, and connect nodes from JS. Enables user-configurable signal chains, parallel processing, send/return buses, and sidechain routing.
65
+
66
+ ### Sub-project 6: Electron / Desktop
67
+ **Status:** Planned
68
+
69
+ Compile the same C++ engine for macOS, Windows, and Linux. JUCE abstracts platform audio (CoreAudio, WASAPI, ALSA). Package as an Electron native addon. Same graph, same processors, same API — different platform.
70
+
71
+ ## Quick Start
72
+
73
+ ### Installation
74
+
75
+ Requires an Expo dev client or EAS build (not Expo Go). Pin an exact version; prereleases are
76
+ published under a dist-tag:
77
+
78
+ ```bash
79
+ npm install expo-juce@0.5.2-diag.1 --save-exact # dist-tag "diag"; "latest" is 0.5.1
80
+ ```
81
+
82
+ The native code is compiled by CocoaPods during prebuild / EAS build (`ios/ExpoJuce.podspec`,
83
+ JUCE vendored in `ios/juce_modules`).
84
+
85
+ ### Usage
86
+
87
+ ```tsx
88
+ import * as ExpoJuce from 'expo-juce';
89
+
90
+ // Once, e.g. on first mount. Rejects with code ERR_AUDIO_SETUP, message "step N: <error>".
91
+ await ExpoJuce.setup();
92
+ // 0.5.2+: optional device request (defaults 128 samples / 48 kHz; actual values may differ)
93
+ // await ExpoJuce.setup({ bufferSize: 128, sampleRate: 48000 });
94
+
95
+ // Notes — synchronous, never wait on a lock; the audio thread picks them up at the next block
96
+ // (note-offs and allNotesOff are never dropped, even mid-recovery)
97
+ ExpoJuce.noteOn(60, 127); // note 0–127, velocity 0–127
98
+ ExpoJuce.noteOff(60);
99
+ ExpoJuce.allNotesOff();
100
+
101
+ // Global synth parameters — synchronous, std::atomic
102
+ ExpoJuce.setWaveform('saw'); // 'sine' | 'square' | 'saw' | 'triangle'
103
+ ExpoJuce.setFilterCutoff(2000); // Hz, 20–20000 (default 20000)
104
+ ExpoJuce.setFilterResonance(0.3); // 0–1 → Q 0.707–10
105
+ ExpoJuce.setAttack(10); // ms, 1–10000 (default 10; 2 from 0.5.2+)
106
+ ExpoJuce.setDecay(1); // ms (default 1)
107
+ ExpoJuce.setSustain(1); // 0–1 (default 1)
108
+ ExpoJuce.setRelease(200); // ms (default 200)
109
+ ExpoJuce.setDetune(0); // cents, −1200…1200
110
+ ExpoJuce.setMasterLevel(0.5); // 0–1 (default 0.5)
111
+
112
+ // Transport
113
+ ExpoJuce.setTempo(120); // BPM, 20–400
114
+ ExpoJuce.startTransport();
115
+ const sub = ExpoJuce.addBeatListener(({ beat, bpm }) => { /* … */ });
116
+ ExpoJuce.getTransportPosition(); ExpoJuce.getTempo(); ExpoJuce.isTransportPlaying();
117
+ ExpoJuce.setTransportPosition(0);
118
+ ExpoJuce.stopTransport();
119
+ sub.remove();
120
+
121
+ // Diagnostics
122
+ const d = ExpoJuce.getDiagnostics(); // AudioDiagnostics snapshot, see src/ExpoJuce.types.ts
123
+ ExpoJuce.playTestTone(500); // 440 Hz into the device output, bypasses graph + synth;
124
+ // false if the engine/device is not running
125
+
126
+ await ExpoJuce.shutdown();
127
+ ```
128
+
129
+ ### `JucePadView` (0.5.2+)
130
+
131
+ A native view that plays a note on touch from the iOS main thread, without the JS thread,
132
+ and reports to JS afterwards for visuals and haptics. Multi-touch safe: note off is sent when
133
+ the last touch on the view lifts.
134
+
135
+ ```tsx
136
+ import { JucePadView } from 'expo-juce';
137
+
138
+ <JucePadView
139
+ note={60}
140
+ velocity={127}
141
+ enabled
142
+ onNoteOn={() => {/* light the pad, haptic */}}
143
+ onNoteOff={() => {/* unlight */}}
144
+ style={{ width: 80, height: 80 }}
145
+ />
146
+ ```
147
+
148
+ Check the shipped type definitions for the exact event payloads once 0.5.2 is published.
149
+
150
+ ### Diagnostics
151
+
152
+ `getDiagnostics()` returns setup state (`isReady`, `setupStep`, `lastError`), device
153
+ (`deviceName`, `sampleRate`, `bufferSize`, `outputLatencySamples`), counters
154
+ (`callbackCount`, `graphProcessCount`, `synthProcessCount`, `notesQueued/Consumed/Dropped`,
155
+ `nanCount`, `toneSamplesMixed`), peaks (`outputPeak`, `synthPeak`), timing
156
+ (`lastNoteToRenderMs`) and the AVAudioSession state (`sessionCategory`, `sessionSampleRate`,
157
+ `sessionIOBufferMs`, `sessionOutputLatencyMs`, `otherAudioPlaying`, `routeOutputs`). 0.5.2+ adds
158
+ `lastTouchToRenderMs`, `preferredBufferSize`, `preferredSampleRate`, `sessionReassertCount`, `interruptionCount`,
159
+ `mediaResetCount`, `lastRecoveryError`, `engineState`. A debug screen in the app can show them
160
+ as a pass/fail ladder — see [docs/latency.md](docs/latency.md#5-a-diagnostics-panel).
161
+
162
+ Synth settings and transport state (tempo, position, playing) survive the engine's automatic
163
+ rebuilds after a media-services reset or a stalled device; held notes do not.
164
+
165
+ ## Latency
166
+
167
+ Budget < 40 ms touch-to-sound, target ≤ 25 ms. Baseline on iPhone 16 Pro speaker: 48 kHz,
168
+ 257-sample buffer, panel estimate 24.2 ms (before the touch path). The signal path, every
169
+ change, how to read the panel and the **slow-motion measurement procedure** are in
170
+ [docs/latency.md](docs/latency.md).
171
+
172
+ ## Development
173
+
174
+ ### Repository structure
175
+
176
+ ```
177
+ expo-juce/
178
+ ├── src/ # TypeScript API (index.ts, ExpoJuce.types.ts), web stub, JUCE vendoring scripts
179
+ ├── ios/ # Swift module, ObjC bridge, ObjC++ engine, JuceModule_*.mm, podspec
180
+ │ └── juce_modules/ # Vendored JUCE 8.0.1 modules
181
+ ├── android/ # Kotlin scaffold (no audio engine yet)
182
+ ├── test/harness/ # Offline JUCE graph harness (WSL, CMake)
183
+ ├── example/ # Old example app (Expo 51, pinned expo-juce 0.1.1) — stale
184
+ ├── docs/
185
+ │ └── latency.md # Latency guide and measurement procedure
186
+ └── package.json
187
+ ```
188
+
189
+ ### Build and publish (Windows)
190
+
191
+ There is no Mac or Xcode on the dev machine. The `expo-module-scripts` lifecycle scripts are
192
+ bash and fail under npm on Windows, so build and publish by hand from Git Bash:
193
+
194
+ ```bash
195
+ npm install
196
+ # 1. bump "version" in package.json, e.g. 0.5.2-diag.2
197
+ CI=1 bash node_modules/expo-module-scripts/bin/expo-module-build # tsc → build/
198
+ npm publish --tag diag --ignore-scripts # npm 2FA code required
199
+ ```
200
+
201
+ `--ignore-scripts` skips `prepublishOnly`, so step 1's build is mandatory. Prereleases go
202
+ under a dist-tag (`diag`) so `latest` is not moved.
203
+
204
+ Then in the consuming app: `npm install expo-juce@<version> --save-exact`, check that
205
+ `package-lock.json` still has its macOS-only optional entries (a lockfile regenerated on
206
+ Windows can drop them, and EAS's `npm ci` on macOS then fails), and run a new EAS build (EAS
207
+ cannot resolve `file:` dependencies, which is why every native change goes through npm).
208
+
209
+ ### Verifying without Xcode
210
+
211
+ 1. **TypeScript:** `npx tsc --noEmit` here and in the consuming app.
212
+ 2. **Engine C++:** the offline harness compiles the vendored JUCE headless with `Voice`,
213
+ `VoiceAllocator`, `TransportEngine`, `PolySynthProcessor` and a copy of `GraphPlayer`, replays the
214
+ `doSetup` order and drives the callback with iOS-like block sizes. From `test/harness` in
215
+ WSL (or any Linux shell):
216
+ ```sh
217
+ cmake -S . -B build && cmake --build build -j16 && ./build/harness
218
+ ```
219
+ See [test/harness/README.md](test/harness/README.md) (case `g2` is an expected-FAIL negative
220
+ control). Extend it for every new graph node.
221
+ 3. **ObjC++ / Swift:** careful review (no compiler locally); the EAS build is the first compile.
222
+ 4. **Device:** a diagnostics panel built on `getDiagnostics()`
223
+ ([docs/latency.md](docs/latency.md#5-a-diagnostics-panel)), then the slow-motion
224
+ measurement in [docs/latency.md](docs/latency.md#6-slow-motion-measurement-procedure).
225
+
226
+ ### JUCE vendoring
227
+
228
+ `npm run download-juce` fetches JUCE 8.0.1 into `JUCE/` and `npm run vendor-juce` copies the needed modules into
229
+ `ios/juce_modules` (stripping tests, demos and non-iOS code). Each JUCE module is compiled as
230
+ its own translation unit (`ios/JuceModule_*.mm`); `ios/JuceConfig.h` holds the module flags and
231
+ `JUCE_STANDALONE_APPLICATION 0`.
232
+
233
+ ## Documentation
234
+
235
+ - [docs/latency.md](docs/latency.md) — latency budget, signal path, changes, panel fields, slow-motion measurement
236
+ - [test/harness/README.md](test/harness/README.md) — the offline engine harness
237
+
238
+ ## License
239
+
240
+ MIT