expo-juce 0.5.0 → 0.5.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.
@@ -8,12 +8,15 @@
8
8
  #include "PolySynthProcessor.h"
9
9
  #include "TransportEngine.h"
10
10
 
11
- // JUCE init moved to setup() raw pointers at file scope eliminate
12
- // the static init problem, and constructor doesn't guarantee main thread.
13
- static bool sJuceInitialized = false;
11
+ // Ensure MessageManager exists before any JUCE static initializers.
12
+ // On iOS, __attribute__((constructor)) runs on the main thread for
13
+ // statically-linked frameworks (which Expo modules are).
14
+ __attribute__((constructor))
15
+ static void initJuceMessageManager() {
16
+ juce::MessageManager::getInstance();
17
+ }
14
18
 
15
19
  // Lightweight AudioIODeviceCallback that drives an AudioProcessorGraph.
16
- // Replaces juce::AudioProcessorPlayer (from juce_audio_utils, which we don't use).
17
20
  class GraphPlayer : public juce::AudioIODeviceCallback {
18
21
  public:
19
22
  void setProcessor(juce::AudioProcessor* p) { processor = p; }
@@ -65,13 +68,11 @@ private:
65
68
  @property (nonatomic, strong) dispatch_source_t beatTimer;
66
69
  @end
67
70
 
68
- // C++ members stored as raw pointers (ObjC class can't have C++ members inline).
69
- // IMPORTANT: No JUCE smart pointers or std::unique_ptr at file scope —
70
- // their constructors/destructors engage JUCE internals before MessageManager is ready.
71
+ // Raw pointers no JUCE smart pointers at file scope.
71
72
  static juce::AudioDeviceManager* sDeviceManager = nullptr;
72
73
  static GraphPlayer* sPlayer = nullptr;
73
74
  static juce::AudioProcessorGraph* sGraph = nullptr;
74
- static PolySynthProcessor* sSynth = nullptr; // owned by graph
75
+ static PolySynthProcessor* sSynth = nullptr;
75
76
  static TransportEngine* sTransport = nullptr;
76
77
 
77
78
  @implementation BeeperAudioEngine
@@ -94,24 +95,30 @@ static TransportEngine* sTransport = nullptr;
94
95
  }
95
96
 
