expo-juce 0.2.23 → 0.2.25

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.
@@ -12,12 +12,17 @@ static const int MAX_HARMONICS = 8;
12
12
  // ── Simple one-pole low-pass filter ───────────────────────────────
13
13
  class OnePoleLPF {
14
14
  public:
15
- OnePoleLPF() : y1(0.0), cutoff(20000.0), resonance(0.0) {}
15
+ OnePoleLPF() : y1(0.0), cutoff(20000.0), resonance(0.0), pendingReset(false) {}
16
16
 
17
17
  void setCutoff(double hz) { cutoff.store(hz); }
18
18
  void setResonance(double q) { resonance.store(q); }
19
19
 
20
20
  float process(float input, double sampleRate) {
21
+ // Handle reset on audio thread to avoid data race
22
+ if (pendingReset.exchange(false)) {
23
+ y1 = 0.0;
24
+ }
25
+
21
26
  double fc = cutoff.load();
22
27
 
23
28
  // Clamp cutoff to Nyquist
@@ -34,12 +39,14 @@ public:
34
39
  return (float)y1;
35
40
  }
36
41
 
37
- void reset() { y1 = 0.0; }
42
+ // Signal reset from main thread applied on next audio render
43
+ void reset() { pendingReset.store(true); }
38
44
 
39
45
  private:
40
- double y1;
46
+ double y1; // Only touched by audio thread
41
47
  std::atomic<double> cutoff;
42
48
  std::atomic<double> resonance;
49
+ std::atomic<bool> pendingReset;
43
50
  };
44
51
 
45
52
  // ── ADSR Envelope ─────────────────────────────────────────────────
@@ -130,7 +137,7 @@ public:
130
137
  }
131
138
 
132
139
  void noteOn() {
133
- phase = 0.0;
140
+ pendingNoteOn.store(true);
134
141
  filter.reset();
135
142
  envelope.noteOn();
136
143
  }
@@ -145,6 +152,11 @@ public:
145
152
  void setFilterResonance(double q) { filter.setResonance(q); }
146
153
 
147
154
  float getNextSample(double sampleRate) {
155
+ // Handle phase reset on audio thread to avoid data race
156
+ if (pendingNoteOn.exchange(false)) {
157
+ phase = 0.0;
158
+ }
159
+
148
160
  float envGain = envelope.process(sampleRate);
149
161
  if (envGain < 0.00001f && !envelope.isActive()) {
150
162
  return 0.0f;
@@ -203,7 +215,8 @@ public:
203
215
  private:
204
216
  std::atomic<double> frequency;
205
217
  std::atomic<double> level;
206
- double phase;
218
+ double phase; // Only touched by audio thread
219
+ std::atomic<bool> pendingNoteOn{false};
207
220
  std::atomic<Waveform> waveform;
208
221
  std::atomic<double> detuneCents;
209
222
  std::atomic<double> harmonics[MAX_HARMONICS];
@@ -300,79 +313,126 @@ struct BeatState {
300
313
  }
301
314
 
302
315
  - (void)initialize {
316
+ [self initializeWithRetries:3];
317
+ }
318
+
319
+ - (void)initializeWithRetries:(int)maxRetries {
303
320
  if (self.audioEngine) {
304
321
  NSLog(@"[ExpoJuce] Audio engine already initialized, skipping");
305
322
  return;
306
323
  }
307
324
 
308
- NSLog(@"[ExpoJuce] Initializing audio engine...");
309
-
310
- @try {
311
- NSError *error = nil;
325
+ // Listen for audio interruptions (phone calls, Siri, etc.)
326
+ [[NSNotificationCenter defaultCenter] removeObserver:self
327
+ name:AVAudioSessionInterruptionNotification
328
+ object:nil];
329
+ [[NSNotificationCenter defaultCenter] addObserver:self
330
+ selector:@selector(handleAudioInterruption:)
331
+ name:AVAudioSessionInterruptionNotification
332
+ object:[AVAudioSession sharedInstance]];
333
+
334
+ for (int attempt = 1; attempt <= maxRetries; attempt++) {
335
+ NSLog(@"[ExpoJuce] Initializing audio engine (attempt %d/%d)...", attempt, maxRetries);
336
+
337
+ @try {
338
+ NSError *error = nil;
339
+
340
+ AVAudioSession *audioSession = [AVAudioSession sharedInstance];
341
+ [audioSession setCategory:AVAudioSessionCategoryPlayback
342
+ withOptions:AVAudioSessionCategoryOptionMixWithOthers
343
+ error:&error];
344
+ if (error) {
345
+ NSLog(@"[ExpoJuce] Error setting audio session category: %@", error);
346
+ continue;
347
+ }
312
348
 
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
- }
349
+ [audioSession setActive:YES error:&error];
350
+ if (error) {
351
+ NSLog(@"[ExpoJuce] Error activating audio session: %@", error);
352
+ continue;
353
+ }
321
354
 
322
- [audioSession setActive:YES error:&error];
323
- if (error) {
324
- NSLog(@"[ExpoJuce] Error activating audio session: %@", error);
325
- error = nil;
326
- }
355
+ self.audioEngine = [[AVAudioEngine alloc] init];
327
356
 
328
- self.audioEngine = [[AVAudioEngine alloc] init];
357
+ double sampleRate = audioSession.sampleRate > 0 ? audioSession.sampleRate : 44100.0;
358
+ AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:sampleRate channels:2];
359
+ NSLog(@"[ExpoJuce] Using sample rate: %.0f", sampleRate);
329
360
 
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);
361
+ SynthEngine *eng = self.engine;
362
+ TransportEngine *trans = self.transport;
333
363
 
334
- SynthEngine *eng = self.engine;
335
- TransportEngine *trans = self.transport;
364
+ if (!self.beatState) {
365
+ self.beatState = new BeatState();
366
+ }
367
+ BeatState *bs = self.beatState;
368
+
369
+ self.sourceNode = [[AVAudioSourceNode alloc] initWithRenderBlock:^OSStatus(BOOL *isSilence, const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, AudioBufferList *outputData) {
370
+ double sr = sampleRate;
371
+ float *leftChannel = (float *)outputData->mBuffers[0].mData;
372
+ float *rightChannel = (outputData->mNumberBuffers > 1) ? (float *)outputData->mBuffers[1].mData : leftChannel;
373
+
374
+ for (AVAudioFrameCount i = 0; i < frameCount; i++) {
375
+ if (trans->advance(sr)) {
376
+ bs->crossed.store(true);
377
+ bs->beat.store(trans->getPosition());
378
+ bs->bpm.store(trans->getTempo());
379
+ }
336
380
 
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());
381
+ float sample = eng->getNextSample(sr);
382
+ leftChannel[i] = sample;
383
+ rightChannel[i] = sample;
352
384
  }
353
385
 
354
- float sample = eng->getNextSample(sr);
355
- leftChannel[i] = sample;
356
- rightChannel[i] = sample;
386
+ return noErr;
387
+ }];
388
+
389
+ [self.audioEngine attachNode:self.sourceNode];
390
+ [self.audioEngine connect:self.sourceNode to:self.audioEngine.mainMixerNode format:format];
391
+ [self.audioEngine connect:self.audioEngine.mainMixerNode to:self.audioEngine.outputNode format:nil];
392
+
393
+ [self.audioEngine prepare];
394
+ [self.audioEngine startAndReturnError:&error];
395
+ if (error) {
396
+ NSLog(@"[ExpoJuce] Error starting audio engine: %@", error);
397
+ [self tearDownAudioEngine];
398
+ continue;
357
399
  }
358
400
 
359
- return noErr;
360
- }];
401
+ NSLog(@"[ExpoJuce] Audio engine started successfully");
402
+ return;
361
403
 
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];
404
+ } @catch (NSException *exception) {
405
+ NSLog(@"[ExpoJuce] CRASH in audio init (attempt %d): %@ — %@", attempt, exception.name, exception.reason);
406
+ [self tearDownAudioEngine];
407
+ }
408
+ }
365
409
 
