expo-juce 0.2.21 → 0.2.23

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,6 +8,8 @@ typedef void(^ExpoJuceBeatBlock)(double beat, double bpm);
8
8
 
9
9
  + (void)setup;
10
10
  + (void)shutdown;
11
+ + (void)startBeatTimer;
12
+ + (void)stopBeatTimer;
11
13
 
12
14
  // ── Synth ─────────────────────────────────────────────────────────
13
15
  + (void)playToneWithFrequency:(double)frequency duration:(double)duration;
@@ -11,6 +11,14 @@
11
11
  [[JuceToneGenerator sharedInstance] shutdown];
12
12
  }
13
13
 
14
+ + (void)startBeatTimer {
15
+ [[JuceToneGenerator sharedInstance] startBeatTimer];
16
+ }
17
+
18
+ + (void)stopBeatTimer {
19
+ [[JuceToneGenerator sharedInstance] stopBeatTimer];
20
+ }
21
+
14
22
  // ── Synth ─────────────────────────────────────────────────────────
15
23
 
16
24
  + (void)playToneWithFrequency:(double)frequency duration:(double)duration {
@@ -1,6 +1,8 @@
1
1
  import ExpoModulesCore
2
2
 
3
3
  public class ExpoJuceModule: Module {
4
+ private var hasListeners = false
5
+
4
6
  public func definition() -> ModuleDefinition {
5
7
  Name("ExpoJuce")
6
8
 
@@ -8,6 +10,7 @@ public class ExpoJuceModule: Module {
8
10
  // Initializing AVAudioEngine in OnCreate can crash before JS is ready.
9
11
 
10
12
  OnDestroy {
13
+ ExpoJuceBridge.stopBeatTimer()
11
14
  ExpoJuceBridge.shutdown()
12
15
  }
13
16
 
@@ -17,15 +20,33 @@ public class ExpoJuceModule: Module {
17
20
 
18
21
  Events("onChange", "onBeat")
19
22
 
23
+ // Beat timer only runs while JS has active listeners.
24
+ // This prevents sendEvent from firing before listeners are attached (crash).
25
+ OnStartObserving {
26
+ self.hasListeners = true
27
+ ExpoJuceBridge.startBeatTimer()
28
+ }
29
+
30
+ OnStopObserving {
31
+ self.hasListeners = false
32
+ ExpoJuceBridge.stopBeatTimer()
33
+ }
34
+
20
35
  Function("hello") {
21
36
  return "Hello world!"
22
37
  }
23
38
 
24
39
  // ── Synth ───────────────────────────────────────────────────────
25
40
 
26
- Function("setup") {
27
- NSLog("[ExpoJuce] setup called from JS (NO-OP diagnostic)")
28
- // DIAGNOSTIC: skip all native calls to isolate crash
41
+ Function("setup") { [weak self] in
42
+ ExpoJuceBridge.setup()
43
+ ExpoJuceBridge.setBeatCallback { [weak self] beat, bpm in
44
+ guard let self = self, self.hasListeners else { return }
45
+ self.sendEvent("onBeat", [
46
+ "beat": beat,
47
+ "bpm": bpm,
48
+ ])
49
+ }
29
50
  }
30
51
 
31
52
  Function("playTone") { (frequency: Double, duration: Double) -> String in
@@ -7,6 +7,8 @@ typedef void(^BeatCallback)(double beat, double bpm);
7
7
  + (instancetype)sharedInstance;
8
8
  - (void)initialize;
9
9
  - (void)shutdown;
10
+ - (void)startBeatTimer;
11
+ - (void)stopBeatTimer;
10
12
 
11
13
  // ── Synth ─────────────────────────────────────────────────────────
12
14
  - (void)playToneWithFrequency:(double)frequency duration:(double)duration;
@@ -255,11 +255,18 @@ private:
255
255
 
256
256
  // ── Objective-C Wrapper ───────────────────────────────────────────
257
257
 
258
+ struct BeatState {
259
+ std::atomic<bool> crossed{false};
260
+ std::atomic<double> beat{0.0};
261
+ std::atomic<double> bpm{120.0};
262
+ };
263
+
258
264
  @interface JuceToneGenerator ()
259
265
  @property (nonatomic, strong) AVAudioEngine *audioEngine;
260
266
  @property (nonatomic, strong) AVAudioSourceNode *sourceNode;
261
267
  @property (nonatomic, assign) SynthEngine *engine;
262
268
  @property (nonatomic, assign) TransportEngine *transport;
269
+ @property (nonatomic, assign) BeatState *beatState;
263
270
  @property (nonatomic, strong) dispatch_source_t beatTimer;
264
271
  @property (nonatomic, copy) BeatCallback beatCallback;
265
272
  @end
@@ -289,20 +296,115 @@ private:
289
296
  [self shutdown];
290
297
  if (_engine) { delete _engine; _engine = nullptr; }
291
298
  if (_transport) { delete _transport; _transport = nullptr; }
299
+ if (_beatState) { delete _beatState; _beatState = nullptr; }
292
300
  }
293
301
 
294
302
  - (void)initialize {
295
- // DIAGNOSTIC: no-op to isolate crash source.
296
- // If app still crashes on pad tap, the problem is in setFrequency/noteOn.
297
- // If app stops crashing, the problem is in AVAudioEngine setup.
298
- NSLog(@"[ExpoJuce] initialize called (NO-OP diagnostic mode)");
303
+ if (self.audioEngine) {
304
+ NSLog(@"[ExpoJuce] Audio engine already initialized, skipping");
305
+ return;
306
+ }
307
+
308
+ NSLog(@"[ExpoJuce] Initializing audio engine...");
309
+
310
+ @try {
311
+ NSError *error = nil;
312
+
313
+ AVAudioSession *audioSession = [AVAudioSession sharedInstance];
314
+ [audioSession setCategory:AVAudioSessionCategoryPlayback
315
+ withOptions:AVAudioSessionCategoryOptionMixWithOthers
316
+ error:&error];
317
+ if (error) {
318
+ NSLog(@"[ExpoJuce] Error setting audio session category: %@", error);
319
+ error = nil;
320
+ }
321
+
322
+ [audioSession setActive:YES error:&error];
323
+ if (error) {
324
+ NSLog(@"[ExpoJuce] Error activating audio session: %@", error);
325
+ error = nil;
326
+ }
327
+
328
+ self.audioEngine = [[AVAudioEngine alloc] init];
329
+
330
+ double sampleRate = audioSession.sampleRate > 0 ? audioSession.sampleRate : 44100.0;
331
+ AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:sampleRate channels:2];
332
+ NSLog(@"[ExpoJuce] Using sample rate: %.0f", sampleRate);
333
+
334
+ SynthEngine *eng = self.engine;
335
+ TransportEngine *trans = self.transport;
336
+
337
+ if (!self.beatState) {
338
+ self.beatState = new BeatState();
339
+ }
340
+ BeatState *bs = self.beatState;
341
+
342
+ self.sourceNode = [[AVAudioSourceNode alloc] initWithRenderBlock:^OSStatus(BOOL *isSilence, const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, AudioBufferList *outputData) {
343
+ double sr = sampleRate;
344
+ float *leftChannel = (float *)outputData->mBuffers[0].mData;
345
+ float *rightChannel = (outputData->mNumberBuffers > 1) ? (float *)outputData->mBuffers[1].mData : leftChannel;
346
+
347
+ for (AVAudioFrameCount i = 0; i < frameCount; i++) {
348
+ if (trans->advance(sr)) {
349
+ bs->crossed.store(true);
350
+ bs->beat.store(trans->getPosition());
351
+ bs->bpm.store(trans->getTempo());
352
+ }
353
+
354
+ float sample = eng->getNextSample(sr);
355
+ leftChannel[i] = sample;
356
+ rightChannel[i] = sample;
357
+ }
358
+
359
+ return noErr;
360
+ }];
361
+
362
+ [self.audioEngine attachNode:self.sourceNode];
363
+ [self.audioEngine connect:self.sourceNode to:self.audioEngine.mainMixerNode format:format];
364
+ [self.audioEngine connect:self.audioEngine.mainMixerNode to:self.audioEngine.outputNode format:nil];
365
+
366
+ [self.audioEngine prepare];
367
+ [self.audioEngine startAndReturnError:&error];
368
+ if (error) {
369
+ NSLog(@"[ExpoJuce] Error starting audio engine: %@", error);
370
+ } else {
371
+ NSLog(@"[ExpoJuce] Audio engine started successfully");
372
+ }
373
+
374
+ } @catch (NSException *exception) {
375
+ NSLog(@"[ExpoJuce] CRASH in audio init: %@ — %@", exception.name, exception.reason);
376
+ }
299
377
  }
300
378
 
301
- - (void)shutdown {
379
+ - (void)stopBeatTimer {
302
380
  if (self.beatTimer) {
303
381
  dispatch_source_cancel(self.beatTimer);
304
382
  self.beatTimer = nil;
305
383
  }
384
+ self.beatCallback = nil;
385
+ }
386
+
387
+ - (void)startBeatTimer {
388
+ if (self.beatTimer) return;
389
+ if (!self.beatState) return;
390
+
391
+ BeatState *bs = self.beatState;
392
+ self.beatTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
393
+ dispatch_source_set_timer(self.beatTimer, DISPATCH_TIME_NOW, 16 * NSEC_PER_MSEC, 1 * NSEC_PER_MSEC);
394
+ __weak JuceToneGenerator *weakSelf = self;
395
+ dispatch_source_set_event_handler(self.beatTimer, ^{
396
+ JuceToneGenerator *strongSelf = weakSelf;
397
+ if (!strongSelf) return;
398
+ BeatCallback cb = strongSelf.beatCallback;
399
+ if (bs->crossed.exchange(false) && cb) {
400
+ cb(bs->beat.load(), bs->bpm.load());
401
+ }
402
+ });
403
+ dispatch_resume(self.beatTimer);
404
+ }
405
+
406
+ - (void)shutdown {
407
+ [self stopBeatTimer];
306
408
  if (self.audioEngine) {
307
409
  [self.audioEngine stop];
308
410
  self.audioEngine = nil;
@@ -313,13 +415,20 @@ private:
313
415
  // ── Synth Methods ─────────────────────────────────────────────────
314
416
 
315
417
  - (void)playToneWithFrequency:(double)frequency duration:(double)duration {
316
- NSLog(@"[ExpoJuce] playTone freq=%.1f dur=%.0f (NO-OP)", frequency, duration);
317
- // DIAGNOSTIC: skip C++ call
418
+ NSLog(@"[ExpoJuce] playTone freq=%.1f dur=%.0f", frequency, duration);
419
+ if (!self.engine) return;
420
+
421
+ self.engine->setFrequency(frequency);
422
+ self.engine->noteOn();
423
+
424
+ __weak JuceToneGenerator *weakSelf = self;
425
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration / 1000.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
426
+ if (weakSelf.engine) weakSelf.engine->noteOff();
427
+ });
318
428
  }
319
429
 
320
430
  - (void)setFrequency:(double)frequency {
321
- NSLog(@"[ExpoJuce] setFrequency: %.1f (NO-OP)", frequency);
322
- // DIAGNOSTIC: skip C++ call
431
+ if (self.engine) self.engine->setFrequency(frequency);
323
432
  }
324
433
 
325
434
  - (void)setLevel:(double)level {
@@ -351,13 +460,11 @@ private:
351
460
  }
352
461
 
353
462
  - (void)noteOn {
354
- NSLog(@"[ExpoJuce] noteOn (NO-OP)");
355
- // DIAGNOSTIC: skip C++ call
463
+ if (self.engine) self.engine->noteOn();
356
464
  }
357
465
 
358
466
  - (void)noteOff {
359
- NSLog(@"[ExpoJuce] noteOff (NO-OP)");
360
- // DIAGNOSTIC: skip C++ call
467
+ if (self.engine) self.engine->noteOff();
361
468
  }
362
469
 
363
470
  // ── DSP Params ────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-juce",
3
- "version": "0.2.21",
3
+ "version": "0.2.23",
4
4
  "description": "Realtime DSP w/C++ & JUCE",
5
5
  "type": "module",
6
6
  "main": "build/index.js",