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.
- package/ios/ExpoJuceBridge.h +2 -0
- package/ios/ExpoJuceBridge.m +8 -0
- package/ios/ExpoJuceModule.swift +24 -3
- package/ios/JuceToneGenerator.h +2 -0
- package/ios/JuceToneGenerator.mm +120 -13
- package/package.json +1 -1
package/ios/ExpoJuceBridge.h
CHANGED
|
@@ -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;
|
package/ios/ExpoJuceBridge.m
CHANGED
|
@@ -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 {
|
package/ios/ExpoJuceModule.swift
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
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
|
package/ios/JuceToneGenerator.h
CHANGED
|
@@ -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;
|
package/ios/JuceToneGenerator.mm
CHANGED
|
@@ -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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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)
|
|
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
|
|
317
|
-
|
|
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
|
-
|
|
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
|
-
|
|
355
|
-
// DIAGNOSTIC: skip C++ call
|
|
463
|
+
if (self.engine) self.engine->noteOn();
|
|
356
464
|
}
|
|
357
465
|
|
|
358
466
|
- (void)noteOff {
|
|
359
|
-
|
|
360
|
-
// DIAGNOSTIC: skip C++ call
|
|
467
|
+
if (self.engine) self.engine->noteOff();
|
|
361
468
|
}
|
|
362
469
|
|
|
363
470
|
// ── DSP Params ────────────────────────────────────────────────────
|