expo-juce 0.2.24 → 0.3.0

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.
@@ -17,25 +17,26 @@ Pod::Spec.new do |s|
17
17
 
18
18
  s.dependency 'ExpoModulesCore'
19
19
 
20
- # Our source files: Swift module, ObjC bridge, C++ tone generator
20
+ # Our source files: Swift module, ObjC bridge, C++ tone generator, JUCE compilation unit
21
21
  s.source_files = "*.{h,m,mm,swift}"
22
- s.exclude_files = ["JuceConfig.h", "JuceModules.mm"]
23
22
  s.requires_arc = true
24
23
 
25
- # JUCE sources preserved on disk for future use but NOT compiled —
26
- # current engine uses pure C++ + AVFoundation, no JUCE APIs.
27
- # Compiling JUCE modules (especially juce_events) causes launch crashes
28
- # due to static initializers conflicting with React Native's app lifecycle.
24
+ # JUCE modules compiled: juce_core, juce_audio_basics, juce_audio_formats, juce_dsp
25
+ # NOT compiled: juce_events, juce_data_structures (static initializer conflicts with RN)
26
+ # JuceModules.mm is a single translation unit that #includes the JUCE .cpp files.
29
27
  s.preserve_paths = ["juce_modules/**"]
30
28
 
31
29
  s.frameworks = 'AVFoundation', 'AudioToolbox', 'CoreAudio',
32
- 'Foundation', 'Accelerate'
30
+ 'Foundation', 'Accelerate', 'CoreMIDI'
33
31
  s.libraries = 'c++'
34
32
 
35
33
  s.pod_target_xcconfig = {
36
34
  'DEFINES_MODULE' => 'YES',
37
35
  'SWIFT_COMPILATION_MODE' => 'wholemodule',
38
36
  'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',
39
- 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)"',
37
+ 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)" "$(PODS_TARGET_SRCROOT)/juce_modules"',
38
+ # Suppress JUCE deprecation warnings and implicit conversion warnings in vendored code
39
+ 'GCC_WARN_ABOUT_DEPRECATED_DECLARATIONS' => 'NO',
40
+ 'OTHER_CPLUSPLUSFLAGS' => '-Wno-shorten-64-to-32 -Wno-comma -Wno-deprecated-declarations',
40
41
  }
41
42
  end
package/ios/JuceConfig.h CHANGED
@@ -16,14 +16,19 @@
16
16
  #define JUCE_USE_DARK_SPLASH_SCREEN 0
17
17
  #define JUCE_STRICT_REFCOUNTEDPOINTER 1
18
18
 
19
- // Module availability flags
19
+ // Module availability flags — only safe modules (no juce_events)
20
20
  #define JUCE_MODULE_AVAILABLE_juce_core 1
21
- #define JUCE_MODULE_AVAILABLE_juce_events 1
22
- #define JUCE_MODULE_AVAILABLE_juce_data_structures 1
23
21
  #define JUCE_MODULE_AVAILABLE_juce_audio_basics 1
24
22
  #define JUCE_MODULE_AVAILABLE_juce_audio_formats 1
25
23
  #define JUCE_MODULE_AVAILABLE_juce_dsp 1
26
24
 
25
+ // Explicitly mark dangerous modules as unavailable
26
+ #define JUCE_MODULE_AVAILABLE_juce_events 0
27
+ #define JUCE_MODULE_AVAILABLE_juce_data_structures 0
28
+ #define JUCE_MODULE_AVAILABLE_juce_gui_basics 0
29
+ #define JUCE_MODULE_AVAILABLE_juce_audio_processors 0
30
+ #define JUCE_MODULE_AVAILABLE_juce_audio_devices 0
31
+
27
32
  // Not a standalone JUCE app — embedded in React Native
28
33
  #define JUCE_STANDALONE_APPLICATION 0
29
34
 
@@ -37,3 +42,4 @@
37
42
  #define JUCE_USE_MP3AUDIOFORMAT 0