96
97
  - (void)setup {
97
- NSAssert([NSThread isMainThread], @"setup() must be called on main thread");
98
+ if (![NSThread isMainThread]) {
99
+ NSLog(@"[BeeperAudioEngine] ERROR: setup() called off main thread, dispatching to main");
100
+ dispatch_sync(dispatch_get_main_queue(), ^{ [self setup]; });
101
+ return;
102
+ }
98
103
 
99
104
  if (self.isReady) {
100
105
  NSLog(@"[BeeperAudioEngine] Already initialized, skipping");
101
106
  return;
102
107
  }
103
108
 
104
- NSLog(@"[BeeperAudioEngine] Initializing JUCE audio stack...");
105
-
106
- // 0. Initialize JUCE on main thread (must happen before any JUCE API use)
107
- if (!sJuceInitialized) {
108
- NSLog(@"[BeeperAudioEngine] Initializing JUCE MessageManager on main thread...");
109
- juce::MessageManager::getInstance();
110
- juce::initialiseJuce_GUI();
111
- sJuceInitialized = true;
109
+ @try {
110
+ [self doSetup];
111
+ } @catch (NSException *exception) {
112
+ NSLog(@"[BeeperAudioEngine] EXCEPTION in setup: %@ — %@", exception.name, exception.reason);
113
+ [self cleanup];
112
114
  }
115
+ }
116
+
117
+ - (void)doSetup {
118
+ NSLog(@"[BeeperAudioEngine] Step 0: initialiseJuce_GUI");
119
+ juce::initialiseJuce_GUI();
113
120
 
114
- // 1. Configure AVAudioSession Playback only (no microphone needed)
121
+ NSLog(@"[BeeperAudioEngine] Step 1: AVAudioSession (Playback)");
115
122
  NSError *sessionError = nil;
116
123
  AVAudioSession *session = [AVAudioSession sharedInstance];
117
124
  [session setCategory:AVAudioSessionCategoryPlayback
@@ -120,128 +127,92 @@ static TransportEngine* sTransport = nullptr;
120
127
  if (sessionError) {
121
128
  NSLog(@"[BeeperAudioEngine] AVAudioSession category error: %@", sessionError);
122
129
  }
123
- sessionError = nil;
124
130
  [session setActive:YES error:&sessionError];
125
131
  if (sessionError) {
126
132
  NSLog(@"[BeeperAudioEngine] AVAudioSession activation error: %@", sessionError);
127
133
  }
128
134
 
129
- // 2. Create transport
135
+ NSLog(@"[BeeperAudioEngine] Step 2: TransportEngine");
130
136
  sTransport = new TransportEngine();
131
137
 
132
- // 3. Create AudioDeviceManager
138
+ NSLog(@"[BeeperAudioEngine] Step 3: AudioDeviceManager");
133
139
  sDeviceManager = new juce::AudioDeviceManager();
134
140
 
135
- // 4. Initialize device: 0 inputs, 2 outputs (stereo)
141
+ NSLog(@"[BeeperAudioEngine] Step 4: initialise(0, 2)");
136
142
  juce::String error = sDeviceManager->initialise(0, 2, nullptr, true);
137
143
  if (error.isNotEmpty()) {
138
144
  NSLog(@"[BeeperAudioEngine] AudioDeviceManager init failed: %s", error.toRawUTF8());
139
- delete sDeviceManager;
140
- sDeviceManager = nullptr;
141
- delete sTransport;
142
- sTransport = nullptr;
145
+ [self cleanup];
143
146
  return;
144
147
  }
145
148
 
146
- // 5. Create AudioProcessorGraph and add nodes BEFORE preparing
147
149
  double sr = sDeviceManager->getAudioDeviceSetup().sampleRate;
148
150
  int bufSize = sDeviceManager->getAudioDeviceSetup().bufferSize;
151
+ NSLog(@"[BeeperAudioEngine] Step 5: AudioProcessorGraph (sr=%.0f, buf=%d)", sr, bufSize);
149
152
 
150
153
  sGraph = new juce::AudioProcessorGraph();
151
154
  sGraph->setPlayConfigDetails(0, 2, sr, bufSize);
152
155
 
153
- // 6. Add PolySynthProcessor node
156
+ NSLog(@"[BeeperAudioEngine] Step 6: PolySynthProcessor");
154
157
  auto synthProcessor = std::make_unique<PolySynthProcessor>();
155
158
  sSynth = synthProcessor.get();
156
159
  sSynth->setTransport(sTransport);
160
+
161
+ NSLog(@"[BeeperAudioEngine] Step 7: addNode (synth)");
157
162
  auto synthNode = sGraph->addNode(std::move(synthProcessor));
158
163
 
159
- // 7. Add output node
164
+ NSLog(@"[BeeperAudioEngine] Step 8: addNode (output)");
160
165
  auto outputNode = sGraph->addNode(
161
166
  std::make_unique<juce::AudioProcessorGraph::AudioGraphIOProcessor>(
162
167
  juce::AudioProcessorGraph::AudioGraphIOProcessor::audioOutputNode));
163
168
 
164
- // 8. Connect synth → output (stereo: channels 0 and 1)
165
- sGraph->addConnection({
166
- {synthNode->nodeID, 0},
167
- {outputNode->nodeID, 0}
168
- });
169
- sGraph->addConnection({
170
- {synthNode->nodeID, 1},
171
- {outputNode->nodeID, 1}
172
- });
169
+ NSLog(@"[BeeperAudioEngine] Step 9: connections");
170
+ sGraph->addConnection({{synthNode->nodeID, 0}, {outputNode->nodeID, 0}});
171
+ sGraph->addConnection({{synthNode->nodeID, 1}, {outputNode->nodeID, 1}});
173
172
 
174
- // 9. Prepare graph AFTER all nodes/connections are set up
173
+ NSLog(@"[BeeperAudioEngine] Step 10: prepareToPlay");
175
174
  sGraph->prepareToPlay(sr, bufSize);
176
175
 
177
- // 10. Create player and attach to device
176
+ NSLog(@"[BeeperAudioEngine] Step 11: GraphPlayer + addAudioCallback");
178
177
  sPlayer = new GraphPlayer();
179
178
  sPlayer->setProcessor(sGraph);
180
179
  sDeviceManager->addAudioCallback(sPlayer);
181
180
 
182
181
  self.isReady = true;
183
- NSLog(@"[BeeperAudioEngine] JUCE audio stack initialized (sr=%.0f, buf=%d)",
184
- sDeviceManager->getAudioDeviceSetup().sampleRate,
185
- sDeviceManager->getAudioDeviceSetup().bufferSize);
182
+ NSLog(@"[BeeperAudioEngine] DONE audio stack initialized (sr=%.0f, buf=%d)", sr, bufSize);
186
183
  }
187
184
 
188
- - (void)shutdown {
189
- NSAssert([NSThread isMainThread], @"shutdown() must be called on main thread");
185
+ - (void)cleanup {
186
+ NSLog(@"[BeeperAudioEngine] Cleaning up after failure...");
187
+ if (sDeviceManager && sPlayer) sDeviceManager->removeAudioCallback(sPlayer);
188
+ delete sPlayer; sPlayer = nullptr;
189
+ sSynth = nullptr;
190
+ delete sGraph; sGraph = nullptr;
191
+ if (sDeviceManager) { sDeviceManager->closeAudioDevice(); delete sDeviceManager; sDeviceManager = nullptr; }
192
+ delete sTransport; sTransport = nullptr;
193
+ self.isReady = false;
194
+ }
190
195
 
196
+ - (void)shutdown {
197
+ if (![NSThread isMainThread]) return;
191
198
  if (!self.isReady) return;
192
199
 
193
200
  NSLog(@"[BeeperAudioEngine] Shutting down...");
194
-
195
201
  [self stopBeatTimer];
196
-
197
- if (sSynth) {
198
- sSynth->allNotesOff();
199
- }
200
-
201
- if (sDeviceManager && sPlayer) {
202
- sDeviceManager->removeAudioCallback(sPlayer);
203
- }
204
-
205
- if (sPlayer) {
206
- sPlayer->setProcessor(nullptr);
207
- delete sPlayer;
208
- sPlayer = nullptr;
209
- }
210
-
211
- sSynth = nullptr; // owned by graph, don't delete
212
- delete sGraph;
213
- sGraph = nullptr;
214
-
215
- if (sDeviceManager) {
216
- sDeviceManager->closeAudioDevice();
217
- delete sDeviceManager;
218
- sDeviceManager = nullptr;
219
- }
220
-
221
- if (sTransport) {
222
- delete sTransport;
223
- sTransport = nullptr;
224
- }
225
-
226
- self.isReady = false;
202
+ if (sSynth) sSynth->allNotesOff();
203
+ [self cleanup];
227
204
  NSLog(@"[BeeperAudioEngine] Shutdown complete");
228
205
  }
229
206
 
230
207
  // ── Synth forwarding ──
231
208
 
232
209
  - (void)noteOn:(int)note velocity:(int)velocity {
233
- if (!self.isReady || !sSynth) {
234
- NSLog(@"[BeeperAudioEngine] noteOn ignored — engine not initialized. Call setup() first.");
235
- return;
236
- }
210
+ if (!self.isReady || !sSynth) return;
237
211
  sSynth->noteOn(note, velocity);
238
212
  }
239
213
 
240
214
  - (void)noteOff:(int)note {
241
- if (!self.isReady || !sSynth) {
242
- NSLog(@"[BeeperAudioEngine] noteOff ignored — engine not initialized.");
243
- return;
244
- }
215
+ if (!self.isReady || !sSynth) return;
245
216
  sSynth->noteOff(note);
246
217
  }
247
218
 
@@ -252,84 +223,34 @@ static TransportEngine* sTransport = nullptr;
252
223
 
253
224
  - (void)setWaveform:(NSString *)waveform {
254
225
  if (!self.isReady || !sSynth) return;
255
- if ([waveform isEqualToString:@"square"]) {
256
- sSynth->setWaveform(Waveform::Square);
257
- } else if ([waveform isEqualToString:@"saw"]) {
258
- sSynth->setWaveform(Waveform::Saw);
259
- } else if ([waveform isEqualToString:@"triangle"]) {
260
- sSynth->setWaveform(Waveform::Triangle);
261
- } else {
262
- sSynth->setWaveform(Waveform::Sine);
263
- }
264
- }
265
-
266
- - (void)setFilterCutoff:(double)hz {
267
- if (self.isReady && sSynth) sSynth->setFilterCutoff(hz);
268
- }
269
-
270
- - (void)setFilterResonance:(double)q {
271
- if (self.isReady && sSynth) sSynth->setFilterResonance(q);
272
- }
273
-
274
- - (void)setAttack:(double)ms {
275
- if (self.isReady && sSynth) sSynth->setAttack(ms);
276
- }
277
-
278
- - (void)setDecay:(double)ms {
279
- if (self.isReady && sSynth) sSynth->setDecay(ms);
280
- }
281
-
282
- - (void)setSustain:(double)level {
283
- if (self.isReady && sSynth) sSynth->setSustain(level);
284
- }
285
-
286
- - (void)setRelease:(double)ms {
287
- if (self.isReady && sSynth) sSynth->setRelease(ms);
288
- }
289
-
290
- - (void)setDetune:(double)cents {
291
- if (self.isReady && sSynth) sSynth->setDetune(cents);
226
+ if ([waveform isEqualToString:@"square"]) sSynth->setWaveform(Waveform::Square);
227
+ else if ([waveform isEqualToString:@"saw"]) sSynth->setWaveform(Waveform::Saw);
228
+ else if ([waveform isEqualToString:@"triangle"]) sSynth->setWaveform(Waveform::Triangle);
229
+ else sSynth->setWaveform(Waveform::Sine);
292
230
  }
293
231
 
294
- - (void)setMasterLevel:(double)level {
295
- if (self.isReady && sSynth) sSynth->setMasterLevel(level);
296
- }
232
+ - (void)setFilterCutoff:(double)hz { if (self.isReady && sSynth) sSynth->setFilterCutoff(hz); }
233
+ - (void)setFilterResonance:(double)q { if (self.isReady && sSynth) sSynth->setFilterResonance(q); }
234
+ - (void)setAttack:(double)ms { if (self.isReady && sSynth) sSynth->setAttack(ms); }
235
+ - (void)setDecay:(double)ms { if (self.isReady && sSynth) sSynth->setDecay(ms); }
236
+ - (void)setSustain:(double)level { if (self.isReady && sSynth) sSynth->setSustain(level); }
237
+ - (void)setRelease:(double)ms { if (self.isReady && sSynth) sSynth->setRelease(ms); }
238
+ - (void)setDetune:(double)cents { if (self.isReady && sSynth) sSynth->setDetune(cents); }
239
+ - (void)setMasterLevel:(double)level { if (self.isReady && sSynth) sSynth->setMasterLevel(level); }
297
240
 
298
241
  // ── Transport ──
299
242
 
300
- - (void)startTransport {
301
- if (self.isReady && sTransport) sTransport->start();
302
- }
303
-
304
- - (void)stopTransport {
305
- if (self.isReady && sTransport) sTransport->stop();
306
- }
307
-
308
- - (void)setTempo:(double)bpm {
309
- if (self.isReady && sTransport) sTransport->setTempo(bpm);
310
- }
311
-
312
- - (void)setTransportPosition:(double)beat {
313
- if (self.isReady && sTransport) sTransport->setPosition(beat);
314
- }
315
-
316
- - (double)getTransportPosition {
317
- return (self.isReady && sTransport) ? sTransport->getPosition() : 0.0;
318
- }
319
-
320
- - (double)getTempo {
321
- return (self.isReady && sTransport) ? sTransport->getTempo() : 120.0;
322
- }
323
-
324
- - (BOOL)isTransportPlaying {
325
- return (self.isReady && sTransport) ? sTransport->isPlaying() : NO;
326
- }
243
+ - (void)startTransport { if (self.isReady && sTransport) sTransport->start(); }
244
+ - (void)stopTransport { if (self.isReady && sTransport) sTransport->stop(); }
245
+ - (void)setTempo:(double)bpm { if (self.isReady && sTransport) sTransport->setTempo(bpm); }
246
+ - (void)setTransportPosition:(double)beat { if (self.isReady && sTransport) sTransport->setPosition(beat); }
247
+ - (double)getTransportPosition { return (self.isReady && sTransport) ? sTransport->getPosition() : 0.0; }
248
+ - (double)getTempo { return (self.isReady && sTransport) ? sTransport->getTempo() : 120.0; }
249
+ - (BOOL)isTransportPlaying { return (self.isReady && sTransport) ? sTransport->isPlaying() : NO; }
327
250
 
328
251
  // ── Beat events ──
329
252
 
330
- - (void)setBeatCallback:(BeatCallback)callback {
331
- self.beatCallback = callback;
332
- }
253
+ - (void)setBeatCallback:(BeatCallback)callback { self.beatCallback = callback; }
333
254
 
334
255
  - (void)startBeatTimer {
335
256
  if (self.beatTimer) return;
@@ -357,8 +278,6 @@ static TransportEngine* sTransport = nullptr;
357
278
  self.beatCallback = nil;
358
279
  }
359
280
 
360
- - (void)dealloc {
361
- [self shutdown];
362
- }
281
+ - (void)dealloc { [self shutdown]; }
363
282
 
364
283
  @end
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-juce",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Realtime DSP w/C++ & JUCE",
5
5
  "type": "module",
6
6
  "main": "build/index.js",