expo-juce 0.2.22 → 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 +117 -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,14 +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
|
-
NSLog(@"[ExpoJuce] setFrequency: %.1f, engine=%p", frequency, self.engine);
|
|
322
431
|
if (self.engine) self.engine->setFrequency(frequency);
|
|
323
|
-
NSLog(@"[ExpoJuce] setFrequency done");
|
|
324
432
|
}
|
|
325
433
|
|
|
326
434
|
- (void)setLevel:(double)level {
|
|
@@ -352,15 +460,11 @@ private:
|
|
|
352
460
|
}
|
|
353
461
|
|
|
354
462
|
- (void)noteOn {
|
|
355
|
-
NSLog(@"[ExpoJuce] noteOn, engine=%p", self.engine);
|
|
356
463
|
if (self.engine) self.engine->noteOn();
|
|
357
|
-
NSLog(@"[ExpoJuce] noteOn done");
|
|
358
464
|
}
|
|
359
465
|
|
|
360
466
|
- (void)noteOff {
|
|
361
|
-
NSLog(@"[ExpoJuce] noteOff, engine=%p", self.engine);
|
|
362
467
|
if (self.engine) self.engine->noteOff();
|
|
363
|
-
NSLog(@"[ExpoJuce] noteOff done");
|
|
364
468
|
}
|
|
365
469
|
|
|
366
470
|
// ── DSP Params ────────────────────────────────────────────────────
|