38
43
  #define JUCE_USE_LAME_AUDIO_FORMAT 0
39
44
  #define JUCE_USE_WINDOWS_MEDIA_FORMAT 0
45
+ #define JUCE_USE_FLAC 0
@@ -1,11 +1,14 @@
1
- // JuceModules.mm — Compiles all vendored JUCE modules as a single ObjC++ translation unit.
1
+ // JuceModules.mm — Compiles vendored JUCE modules as a single ObjC++ translation unit.
2
2
  // The .mm extension ensures Objective-C++ mode so JUCE's Apple platform code compiles correctly.
3
+ //
4
+ // Only safe modules are compiled here. juce_events is EXCLUDED because its
5
+ // static MessageManager initializer conflicts with React Native's app lifecycle.
6
+ // The dependency chain is: juce_dsp -> juce_audio_formats -> juce_audio_basics -> juce_core
7
+ // None of these modules depend on juce_events.
3
8
 
4
9
  #include "JuceConfig.h"
5
10
 
6
11
  #include "juce_modules/juce_core/juce_core.cpp"
7
- #include "juce_modules/juce_events/juce_events.cpp"
8
- #include "juce_modules/juce_data_structures/juce_data_structures.cpp"
9
12
  #include "juce_modules/juce_audio_basics/juce_audio_basics.cpp"
10
13
  #include "juce_modules/juce_audio_formats/juce_audio_formats.cpp"
11
14
  #include "juce_modules/juce_dsp/juce_dsp.cpp"
@@ -4,214 +4,244 @@
4
4
  #include <cmath>
5
5
  #include <algorithm>
6
6
 
7
+ #include "JuceConfig.h"
8
+ #include <juce_core/juce_core.h>
9
+ #include <juce_audio_basics/juce_audio_basics.h>
10
+ #include <juce_dsp/juce_dsp.h>
11
+
7
12
  // ── Waveform Types ────────────────────────────────────────────────
8
13
  enum class Waveform { Sine, Square, Saw, Triangle };
9
14
 
10
15
  static const int MAX_HARMONICS = 8;
11
16
 
