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.
- package/ios/ExpoJuce.podspec +9 -8
- package/ios/JuceConfig.h +9 -3
- package/ios/JuceModules.mm +6 -3
- package/ios/JuceToneGenerator.mm +187 -150
- package/package.json +1 -1
package/ios/ExpoJuce.podspec
CHANGED
|
@@ -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
|
|
26
|
-
#
|
|
27
|
-
#
|
|
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
|
package/ios/JuceModules.mm
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
// JuceModules.mm — Compiles
|
|
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"
|
package/ios/JuceToneGenerator.mm
CHANGED
|
@@ -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
|
-
// ──
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
62
|
-
|
|
54
|
+
sawOsc.initialise([](float x) -> float { return x / juce::MathConstants<float>::pi; }, 2048);
|
|
55
|
+
sawOsc.prepare(spec);
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
stage.store(Stage::Idle);
|
|
69
|
+
cachedSampleRate = sampleRate;
|
|
70
|
+
prepared = true;
|
|
100
71
|
}
|
|
101
72
|
|
|
102
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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]
|
|
82
|
+
harmonics[i].store((i < count) ? levels[i] : 0.0);
|
|
129
83
|
}
|
|
130
84
|
}
|
|
131
85
|
|
|
132
86
|
void noteOn() {
|
|
133
|
-
|
|
134
|
-
filter.reset();
|
|
135
|
-
envelope.noteOn();
|
|
87
|
+
pendingNoteOn.store(true);
|
|
136
88
|
}
|
|
137
89
|
|
|
138
90
|
void noteOff() {
|
|
139
|
-
|
|
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
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
146
|
+
float lev = (float)level.load();
|
|
155
147
|
Waveform wf = waveform.load();
|
|
156
148
|
double detune = detuneCents.load();
|
|
157
149
|
|
|
158
|
-
// Apply detune
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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.
|
|
212
|
+
float filtered = filter.processSample(0, sample);
|
|
199
213
|
|
|
200
|
-
return filtered *
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
// ──
|
|
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(
|
|
357
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|