expo-juce 0.2.25 → 0.3.1
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/ExpoJuceModule.swift +47 -47
- package/ios/JuceConfig.h +9 -3
- package/ios/JuceModules.mm +6 -3
- package/ios/JuceToneGenerator.mm +182 -158
- 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/ExpoJuceModule.swift
CHANGED
|
@@ -6,9 +6,6 @@ public class ExpoJuceModule: Module {
|
|
|
6
6
|
public func definition() -> ModuleDefinition {
|
|
7
7
|
Name("ExpoJuce")
|
|
8
8
|
|
|
9
|
-
// Audio engine is initialized lazily on first use, not at module load.
|
|
10
|
-
// Initializing AVAudioEngine in OnCreate can crash before JS is ready.
|
|
11
|
-
|
|
12
9
|
OnDestroy {
|
|
13
10
|
ExpoJuceBridge.stopBeatTimer()
|
|
14
11
|
ExpoJuceBridge.shutdown()
|
|
@@ -20,8 +17,6 @@ public class ExpoJuceModule: Module {
|
|
|
20
17
|
|
|
21
18
|
Events("onChange", "onBeat")
|
|
22
19
|
|
|
23
|
-
// Beat timer only runs while JS has active listeners.
|
|
24
|
-
// This prevents sendEvent from firing before listeners are attached (crash).
|
|
25
20
|
OnStartObserving {
|
|
26
21
|
self.hasListeners = true
|
|
27
22
|
ExpoJuceBridge.startBeatTimer()
|
|
@@ -37,8 +32,13 @@ public class ExpoJuceModule: Module {
|
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
// ── Synth ───────────────────────────────────────────────────────
|
|
35
|
+
// All audio methods use AsyncFunction dispatched on main queue.
|
|
36
|
+
// AVAudioEngine and CoreAudio must be accessed from the main thread.
|
|
37
|
+
// Expo's synchronous Function() runs on the JS/Hermes thread,
|
|
38
|
+
// which is NOT the main thread in production builds — this causes
|
|
39
|
+
// native crashes when calling CoreAudio APIs.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
AsyncFunction("setup") { [weak self] in
|
|
42
42
|
ExpoJuceBridge.setup()
|
|
43
43
|
ExpoJuceBridge.setBeatCallback { [weak self] beat, bpm in
|
|
44
44
|
guard let self = self, self.hasListeners else { return }
|
|
@@ -47,61 +47,61 @@ public class ExpoJuceModule: Module {
|
|
|
47
47
|
"bpm": bpm,
|
|
48
48
|
])
|
|
49
49
|
}
|
|
50
|
-
}
|
|
50
|
+
}.runOnQueue(.main)
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
AsyncFunction("playTone") { (frequency: Double, duration: Double) -> String in
|
|
53
53
|
ExpoJuceBridge.playTone(withFrequency: frequency, duration: duration)
|
|
54
54
|
return "Playing tone at \(frequency)Hz for \(duration)ms"
|
|
55
|
-
}
|
|
55
|
+
}.runOnQueue(.main)
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
AsyncFunction("setFrequency") { (frequency: Double) in
|
|
58
58
|
ExpoJuceBridge.setFrequency(frequency)
|
|
59
|
-
}
|
|
59
|
+
}.runOnQueue(.main)
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
AsyncFunction("setLevel") { (level: Double) in
|
|
62
62
|
ExpoJuceBridge.setLevel(level)
|
|
63
|
-
}
|
|
63
|
+
}.runOnQueue(.main)
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
AsyncFunction("setWaveform") { (waveform: String) in
|
|
66
66
|
ExpoJuceBridge.setWaveform(waveform)
|
|
67
|
-
}
|
|
67
|
+
}.runOnQueue(.main)
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
AsyncFunction("setHarmonicLevels") { (levels: [Double]) in
|
|
70
70
|
let nsLevels = levels.map { NSNumber(value: $0) }
|
|
71
71
|
ExpoJuceBridge.setHarmonicLevels(nsLevels)
|
|
72
|
-
}
|
|
72
|
+
}.runOnQueue(.main)
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
AsyncFunction("noteOn") {
|
|
75
75
|
ExpoJuceBridge.noteOn()
|
|
76
|
-
}
|
|
76
|
+
}.runOnQueue(.main)
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
AsyncFunction("noteOff") {
|
|
79
79
|
ExpoJuceBridge.noteOff()
|
|
80
|
-
}
|
|
80
|
+
}.runOnQueue(.main)
|
|
81
81
|
|
|
82
82
|
// ── DSP Params ──────────────────────────────────────────────────
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
AsyncFunction("setAttack") { (ms: Double) in
|
|
85
85
|
ExpoJuceBridge.setAttack(ms)
|
|
86
|
-
}
|
|
86
|
+
}.runOnQueue(.main)
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
AsyncFunction("setRelease") { (ms: Double) in
|
|
89
89
|
ExpoJuceBridge.setRelease(ms)
|
|
90
|
-
}
|
|
90
|
+
}.runOnQueue(.main)
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
AsyncFunction("setFilterCutoff") { (hz: Double) in
|
|
93
93
|
ExpoJuceBridge.setFilterCutoff(hz)
|
|
94
|
-
}
|
|
94
|
+
}.runOnQueue(.main)
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
AsyncFunction("setFilterResonance") { (q: Double) in
|
|
97
97
|
ExpoJuceBridge.setFilterResonance(q)
|
|
98
|
-
}
|
|
98
|
+
}.runOnQueue(.main)
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
AsyncFunction("setDetune") { (cents: Double) in
|
|
101
101
|
ExpoJuceBridge.setDetune(cents)
|
|
102
|
-
}
|
|
102
|
+
}.runOnQueue(.main)
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
AsyncFunction("setDSPValue") { (param: String, value: Double) in
|
|
105
105
|
switch param {
|
|
106
106
|
case "frequency":
|
|
107
107
|
ExpoJuceBridge.setFrequency(value)
|
|
@@ -120,37 +120,37 @@ public class ExpoJuceModule: Module {
|
|
|
120
120
|
default:
|
|
121
121
|
break
|
|
122
122
|
}
|
|
123
|
-
}
|
|
123
|
+
}.runOnQueue(.main)
|
|
124
124
|
|
|
125
125
|
// ── Transport ───────────────────────────────────────────────────
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
AsyncFunction("startTransport") {
|
|
128
128
|
ExpoJuceBridge.startTransport()
|
|
129
|
-
}
|
|
129
|
+
}.runOnQueue(.main)
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
AsyncFunction("stopTransport") {
|
|
132
132
|
ExpoJuceBridge.stopTransport()
|
|
133
|
-
}
|
|
133
|
+
}.runOnQueue(.main)
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
AsyncFunction("setTempo") { (bpm: Double) in
|
|
136
136
|
ExpoJuceBridge.setTempo(bpm)
|
|
137
|
-
}
|
|
137
|
+
}.runOnQueue(.main)
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
AsyncFunction("setTransportPosition") { (beat: Double) in
|
|
140
140
|
ExpoJuceBridge.setTransportPosition(beat)
|
|
141
|
-
}
|
|
141
|
+
}.runOnQueue(.main)
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
AsyncFunction("getTransportPosition") { () -> Double in
|
|
144
144
|
return ExpoJuceBridge.getTransportPosition()
|
|
145
|
-
}
|
|
145
|
+
}.runOnQueue(.main)
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
AsyncFunction("getTempo") { () -> Double in
|
|
148
148
|
return ExpoJuceBridge.getTempo()
|
|
149
|
-
}
|
|
149
|
+
}.runOnQueue(.main)
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
AsyncFunction("isTransportPlaying") { () -> Bool in
|
|
152
152
|
return ExpoJuceBridge.isTransportPlaying()
|
|
153
|
-
}
|
|
153
|
+
}.runOnQueue(.main)
|
|
154
154
|
|
|
155
155
|
// ── Async / View ────────────────────────────────────────────────
|
|
156
156
|
|
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,227 +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
|
-
|
|
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;
|
|
24
37
|
}
|
|
25
|
-
|
|
26
|
-
double fc = cutoff.load();
|
|
27
|
-
|
|
28
|
-
// Clamp cutoff to Nyquist
|
|
29
|
-
if (fc > sampleRate * 0.49) fc = sampleRate * 0.49;
|
|
30
|
-
if (fc < 20.0) fc = 20.0;
|
|
31
|
-
|
|
32
|
-
// Compute coefficient (simple RC low-pass)
|
|
33
|
-
double w = 2.0 * M_PI * fc / sampleRate;
|
|
34
|
-
double g = w / (1.0 + w);
|
|
35
|
-
|
|
36
|
-
// One-pole filter
|
|
37
|
-
y1 = y1 + g * (input - y1);
|
|
38
|
-
|
|
39
|
-
return (float)y1;
|
|
40
38
|
}
|
|
41
39
|
|
|
42
|
-
//
|
|
43
|
-
void
|
|
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;
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
std::atomic<double> resonance;
|
|
49
|
-
std::atomic<bool> pendingReset;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// ── ADSR Envelope ─────────────────────────────────────────────────
|
|
53
|
-
class ADSREnvelope {
|
|
54
|
-
public:
|
|
55
|
-
enum class Stage { Idle, Attack, Sustain, Release };
|
|
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
|
-
stage(Stage::Idle), gain(0.0) {}
|
|
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
|
-
|
|
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);
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
filter.prepare(spec);
|
|
63
|
+
filter.setType(juce::dsp::StateVariableTPTFilter<float>::Type::lowpass);
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
break;
|
|
65
|
+
adsr.setSampleRate(sampleRate);
|
|
66
|
+
juce::ADSR::Parameters adsrParams { 0.01f, 0.0f, 1.0f, 0.2f };
|
|
67
|
+
adsr.setParameters(adsrParams);
|
|
75
68
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
gain += rate;
|
|
79
|
-
if (gain >= 1.0) {
|
|
80
|
-
gain = 1.0;
|
|
81
|
-
stage.store(Stage::Sustain);
|
|
82
|
-
}
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case Stage::Sustain:
|
|
87
|
-
gain = 1.0;
|
|
88
|
-
break;
|
|
89
|
-
|
|
90
|
-
case Stage::Release: {
|
|
91
|
-
double rate = 1.0 / (releaseMs.load() * 0.001 * sampleRate);
|
|
92
|
-
gain -= rate;
|
|
93
|
-
if (gain <= 0.0) {
|
|
94
|
-
gain = 0.0;
|
|
95
|
-
stage.store(Stage::Idle);
|
|
96
|
-
}
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return (float)gain;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
void reset() {
|
|
105
|
-
gain = 0.0;
|
|
106
|
-
stage.store(Stage::Idle);
|
|
69
|
+
cachedSampleRate = sampleRate;
|
|
70
|
+
prepared = true;
|
|
107
71
|
}
|
|
108
72
|
|
|
109
|
-
|
|
110
|
-
std::atomic<double> attackMs;
|
|
111
|
-
std::atomic<double> releaseMs;
|
|
112
|
-
std::atomic<Stage> stage;
|
|
113
|
-
double gain;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
// ── Synth Engine ──────────────────────────────────────────────────
|
|
117
|
-
class SynthEngine {
|
|
118
|
-
public:
|
|
119
|
-
SynthEngine()
|
|
120
|
-
: frequency(440.0), level(0.5), phase(0.0),
|
|
121
|
-
waveform(Waveform::Sine), detuneCents(0.0)
|
|
122
|
-
{
|
|
123
|
-
for (int i = 0; i < MAX_HARMONICS; i++) {
|
|
124
|
-
harmonics[i] = (i == 0) ? 1.0 : 0.0;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
73
|
+
// ── Main-thread setters (all atomic or lock-free) ─────────────
|
|
127
74
|
|
|
128
|
-
void setFrequency(double freq) { frequency
|
|
129
|
-
void setLevel(double lev) { level
|
|
75
|
+
void setFrequency(double freq) { frequency.store(freq); }
|
|
76
|
+
void setLevel(double lev) { level.store(lev); }
|
|
130
77
|
void setWaveform(Waveform wf) { waveform.store(wf); }
|
|
131
78
|
void setDetune(double cents) { detuneCents.store(cents); }
|
|
132
79
|
|
|
133
80
|
void setHarmonics(const double *levels, int count) {
|
|
134
81
|
for (int i = 0; i < MAX_HARMONICS; i++) {
|
|
135
|
-
harmonics[i]
|
|
82
|
+
harmonics[i].store((i < count) ? levels[i] : 0.0);
|
|
136
83
|
}
|
|
137
84
|
}
|
|
138
85
|
|
|
139
86
|
void noteOn() {
|
|
140
87
|
pendingNoteOn.store(true);
|
|
141
|
-
filter.reset();
|
|
142
|
-
envelope.noteOn();
|
|
143
88
|
}
|
|
144
89
|
|
|
145
90
|
void noteOff() {
|
|
146
|
-
|
|
91
|
+
pendingNoteOff.store(true);
|
|
147
92
|
}
|
|
148
93
|
|
|
149
|
-
void setAttack(double ms) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
94
|
+
void setAttack(double ms) {
|
|
95
|
+
attackMs.store(std::max(1.0, ms));
|
|
96
|
+
pendingParamUpdate.store(true);
|
|
97
|
+
}
|
|
98
|
+
|
|
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 ───────────────────────────────────────
|
|
153
113
|
|
|
154
114
|
float getNextSample(double sampleRate) {
|
|
155
|
-
|
|
115
|
+
if (!prepared) return 0.0f;
|
|
116
|
+
|
|
117
|
+
// Apply pending note events (main thread -> audio thread)
|
|
156
118
|
if (pendingNoteOn.exchange(false)) {
|
|
157
|
-
|
|
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);
|
|
158
136
|
}
|
|
159
137
|
|
|
160
|
-
|
|
161
|
-
|
|
138
|
+
// Get envelope value
|
|
139
|
+
float envGain = adsr.getNextSample();
|
|
140
|
+
if (envGain < 0.00001f && !adsr.isActive()) {
|
|
162
141
|
return 0.0f;
|
|
163
142
|
}
|
|
164
143
|
|
|
144
|
+
// Read atomic parameters
|
|
165
145
|
double freq = frequency.load();
|
|
166
|
-
|
|
146
|
+
float lev = (float)level.load();
|
|
167
147
|
Waveform wf = waveform.load();
|
|
168
148
|
double detune = detuneCents.load();
|
|
169
149
|
|
|
170
|
-
// Apply detune
|
|
150
|
+
// Apply detune
|
|
171
151
|
if (std::fabs(detune) > 0.01) {
|
|
172
152
|
freq *= std::pow(2.0, detune / 1200.0);
|
|
173
153
|
}
|
|
174
154
|
|
|
175
|
-
|
|
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;
|
|
176
171
|
|
|
177
172
|
switch (wf) {
|
|
178
|
-
case Waveform::Sine:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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;
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
188
|
break;
|
|
189
|
+
}
|
|
186
190
|
|
|
187
|
-
case Waveform::Square:
|
|
188
|
-
|
|
189
|
-
sample = (p < 0.5) ? 0.7 : -0.7;
|
|
191
|
+
case Waveform::Square:
|
|
192
|
+
sample = squareOsc.processSample(0.0f) * 0.7f;
|
|
190
193
|
break;
|
|
191
|
-
}
|
|
192
194
|
|
|
193
|
-
case Waveform::Saw:
|
|
194
|
-
|
|
195
|
-
sample = (2.0 * p - 1.0) * 0.6;
|
|
195
|
+
case Waveform::Saw:
|
|
196
|
+
sample = sawOsc.processSample(0.0f) * 0.6f;
|
|
196
197
|
break;
|
|
197
|
-
}
|
|
198
198
|
|
|
199
|
-
case Waveform::Triangle:
|
|
200
|
-
|
|
201
|
-
sample = (4.0 * std::fabs(p - 0.5) - 1.0) * 0.8;
|
|
199
|
+
case Waveform::Triangle:
|
|
200
|
+
sample = triOsc.processSample(0.0f) * 0.8f;
|
|
202
201
|
break;
|
|
203
|
-
}
|
|
204
202
|
}
|
|
205
203
|
|
|
206
|
-
|
|
207
|
-
|
|
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);
|
|
208
210
|
|
|
209
211
|
// Apply filter
|
|
210
|
-
float filtered = filter.
|
|
212
|
+
float filtered = filter.processSample(0, sample);
|
|
211
213
|
|
|
212
|
-
return filtered *
|
|
214
|
+
return filtered * lev * envGain;
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
private:
|
|
218
|
+
// Atomic parameters (written by main thread, read by audio thread)
|
|
216
219
|
std::atomic<double> frequency;
|
|
217
220
|
std::atomic<double> level;
|
|
218
|
-
double phase; // Only touched by audio thread
|
|
219
|
-
std::atomic<bool> pendingNoteOn{false};
|
|
220
221
|
std::atomic<Waveform> waveform;
|
|
221
222
|
std::atomic<double> detuneCents;
|
|
222
223
|
std::atomic<double> harmonics[MAX_HARMONICS];
|
|
223
|
-
|
|
224
|
-
|
|
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;
|
|
225
241
|
};
|
|
226
242
|
|
|
227
243
|
// ── Transport Engine ──────────────────────────────────────────────
|
|
244
|
+
// All-atomic transport — safe for cross-thread access.
|
|
228
245
|
class TransportEngine {
|
|
229
246
|
public:
|
|
230
247
|
TransportEngine()
|
|
@@ -241,7 +258,7 @@ public:
|
|
|
241
258
|
void stop() { playing.store(false); }
|
|
242
259
|
bool isPlaying() { return playing.load(); }
|
|
243
260
|
|
|
244
|
-
// Returns true if we crossed a new beat boundary
|
|
261
|
+
// Returns true if we crossed a new beat boundary (audio thread only)
|
|
245
262
|
bool advance(double sampleRate) {
|
|
246
263
|
if (!playing.load()) return false;
|
|
247
264
|
|
|
@@ -266,14 +283,15 @@ private:
|
|
|
266
283
|
std::atomic<int> lastBeatInt;
|
|
267
284
|
};
|
|
268
285
|
|
|
269
|
-
// ──
|
|
270
|
-
|
|
286
|
+
// ── Beat State (lock-free audio->main thread communication) ──────
|
|
271
287
|
struct BeatState {
|
|
272
288
|
std::atomic<bool> crossed{false};
|
|
273
289
|
std::atomic<double> beat{0.0};
|
|
274
290
|
std::atomic<double> bpm{120.0};
|
|
275
291
|
};
|
|
276
292
|
|
|
293
|
+
// ── Objective-C Wrapper ───────────────────────────────────────────
|
|
294
|
+
|
|
277
295
|
@interface JuceToneGenerator ()
|
|
278
296
|
@property (nonatomic, strong) AVAudioEngine *audioEngine;
|
|
279
297
|
@property (nonatomic, strong) AVAudioSourceNode *sourceNode;
|
|
@@ -300,7 +318,6 @@ struct BeatState {
|
|
|
300
318
|
if (self) {
|
|
301
319
|
_engine = new SynthEngine();
|
|
302
320
|
_transport = new TransportEngine();
|
|
303
|
-
// Audio engine initialized lazily via [self initialize] called from setup
|
|
304
321
|
}
|
|
305
322
|
return self;
|
|
306
323
|
}
|
|
@@ -358,6 +375,9 @@ struct BeatState {
|
|
|
358
375
|
AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:sampleRate channels:2];
|
|
359
376
|
NSLog(@"[ExpoJuce] Using sample rate: %.0f", sampleRate);
|
|
360
377
|
|
|
378
|
+
// Prepare JUCE DSP objects on main thread BEFORE audio thread starts
|
|
379
|
+
self.engine->prepare(sampleRate, 512);
|
|
380
|
+
|
|
361
381
|
SynthEngine *eng = self.engine;
|
|
362
382
|
TransportEngine *trans = self.transport;
|
|
363
383
|
|
|
@@ -366,19 +386,27 @@ struct BeatState {
|
|
|
366
386
|
}
|
|
367
387
|
BeatState *bs = self.beatState;
|
|
368
388
|
|
|
369
|
-
self.sourceNode = [[AVAudioSourceNode alloc] initWithRenderBlock:^OSStatus(
|
|
370
|
-
|
|
389
|
+
self.sourceNode = [[AVAudioSourceNode alloc] initWithRenderBlock:^OSStatus(
|
|
390
|
+
BOOL *isSilence,
|
|
391
|
+
const AudioTimeStamp *timestamp,
|
|
392
|
+
AVAudioFrameCount frameCount,
|
|
393
|
+
AudioBufferList *outputData
|
|
394
|
+
) {
|
|
371
395
|
float *leftChannel = (float *)outputData->mBuffers[0].mData;
|
|
372
|
-
float *rightChannel = (outputData->mNumberBuffers > 1)
|
|
396
|
+
float *rightChannel = (outputData->mNumberBuffers > 1)
|
|
397
|
+
? (float *)outputData->mBuffers[1].mData
|
|
398
|
+
: leftChannel;
|
|
373
399
|
|
|
374
400
|
for (AVAudioFrameCount i = 0; i < frameCount; i++) {
|
|
375
|
-
|
|
401
|
+
// Advance transport
|
|
402
|
+
if (trans->advance(sampleRate)) {
|
|
376
403
|
bs->crossed.store(true);
|
|
377
404
|
bs->beat.store(trans->getPosition());
|
|
378
405
|
bs->bpm.store(trans->getTempo());
|
|
379
406
|
}
|
|
380
407
|
|
|
381
|
-
|
|
408
|
+
// Render synth sample
|
|
409
|
+
float sample = eng->getNextSample(sampleRate);
|
|
382
410
|
leftChannel[i] = sample;
|
|
383
411
|
rightChannel[i] = sample;
|
|
384
412
|
}
|
|
@@ -398,7 +426,7 @@ struct BeatState {
|
|
|
398
426
|
continue;
|
|
399
427
|
}
|
|
400
428
|
|
|
401
|
-
NSLog(@"[ExpoJuce] Audio engine started successfully");
|
|
429
|
+
NSLog(@"[ExpoJuce] Audio engine started successfully (JUCE DSP active)");
|
|
402
430
|
return;
|
|
403
431
|
|
|
404
432
|
} @catch (NSException *exception) {
|
|
@@ -476,9 +504,7 @@ struct BeatState {
|
|
|
476
504
|
// ── Synth Methods ─────────────────────────────────────────────────
|
|
477
505
|
|
|
478
506
|
- (void)playToneWithFrequency:(double)frequency duration:(double)duration {
|
|
479
|
-
NSLog(@"[ExpoJuce] playTone freq=%.1f dur=%.0f", frequency, duration);
|
|
480
507
|
if (!self.engine) return;
|
|
481
|
-
|
|
482
508
|
self.engine->setFrequency(frequency);
|
|
483
509
|
self.engine->noteOn();
|
|
484
510
|
|
|
@@ -553,12 +579,10 @@ struct BeatState {
|
|
|
553
579
|
// ── Transport ─────────────────────────────────────────────────────
|
|
554
580
|
|
|
555
581
|
- (void)startTransport {
|
|
556
|
-
NSLog(@"[ExpoJuce] Transport start");
|
|
557
582
|
if (self.transport) self.transport->start();
|
|
558
583
|
}
|
|
559
584
|
|
|
560
585
|
- (void)stopTransport {
|
|
561
|
-
NSLog(@"[ExpoJuce] Transport stop");
|
|
562
586
|
if (self.transport) self.transport->stop();
|
|
563
587
|
}
|
|
564
588
|
|