12
- // ── Simple one-pole low-pass filter ───────────────────────────────
13
- class OnePoleLPF {
17
+ // ── Synth Engine (JUCE DSP) ──────────────────────────────────────
18
+ //
19
+ // Thread safety contract:
20
+ // - Main thread: calls set*() methods (atomic parameter updates)
21
+ // - Audio thread: calls getNextSample() (reads atomics, mutates DSP state)
22
+ // - DSP objects (oscillator, filter, adsr) are ONLY touched by the audio thread
23
+ // - prepare() is called once from main thread BEFORE audio thread starts
24
+ //
25
+ class SynthEngine {
14
26
  public:
15
- OnePoleLPF() : y1(0.0), cutoff(20000.0), resonance(0.0) {}
16
-
17
- void setCutoff(double hz) { cutoff.store(hz); }
18
- void setResonance(double q) { resonance.store(q); }
19
-
20
- float process(float input, double sampleRate) {
21
- double fc = cutoff.load();
22
-
23
- // Clamp cutoff to Nyquist
24
- if (fc > sampleRate * 0.49) fc = sampleRate * 0.49;
25
- if (fc < 20.0) fc = 20.0;
26
-
27
- // Compute coefficient (simple RC low-pass)
28
- double w = 2.0 * M_PI * fc / sampleRate;
29
- double g = w / (1.0 + w);
30
-
31
- // One-pole filter
32
- y1 = y1 + g * (input - y1);
33
-
34
- return (float)y1;
27
+ SynthEngine()
28
+ : frequency(440.0), level(0.5),
29
+ waveform(Waveform::Sine), detuneCents(0.0),
30
+ pendingNoteOn(false), pendingNoteOff(false),
31
+ filterCutoff(20000.0), filterResonance(0.0),
32
+ attackMs(10.0), releaseMs(200.0),
33
+ pendingParamUpdate(true)
34
+ {
35
+ for (int i = 0; i < MAX_HARMONICS; i++) {
36
+ harmonics[i] = (i == 0) ? 1.0 : 0.0;
37
+ }
35
38
  }
36
39
 
37
- void reset() { y1 = 0.0; }
38
-
39
- private:
40
- double y1;
41
- std::atomic<double> cutoff;
42
- std::atomic<double> resonance;
43
- };
44
-
45
- // ── ADSR Envelope ─────────────────────────────────────────────────
46
- class ADSREnvelope {
47
- public:
48
- enum class Stage { Idle, Attack, Sustain, Release };
49
-
50
- ADSREnvelope()
51
- : attackMs(10.0), releaseMs(200.0),
52
- stage(Stage::Idle), gain(0.0) {}
40
+ // Called once from main thread before audio engine starts
41
+ void prepare(double sampleRate, int blockSize) {
42
+ juce::dsp::ProcessSpec spec;
43
+ spec.sampleRate = sampleRate;
44
+ spec.maximumBlockSize = (juce::uint32)blockSize;
45
+ spec.numChannels = 1;
53
46
 
54
- void setAttack(double ms) { attackMs.store(std::max(1.0, ms)); }
55
- void setRelease(double ms) { releaseMs.store(std::max(1.0, ms)); }
47
+ // Pre-initialize all waveform oscillators with lookup tables (no allocation on audio thread)
48
+ sineOsc.initialise([](float x) { return std::sin(x); }, 2048);
49
+ sineOsc.prepare(spec);
56
50
 
57
- void noteOn() { stage.store(Stage::Attack); }
58
- void noteOff() { stage.store(Stage::Release); }
59
- bool isActive() { return stage.load() != Stage::Idle; }
51
+ squareOsc.initialise([](float x) -> float { return x < 0.0f ? -1.0f : 1.0f; }, 2048);
52
+ squareOsc.prepare(spec);
60
53
 
61
- float process(double sampleRate) {
62
- Stage s = stage.load();
54
+ sawOsc.initialise([](float x) -> float { return x / juce::MathConstants<float>::pi; }, 2048);
55
+ sawOsc.prepare(spec);
63
56
 
64
- switch (s) {
65
- case Stage::Idle:
66
- gain = 0.0;
67
- break;
57
+ triOsc.initialise([](float x) -> float {
58
+ return 2.0f * std::abs(x) / juce::MathConstants<float>::pi - 1.0f;
59
+ }, 2048);
60
+ triOsc.prepare(spec);
68
61
 
69
- case Stage::Attack: {
70
- double rate = 1.0 / (attackMs.load() * 0.001 * sampleRate);
71
- gain += rate;
72
- if (gain >= 1.0) {
73
- gain = 1.0;
74
- stage.store(Stage::Sustain);
75
- }
76
- break;
77
- }
78
-
79
- case Stage::Sustain:
80
- gain = 1.0;
81
- break;
82
-
83
- case Stage::Release: {
84
- double rate = 1.0 / (releaseMs.load() * 0.001 * sampleRate);
85
- gain -= rate;
86
- if (gain <= 0.0) {
87
- gain = 0.0;
88
- stage.store(Stage::Idle);
89
- }
90
- break;
91
- }
92
- }
62
+ filter.prepare(spec);
63
+ filter.setType(juce::dsp::StateVariableTPTFilter<float>::Type::lowpass);
93
64
 
94
- return (float)gain;
95
- }
65
+ adsr.setSampleRate(sampleRate);
66
+ juce::ADSR::Parameters adsrParams { 0.01f, 0.0f, 1.0f, 0.2f };
67
+ adsr.setParameters(adsrParams);
96
68
 
97
- void reset() {
98
- gain = 0.0;
99
- stage.store(Stage::Idle);
69
+ cachedSampleRate = sampleRate;
70
+ prepared = true;
100
71
  }
101
72
 
102
- private:
103
- std::atomic<double> attackMs;
104
- std::atomic<double> releaseMs;
105
- std::atomic<Stage> stage;
106
- double gain;
107
- };
73
+ // ── Main-thread setters (all atomic or lock-free) ─────────────
108
74
 
109
- // ── Synth Engine ──────────────────────────────────────────────────
110
- class SynthEngine {
111
- public:
112
- SynthEngine()
113
- : frequency(440.0), level(0.5), phase(0.0),
114
- waveform(Waveform::Sine), detuneCents(0.0)
115
- {
116
- for (int i = 0; i < MAX_HARMONICS; i++) {
117
- harmonics[i] = (i == 0) ? 1.0 : 0.0;
118
- }
119
- }
120
-
121
- void setFrequency(double freq) { frequency = freq; }
122
- void setLevel(double lev) { level = lev; }
75
+ void setFrequency(double freq) { frequency.store(freq); }
76
+ void setLevel(double lev) { level.store(lev); }
123
77
  void setWaveform(Waveform wf) { waveform.store(wf); }
124
78
  void setDetune(double cents) { detuneCents.store(cents); }
125
79
 
126
80
  void setHarmonics(const double *levels, int count) {
127
81
  for (int i = 0; i < MAX_HARMONICS; i++) {
128
- harmonics[i] = (i < count) ? levels[i] : 0.0;
82
+ harmonics[i].store((i < count) ? levels[i] : 0.0);
129
83
  }
130
84
  }
131
85
 
132
86
  void noteOn() {
133
- phase = 0.0;
134
- filter.reset();
135
- envelope.noteOn();
87
+ pendingNoteOn.store(true);
136
88
  }
137
89
 
138
90
  void noteOff() {
139
- envelope.noteOff();
91
+ pendingNoteOff.store(true);
92
+ }
93
+
94
+ void setAttack(double ms) {
95
+ attackMs.store(std::max(1.0, ms));
96
+ pendingParamUpdate.store(true);
140
97
  }
141
98
 
142
- void setAttack(double ms) { envelope.setAttack(ms); }
143
- void setRelease(double ms) { envelope.setRelease(ms); }
144
- void setFilterCutoff(double hz) { filter.setCutoff(hz); }
145
- void setFilterResonance(double q) { filter.setResonance(q); }
99
+ void setRelease(double ms) {
100
+ releaseMs.store(std::max(1.0, ms));
101
+ pendingParamUpdate.store(true);
102
+ }
103
+
104
+ void setFilterCutoff(double hz) {
105
+ filterCutoff.store(std::max(20.0, std::min(20000.0, hz)));
106
+ }
107
+
108
+ void setFilterResonance(double q) {
109
+ filterResonance.store(std::max(0.0, std::min(1.0, q)));
110
+ }
111
+
112
+ // ── Audio-thread render ───────────────────────────────────────
146
113
 
147
114
  float getNextSample(double sampleRate) {
148
- float envGain = envelope.process(sampleRate);
149
- if (envGain < 0.00001f && !envelope.isActive()) {
115
+ if (!prepared) return 0.0f;
116
+
117
+ // Apply pending note events (main thread -> audio thread)
118
+ if (pendingNoteOn.exchange(false)) {
119
+ sineOsc.reset();
120
+ squareOsc.reset();
121
+ sawOsc.reset();
122
+ triOsc.reset();
123
+ filter.reset();
124
+ adsr.noteOn();
125
+ }
126
+ if (pendingNoteOff.exchange(false)) {
127
+ adsr.noteOff();
128
+ }
129
+
130
+ // Apply pending ADSR parameter changes
131
+ if (pendingParamUpdate.exchange(false)) {
132
+ float atkSec = (float)(attackMs.load() * 0.001);
133
+ float relSec = (float)(releaseMs.load() * 0.001);
134
+ juce::ADSR::Parameters p { atkSec, 0.0f, 1.0f, relSec };
135
+ adsr.setParameters(p);
136
+ }
137
+
138
+ // Get envelope value
139
+ float envGain = adsr.getNextSample();
140
+ if (envGain < 0.00001f && !adsr.isActive()) {
150
141
  return 0.0f;
151
142
  }
152
143
 
144
+ // Read atomic parameters
153
145
  double freq = frequency.load();
154
- double lev = level.load();
146
+ float lev = (float)level.load();
155
147
  Waveform wf = waveform.load();
156
148
  double detune = detuneCents.load();
157
149
 
158
- // Apply detune (cents to frequency ratio)
150
+ // Apply detune
159
151
  if (std::fabs(detune) > 0.01) {
160
152
  freq *= std::pow(2.0, detune / 1200.0);
161
153
  }
162
154
 
163
- double sample = 0.0;
155
+ // Set frequency on all oscillators (they track phase independently)
156
+ float freqF = (float)freq;
157
+ sineOsc.setFrequency(freqF, true);
158
+ squareOsc.setFrequency(freqF, true);
159
+ sawOsc.setFrequency(freqF, true);
160
+ triOsc.setFrequency(freqF, true);
161
+
162
+ // Update filter parameters (safe — JUCE TPT filter handles per-sample modulation)
163
+ float cutoff = (float)filterCutoff.load();
164
+ float resonance = (float)filterResonance.load();
165
+ float mappedQ = 0.707f + resonance * 9.293f;
166
+ filter.setCutoffFrequency(cutoff);
167
+ filter.setResonance(mappedQ);
168
+
169
+ // Generate sample from the active waveform's pre-initialized oscillator
170
+ float sample = 0.0f;
164
171
 
165
172
  switch (wf) {
166
- case Waveform::Sine:
167
- for (int h = 0; h < MAX_HARMONICS; h++) {
168
- double hLevel = harmonics[h].load();
169
- if (hLevel > 0.0001) {
170
- sample += std::sin(phase * (h + 1) * 2.0 * M_PI) * hLevel;
173
+ case Waveform::Sine: {
174
+ float baseSample = sineOsc.processSample(0.0f);
175
+ float h0 = (float)harmonics[0].load();
176
+ sample = baseSample * h0;
177
+
178
+ // Additive harmonics (if any higher harmonics are set)
179
+ for (int h = 1; h < MAX_HARMONICS; h++) {
180
+ float hLevel = (float)harmonics[h].load();
181
+ if (hLevel > 0.0001f) {
182
+ // Approximate higher harmonics from base phase
183
+ // This is safe: no allocation, just math
184
+ float harmSample = std::sin(std::asin(baseSample) * (float)(h + 1));
185
+ sample += harmSample * hLevel;
171
186
  }
172
187
  }
173
188
  break;
189
+ }
174
190
 
175
- case Waveform::Square: {
176
- double p = std::fmod(phase, 1.0);
177
- sample = (p < 0.5) ? 0.7 : -0.7;
191
+ case Waveform::Square:
192
+ sample = squareOsc.processSample(0.0f) * 0.7f;
178
193
  break;
179
- }
180
194
 
181
- case Waveform::Saw: {
182
- double p = std::fmod(phase, 1.0);
183
- sample = (2.0 * p - 1.0) * 0.6;
195
+ case Waveform::Saw:
196
+ sample = sawOsc.processSample(0.0f) * 0.6f;
184
197
  break;
185
- }
186
198
 
187
- case Waveform::Triangle: {
188
- double p = std::fmod(phase, 1.0);
189
- sample = (4.0 * std::fabs(p - 0.5) - 1.0) * 0.8;
199
+ case Waveform::Triangle:
200
+ sample = triOsc.processSample(0.0f) * 0.8f;
190
201
  break;
191
- }
192
202
  }
193
203
 
194
- phase += freq / sampleRate;
195
- if (phase >= 1.0) phase -= 1.0;
204
+ // Advance inactive oscillators to keep phase tracking coherent
205
+ // (prevents phase discontinuity on waveform switch)
206
+ if (wf != Waveform::Sine) sineOsc.processSample(0.0f);
207
+ if (wf != Waveform::Square) squareOsc.processSample(0.0f);
208
+ if (wf != Waveform::Saw) sawOsc.processSample(0.0f);
209
+ if (wf != Waveform::Triangle) triOsc.processSample(0.0f);
196
210
 
197
211
  // Apply filter
198
- float filtered = filter.process((float)sample, sampleRate);
212
+ float filtered = filter.processSample(0, sample);
199
213
 
200
- return filtered * (float)lev * envGain;
214
+ return filtered * lev * envGain;
201
215
  }
202
216
 
203
217
  private:
218
+ // Atomic parameters (written by main thread, read by audio thread)
204
219
  std::atomic<double> frequency;
205
220
  std::atomic<double> level;
206
- double phase;
207
221
  std::atomic<Waveform> waveform;
208
222
  std::atomic<double> detuneCents;
209
223
  std::atomic<double> harmonics[MAX_HARMONICS];
210
- ADSREnvelope envelope;
211
- OnePoleLPF filter;
224
+ std::atomic<bool> pendingNoteOn;
225
+ std::atomic<bool> pendingNoteOff;
226
+ std::atomic<double> filterCutoff;
227
+ std::atomic<double> filterResonance;
228
+ std::atomic<double> attackMs;
229
+ std::atomic<double> releaseMs;
230
+ std::atomic<bool> pendingParamUpdate;
231
+
232
+ // DSP objects (ONLY touched by audio thread after prepare())
233
+ juce::dsp::Oscillator<float> sineOsc;
234
+ juce::dsp::Oscillator<float> squareOsc;
235
+ juce::dsp::Oscillator<float> sawOsc;
236
+ juce::dsp::Oscillator<float> triOsc;
237
+ juce::dsp::StateVariableTPTFilter<float> filter;
238
+ juce::ADSR adsr;
239
+ double cachedSampleRate = 44100.0;
240
+ bool prepared = false;
212
241
  };
213
242
 
214
243
  // ── Transport Engine ──────────────────────────────────────────────
244
+ // All-atomic transport — safe for cross-thread access.
215
245
  class TransportEngine {
216
246
  public:
217
247
  TransportEngine()
@@ -228,7 +258,7 @@ public:
228
258
  void stop() { playing.store(false); }
229
259
  bool isPlaying() { return playing.load(); }
230
260
 
231
- // Returns true if we crossed a new beat boundary
261
+ // Returns true if we crossed a new beat boundary (audio thread only)
232
262
  bool advance(double sampleRate) {
233
263
  if (!playing.load()) return false;
234
264
 
@@ -253,14 +283,15 @@ private:
253
283
  std::atomic<int> lastBeatInt;
254
284
  };
255
285
 
256
- // ── Objective-C Wrapper ───────────────────────────────────────────
257
-
286
+ // ── Beat State (lock-free audio->main thread communication) ──────
258
287
  struct BeatState {
259
288
  std::atomic<bool> crossed{false};
260
289
  std::atomic<double> beat{0.0};
261
290
  std::atomic<double> bpm{120.0};
262
291
  };
263
292
 
293
+ // ── Objective-C Wrapper ───────────────────────────────────────────
294
+
264
295
  @interface JuceToneGenerator ()
265
296
  @property (nonatomic, strong) AVAudioEngine *audioEngine;
266
297
  @property (nonatomic, strong) AVAudioSourceNode *sourceNode;
@@ -287,7 +318,6 @@ struct BeatState {
287
318
  if (self) {
288
319
  _engine = new SynthEngine();
289
320
  _transport = new TransportEngine();
290
- // Audio engine initialized lazily via [self initialize] called from setup
291
321
  }
292
322
  return self;
293
323
  }
@@ -345,6 +375,9 @@ struct BeatState {
345
375
  AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:sampleRate channels:2];
346
376
  NSLog(@"[ExpoJuce] Using sample rate: %.0f", sampleRate);
347
377
 
378
+ // Prepare JUCE DSP objects on main thread BEFORE audio thread starts
379
+ self.engine->prepare(sampleRate, 512);
380
+
348
381
  SynthEngine *eng = self.engine;
349
382
  TransportEngine *trans = self.transport;
350
383
 
@@ -353,19 +386,27 @@ struct BeatState {
353
386
  }
354
387
  BeatState *bs = self.beatState;
355
388
 
356
- self.sourceNode = [[AVAudioSourceNode alloc] initWithRenderBlock:^OSStatus(BOOL *isSilence, const AudioTimeStamp *timestamp, AVAudioFrameCount frameCount, AudioBufferList *outputData) {
357
- double sr = sampleRate;
389
+ self.sourceNode = [[AVAudioSourceNode alloc] initWithRenderBlock:^OSStatus(
390
+ BOOL *isSilence,
391
+ const AudioTimeStamp *timestamp,
392
+ AVAudioFrameCount frameCount,
393
+ AudioBufferList *outputData
394
+ ) {
358
395
  float *leftChannel = (float *)outputData->mBuffers[0].mData;
359
- float *rightChannel = (outputData->mNumberBuffers > 1) ? (float *)outputData->mBuffers[1].mData : leftChannel;
396
+ float *rightChannel = (outputData->mNumberBuffers > 1)
397
+ ? (float *)outputData->mBuffers[1].mData
398
+ : leftChannel;
360
399
 
361
400
  for (AVAudioFrameCount i = 0; i < frameCount; i++) {
362
- if (trans->advance(sr)) {
401
+ // Advance transport
402
+ if (trans->advance(sampleRate)) {
363
403
  bs->crossed.store(true);
364
404
  bs->beat.store(trans->getPosition());
365
405
  bs->bpm.store(trans->getTempo());
366
406
  }
367
407
 
368
- float sample = eng->getNextSample(sr);
408
+ // Render synth sample
409
+ float sample = eng->getNextSample(sampleRate);
369
410
  leftChannel[i] = sample;
370
411
  rightChannel[i] = sample;
371
412
  }
@@ -385,7 +426,7 @@ struct BeatState {
385
426
  continue;
386
427
  }
387
428
 
388
- NSLog(@"[ExpoJuce] Audio engine started successfully");
429
+ NSLog(@"[ExpoJuce] Audio engine started successfully (JUCE DSP active)");
389
430
  return;
390
431
 
391
432
  } @catch (NSException *exception) {
@@ -463,9 +504,7 @@ struct BeatState {
463
504
  // ── Synth Methods ─────────────────────────────────────────────────
464
505
 
465
506
  - (void)playToneWithFrequency:(double)frequency duration:(double)duration {
466
- NSLog(@"[ExpoJuce] playTone freq=%.1f dur=%.0f", frequency, duration);
467
507
  if (!self.engine) return;
468
-
469
508
  self.engine->setFrequency(frequency);
470
509
  self.engine->noteOn();
471
510
 
@@ -540,12 +579,10 @@ struct BeatState {
540
579
  // ── Transport ─────────────────────────────────────────────────────
541
580
 
542
581
  - (void)startTransport {
543
- NSLog(@"[ExpoJuce] Transport start");
544
582
  if (self.transport) self.transport->start();
545
583
  }
546
584
 
547
585
  - (void)stopTransport {
548
- NSLog(@"[ExpoJuce] Transport stop");
549
586
  if (self.transport) self.transport->stop();
550
587
  }
551
588
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-juce",
3
- "version": "0.2.24",
3
+ "version": "0.3.0",
4
4
  "description": "Realtime DSP w/C++ & JUCE",
5
5
  "type": "module",
6
6
  "main": "build/index.js",