366
- [self.audioEngine prepare];
410
+ NSLog(@"[ExpoJuce] Audio engine failed to initialize after %d attempts", maxRetries);
411
+ }
412
+
413
+ - (void)tearDownAudioEngine {
414
+ if (self.audioEngine) {
415
+ [self.audioEngine stop];
416
+ self.audioEngine = nil;
417
+ }
418
+ self.sourceNode = nil;
419
+ }
420
+
421
+ - (void)handleAudioInterruption:(NSNotification *)notification {
422
+ NSDictionary *info = notification.userInfo;
423
+ AVAudioSessionInterruptionType type = (AVAudioSessionInterruptionType)[info[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
424
+
425
+ if (type == AVAudioSessionInterruptionTypeBegan) {
426
+ NSLog(@"[ExpoJuce] Audio interrupted (phone call, Siri, etc.)");
427
+ } else if (type == AVAudioSessionInterruptionTypeEnded) {
428
+ NSLog(@"[ExpoJuce] Audio interruption ended, restarting engine...");
429
+ NSError *error = nil;
367
430
  [self.audioEngine startAndReturnError:&error];
368
431
  if (error) {
369
- NSLog(@"[ExpoJuce] Error starting audio engine: %@", error);
370
- } else {
371
- NSLog(@"[ExpoJuce] Audio engine started successfully");
432
+ NSLog(@"[ExpoJuce] Failed to restart after interruption: %@, reinitializing...", error);
433
+ [self tearDownAudioEngine];
434
+ [self initializeWithRetries:2];
372
435
  }
373
-
374
- } @catch (NSException *exception) {
375
- NSLog(@"[ExpoJuce] CRASH in audio init: %@ — %@", exception.name, exception.reason);
376
436
  }
377
437
  }
378
438
 
@@ -404,6 +464,7 @@ struct BeatState {
404
464
  }
405
465
 
406
466
  - (void)shutdown {
467
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
407
468
  [self stopBeatTimer];
408
469
  if (self.audioEngine) {
409
470
  [self.audioEngine stop];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-juce",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "description": "Realtime DSP w/C++ & JUCE",
5
5
  "type": "module",
6
6
  "main": "build/index.js",