@geastack/native-webgl-angle 0.1.6
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/LICENSE +202 -0
- package/README.md +184 -0
- package/geatsc-plugin-batched-probe.mjs +98 -0
- package/geatsc-plugin-instancing.mjs +160 -0
- package/geatsc-plugin-uniforms.mjs +1955 -0
- package/geatsc-plugin-uploads.mjs +29 -0
- package/geatsc-plugin.mjs +10275 -0
- package/native/angle_webgl_host.mm +2687 -0
- package/native/audio_host.mm +857 -0
- package/native/audio_uwp.h +116 -0
- package/package.json +70 -0
- package/src/gea-native-types.d.ts +4 -0
- package/src/nativeAudioHost.ts +296 -0
- package/src/nativeThreeUtils.ts +11 -0
- package/src/nativeWebGL.ts +1211 -0
- package/src/nativeWebGLAnimation.ts +13 -0
- package/src/nativeWebGLHost.ts +333 -0
- package/src/nativeWebXRManager.ts +48 -0
- package/src/text/atlas-sans.ts +195 -0
- package/src/text/atlas-serif-italic.ts +195 -0
- package/src/text/atlas-serif.ts +195 -0
- package/src/text/native-text-types.ts +17 -0
- package/src/text/native-text.ts +203 -0
- package/src/troika-three-text.ts +248 -0
- package/test/auto-instanced-render-scene.ts +66 -0
- package/test/batched-render-scene.ts +58 -0
|
@@ -0,0 +1,857 @@
|
|
|
1
|
+
// Native audio host for the ANGLE/WebGL macOS target — a minimal Web-Audio-like
|
|
2
|
+
// graph engine behind the `gea_three_audio_*` externs (see geatsc-plugin.mjs +
|
|
3
|
+
// src/nativeAudioHost.ts). It implements exactly the surface the reference app's
|
|
4
|
+
// sound design uses:
|
|
5
|
+
//
|
|
6
|
+
// nodes — gain, biquad lowpass, buffer source (mp3 clips, loops, synth
|
|
7
|
+
// noise buffers), oscillator (sine/square/sawtooth/triangle),
|
|
8
|
+
// destination
|
|
9
|
+
// params — value get/set, setValueAtTime, linearRampToValueAtTime,
|
|
10
|
+
// exponentialRampToValueAtTime, cancelScheduledValues
|
|
11
|
+
// decode — mp3/wav via AVAudioFile out of the app bundle's Resources/Sounds
|
|
12
|
+
// (basename lookup — the native asset stub lowers `import url from
|
|
13
|
+
// './x.mp3'` to the basename), or $GEA_THREE_AUDIO_DIR for dev runs
|
|
14
|
+
//
|
|
15
|
+
// Output runs through a single AVAudioSourceNode render block; the graph is a
|
|
16
|
+
// tiny DAG (a handful of buses + short-lived one-shot voices) processed per
|
|
17
|
+
// render quantum in topological order from the live sources. Set
|
|
18
|
+
// GEA_THREE_AUDIO_LOG=1 for stderr diagnostics (engine start, decodes, voice
|
|
19
|
+
// starts, per-second output RMS).
|
|
20
|
+
|
|
21
|
+
#if defined(_WIN32)
|
|
22
|
+
#define _USE_MATH_DEFINES
|
|
23
|
+
#include "audio_uwp.h"
|
|
24
|
+
#else
|
|
25
|
+
#import <AVFoundation/AVFoundation.h>
|
|
26
|
+
#import <Foundation/Foundation.h>
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
#include "gea_runtime.h"
|
|
30
|
+
|
|
31
|
+
#include <algorithm>
|
|
32
|
+
#include <atomic>
|
|
33
|
+
#include <cmath>
|
|
34
|
+
#include <cstdint>
|
|
35
|
+
#include <cstdarg>
|
|
36
|
+
#include <cstdio>
|
|
37
|
+
#include <cstdlib>
|
|
38
|
+
#include <cstring>
|
|
39
|
+
#include <memory>
|
|
40
|
+
#include <mutex>
|
|
41
|
+
#include <string>
|
|
42
|
+
#include <unordered_map>
|
|
43
|
+
#include <vector>
|
|
44
|
+
|
|
45
|
+
#if defined(_WIN32)
|
|
46
|
+
static std::wstring geaAudioAssetDirectory;
|
|
47
|
+
extern "C" void gea_three_audio_set_directory_uwp(const wchar_t *path) {
|
|
48
|
+
geaAudioAssetDirectory = path ? path : L"";
|
|
49
|
+
}
|
|
50
|
+
#endif
|
|
51
|
+
|
|
52
|
+
namespace {
|
|
53
|
+
|
|
54
|
+
bool geaAudioLogEnabled() {
|
|
55
|
+
static const bool enabled = [] {
|
|
56
|
+
const char *value = std::getenv("GEA_THREE_AUDIO_LOG");
|
|
57
|
+
return value && value[0] && std::strcmp(value, "0") != 0;
|
|
58
|
+
}();
|
|
59
|
+
return enabled;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
bool geaAudioMutedFromEnvironment() {
|
|
63
|
+
const char *value = std::getenv("GEA_THREE_AUDIO_MUTED");
|
|
64
|
+
return value && value[0] && std::strcmp(value, "0") != 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
void geaAudioLog(const char *format, ...) {
|
|
68
|
+
if (!geaAudioLogEnabled()) return;
|
|
69
|
+
va_list args;
|
|
70
|
+
va_start(args, format);
|
|
71
|
+
std::fprintf(stderr, "[gea-audio] ");
|
|
72
|
+
std::vfprintf(stderr, format, args);
|
|
73
|
+
std::fprintf(stderr, "\n");
|
|
74
|
+
va_end(args);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Param automation — WebAudio-style scheduled events over an anchor value.
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
enum ParamMethod : int {
|
|
82
|
+
kParamSetValue = 0,
|
|
83
|
+
kParamSetValueAtTime = 1,
|
|
84
|
+
kParamLinearRamp = 2,
|
|
85
|
+
kParamExponentialRamp = 3,
|
|
86
|
+
kParamCancel = 4,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
struct ParamEvent {
|
|
90
|
+
int type = kParamSetValueAtTime;
|
|
91
|
+
double value = 0.0;
|
|
92
|
+
double time = 0.0;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
struct AudioParamState {
|
|
96
|
+
double anchorValue = 0.0;
|
|
97
|
+
double anchorTime = 0.0;
|
|
98
|
+
std::vector<ParamEvent> events;
|
|
99
|
+
|
|
100
|
+
explicit AudioParamState(double initial = 0.0) : anchorValue(initial) {}
|
|
101
|
+
|
|
102
|
+
void schedule(int method, double value, double time, double now) {
|
|
103
|
+
if (method == kParamSetValue) {
|
|
104
|
+
anchorValue = value;
|
|
105
|
+
anchorTime = now;
|
|
106
|
+
events.clear();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (method == kParamCancel) {
|
|
110
|
+
events.erase(
|
|
111
|
+
std::remove_if(events.begin(), events.end(), [&](const ParamEvent &e) { return e.time >= time; }),
|
|
112
|
+
events.end());
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// The game re-schedules `set(current, t) + ramp(target, t+dt)` every frame;
|
|
116
|
+
// dropping events at/after the new event's time keeps the list tiny and
|
|
117
|
+
// matches the intended "restart the ramp from here" semantics.
|
|
118
|
+
events.erase(
|
|
119
|
+
std::remove_if(events.begin(), events.end(), [&](const ParamEvent &e) { return e.time >= time - 1e-9; }),
|
|
120
|
+
events.end());
|
|
121
|
+
events.push_back({method, value, time});
|
|
122
|
+
std::sort(events.begin(), events.end(), [](const ParamEvent &a, const ParamEvent &b) { return a.time < b.time; });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Value at time t; consumes fully elapsed events into the anchor. Callers
|
|
126
|
+
// advance t monotonically within a render quantum.
|
|
127
|
+
double valueAt(double t) {
|
|
128
|
+
while (!events.empty()) {
|
|
129
|
+
const ParamEvent e = events.front();
|
|
130
|
+
if (t >= e.time) {
|
|
131
|
+
anchorValue = e.value;
|
|
132
|
+
anchorTime = e.time;
|
|
133
|
+
events.erase(events.begin());
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
if (e.type == kParamLinearRamp) {
|
|
137
|
+
const double span = e.time - anchorTime;
|
|
138
|
+
if (span <= 0.0) return e.value;
|
|
139
|
+
double f = (t - anchorTime) / span;
|
|
140
|
+
if (f < 0.0) f = 0.0;
|
|
141
|
+
return anchorValue + (e.value - anchorValue) * f;
|
|
142
|
+
}
|
|
143
|
+
if (e.type == kParamExponentialRamp) {
|
|
144
|
+
const double span = e.time - anchorTime;
|
|
145
|
+
if (span <= 0.0 || anchorValue == 0.0 || e.value == 0.0 || ((anchorValue < 0.0) != (e.value < 0.0))) {
|
|
146
|
+
return anchorValue; // exponential ramp is undefined across zero; hold
|
|
147
|
+
}
|
|
148
|
+
double f = (t - anchorTime) / span;
|
|
149
|
+
if (f < 0.0) f = 0.0;
|
|
150
|
+
return anchorValue * std::pow(e.value / anchorValue, f);
|
|
151
|
+
}
|
|
152
|
+
// A setValueAtTime still in the future: hold the anchor until it lands.
|
|
153
|
+
return anchorValue;
|
|
154
|
+
}
|
|
155
|
+
return anchorValue;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Buffers + nodes
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
struct AudioBufferData {
|
|
164
|
+
double sampleRate = 44100.0;
|
|
165
|
+
std::vector<std::vector<float>> channels;
|
|
166
|
+
std::string name; // diagnostics only
|
|
167
|
+
size_t frames() const { return channels.empty() ? 0 : channels[0].size(); }
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
enum NodeKind : int {
|
|
171
|
+
kNodeDestination = 0,
|
|
172
|
+
kNodeGain = 1,
|
|
173
|
+
kNodeBiquad = 2,
|
|
174
|
+
kNodeBufferSource = 3,
|
|
175
|
+
kNodeOscillator = 4,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
enum OscShape : int { kOscSine = 0, kOscSquare = 1, kOscSawtooth = 2, kOscTriangle = 3 };
|
|
179
|
+
|
|
180
|
+
struct AudioNodeState {
|
|
181
|
+
long id = 0;
|
|
182
|
+
int kind = kNodeGain;
|
|
183
|
+
|
|
184
|
+
AudioParamState gain{1.0};
|
|
185
|
+
AudioParamState frequency{350.0};
|
|
186
|
+
AudioParamState q{1.0};
|
|
187
|
+
AudioParamState playbackRate{1.0};
|
|
188
|
+
|
|
189
|
+
int shape = kOscSine; // oscillator waveform; biquad type is lowpass-only
|
|
190
|
+
|
|
191
|
+
std::vector<long> outputs;
|
|
192
|
+
|
|
193
|
+
// Buffer source state
|
|
194
|
+
std::shared_ptr<AudioBufferData> buffer;
|
|
195
|
+
bool loop = false;
|
|
196
|
+
bool started = false;
|
|
197
|
+
bool finished = false;
|
|
198
|
+
bool startLogged = false;
|
|
199
|
+
double startTime = 0.0;
|
|
200
|
+
double stopTime = 1e30;
|
|
201
|
+
double playhead = 0.0; // fractional frame index into the buffer
|
|
202
|
+
|
|
203
|
+
// Oscillator phase
|
|
204
|
+
double phase = 0.0;
|
|
205
|
+
|
|
206
|
+
// Biquad (RBJ lowpass), direct form 2 transposed, stereo state
|
|
207
|
+
float b0 = 1, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
|
208
|
+
float z1[2] = {0, 0};
|
|
209
|
+
float z2[2] = {0, 0};
|
|
210
|
+
double lastFreq = -1.0, lastQ = -1.0;
|
|
211
|
+
|
|
212
|
+
// Per-quantum scratch (stereo, plane-major: [0..n) left, [n..2n) right)
|
|
213
|
+
std::vector<float> input;
|
|
214
|
+
std::vector<float> output;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Engine
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
struct GeaAudioEngine {
|
|
222
|
+
std::mutex mutex;
|
|
223
|
+
std::unordered_map<long, std::shared_ptr<AudioNodeState>> nodes;
|
|
224
|
+
std::unordered_map<long, std::shared_ptr<AudioBufferData>> buffers;
|
|
225
|
+
long nextNodeId = 1;
|
|
226
|
+
long nextBufferId = 1;
|
|
227
|
+
double sampleRate = 44100.0;
|
|
228
|
+
std::atomic<long long> renderedFrames{0};
|
|
229
|
+
bool started = false;
|
|
230
|
+
bool startFailed = false;
|
|
231
|
+
#if defined(_WIN32)
|
|
232
|
+
GeaUwpAudioOutput uwpOutput;
|
|
233
|
+
#else
|
|
234
|
+
AVAudioEngine *avEngine = nil;
|
|
235
|
+
AVAudioSourceNode *sourceNode = nil;
|
|
236
|
+
#endif
|
|
237
|
+
|
|
238
|
+
// RMS diagnostics (log-gated)
|
|
239
|
+
double rmsAccum = 0.0;
|
|
240
|
+
long long rmsCount = 0;
|
|
241
|
+
long long rmsNextLogFrame = 0;
|
|
242
|
+
|
|
243
|
+
GeaAudioEngine() {
|
|
244
|
+
auto dest = std::make_shared<AudioNodeState>();
|
|
245
|
+
dest->id = 0;
|
|
246
|
+
dest->kind = kNodeDestination;
|
|
247
|
+
nodes[0] = dest;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
double currentTime() const {
|
|
251
|
+
return static_cast<double>(renderedFrames.load(std::memory_order_relaxed)) / sampleRate;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
bool ensureStarted() {
|
|
255
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
256
|
+
return ensureStartedLocked();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
bool ensureStartedLocked() {
|
|
260
|
+
if (started) return true;
|
|
261
|
+
if (startFailed) return false;
|
|
262
|
+
#if defined(_WIN32)
|
|
263
|
+
sampleRate = 48000.0;
|
|
264
|
+
started = uwpOutput.start(this, [](void *context, size_t frames, float *left, float *right) {
|
|
265
|
+
bool silence = false;
|
|
266
|
+
static_cast<GeaAudioEngine *>(context)->renderPlanar(&silence, frames, left, right);
|
|
267
|
+
});
|
|
268
|
+
startFailed = !started;
|
|
269
|
+
geaAudioLog("XAudio2 engine %s (rate=%.0f)", started ? "started" : "FAILED", sampleRate);
|
|
270
|
+
return started;
|
|
271
|
+
#else
|
|
272
|
+
@autoreleasepool {
|
|
273
|
+
avEngine = [[AVAudioEngine alloc] init];
|
|
274
|
+
const double hwRate = [[avEngine.outputNode inputFormatForBus:0] sampleRate];
|
|
275
|
+
sampleRate = hwRate > 0.0 ? hwRate : 44100.0;
|
|
276
|
+
AVAudioFormat *format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:sampleRate channels:2];
|
|
277
|
+
GeaAudioEngine *self_ = this;
|
|
278
|
+
sourceNode = [[AVAudioSourceNode alloc]
|
|
279
|
+
initWithFormat:format
|
|
280
|
+
renderBlock:^OSStatus(BOOL *isSilence, const AudioTimeStamp *, AVAudioFrameCount frameCount,
|
|
281
|
+
AudioBufferList *outputData) {
|
|
282
|
+
return self_->render(isSilence, frameCount, outputData);
|
|
283
|
+
}];
|
|
284
|
+
[avEngine attachNode:sourceNode];
|
|
285
|
+
[avEngine connect:sourceNode to:avEngine.mainMixerNode format:format];
|
|
286
|
+
NSError *error = nil;
|
|
287
|
+
if (![avEngine startAndReturnError:&error]) {
|
|
288
|
+
geaAudioLog("engine start FAILED: %s", error ? error.localizedDescription.UTF8String : "unknown");
|
|
289
|
+
startFailed = true;
|
|
290
|
+
avEngine = nil;
|
|
291
|
+
sourceNode = nil;
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
started = true;
|
|
295
|
+
geaAudioLog("engine started (rate=%.0f)", sampleRate);
|
|
296
|
+
geaAudioLog("resourcePath=%s",
|
|
297
|
+
[[NSBundle mainBundle] resourcePath] ? [[NSBundle mainBundle] resourcePath].UTF8String : "(nil)");
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
#endif
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
long createNode(int kind) {
|
|
304
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
305
|
+
auto node = std::make_shared<AudioNodeState>();
|
|
306
|
+
node->id = nextNodeId++;
|
|
307
|
+
node->kind = kind;
|
|
308
|
+
if (kind == kNodeOscillator) node->frequency = AudioParamState(440.0);
|
|
309
|
+
nodes[node->id] = node;
|
|
310
|
+
return node->id;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
std::shared_ptr<AudioNodeState> nodeLocked(long id) {
|
|
314
|
+
auto it = nodes.find(id);
|
|
315
|
+
return it == nodes.end() ? nullptr : it->second;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
void connect(long src, long dst) {
|
|
319
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
320
|
+
auto node = nodeLocked(src);
|
|
321
|
+
if (!node || nodes.find(dst) == nodes.end()) return;
|
|
322
|
+
if (std::find(node->outputs.begin(), node->outputs.end(), dst) == node->outputs.end()) {
|
|
323
|
+
node->outputs.push_back(dst);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
void paramEvent(long id, int param, int method, double value, double time) {
|
|
328
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
329
|
+
auto node = nodeLocked(id);
|
|
330
|
+
if (!node) return;
|
|
331
|
+
AudioParamState *state = paramFor(*node, param);
|
|
332
|
+
if (!state) return;
|
|
333
|
+
state->schedule(method, value, time, currentTime());
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
double paramGet(long id, int param) {
|
|
337
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
338
|
+
auto node = nodeLocked(id);
|
|
339
|
+
if (!node) return 0.0;
|
|
340
|
+
AudioParamState *state = paramFor(*node, param);
|
|
341
|
+
if (!state) return 0.0;
|
|
342
|
+
return state->valueAt(currentTime());
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
static AudioParamState *paramFor(AudioNodeState &node, int param) {
|
|
346
|
+
switch (param) {
|
|
347
|
+
case 0: return &node.gain;
|
|
348
|
+
case 1: return &node.frequency;
|
|
349
|
+
case 2: return &node.q;
|
|
350
|
+
case 3: return &node.playbackRate;
|
|
351
|
+
}
|
|
352
|
+
return nullptr;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
void setNodeType(long id, const std::string &type) {
|
|
356
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
357
|
+
auto node = nodeLocked(id);
|
|
358
|
+
if (!node) return;
|
|
359
|
+
if (type == "sine") node->shape = kOscSine;
|
|
360
|
+
else if (type == "square") node->shape = kOscSquare;
|
|
361
|
+
else if (type == "sawtooth") node->shape = kOscSawtooth;
|
|
362
|
+
else if (type == "triangle") node->shape = kOscTriangle;
|
|
363
|
+
// biquad: lowpass is the only implemented (and only used) filter type
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
long registerBuffer(std::shared_ptr<AudioBufferData> data) {
|
|
367
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
368
|
+
const long id = nextBufferId++;
|
|
369
|
+
buffers[id] = std::move(data);
|
|
370
|
+
return id;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
long createBuffer(int channels, long frames, double rate) {
|
|
374
|
+
auto data = std::make_shared<AudioBufferData>();
|
|
375
|
+
data->sampleRate = rate > 0.0 ? rate : 44100.0;
|
|
376
|
+
data->name = "synth";
|
|
377
|
+
if (channels < 1) channels = 1;
|
|
378
|
+
if (channels > 2) channels = 2;
|
|
379
|
+
if (frames < 0) frames = 0;
|
|
380
|
+
data->channels.assign(static_cast<size_t>(channels), std::vector<float>(static_cast<size_t>(frames), 0.0f));
|
|
381
|
+
return registerBuffer(std::move(data));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
void bufferChannelData(long bufferId, int channel, const gea::TypedArray<float> &samples) {
|
|
385
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
386
|
+
auto it = buffers.find(bufferId);
|
|
387
|
+
if (it == buffers.end()) return;
|
|
388
|
+
AudioBufferData &data = *it->second;
|
|
389
|
+
if (channel < 0 || static_cast<size_t>(channel) >= data.channels.size()) return;
|
|
390
|
+
std::vector<float> &plane = data.channels[static_cast<size_t>(channel)];
|
|
391
|
+
const size_t count = std::min(plane.size(), static_cast<size_t>(samples.size()));
|
|
392
|
+
for (size_t i = 0; i < count; i++) plane[i] = samples[i];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
void sourceSetBuffer(long nodeId, long bufferId) {
|
|
396
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
397
|
+
auto node = nodeLocked(nodeId);
|
|
398
|
+
if (!node) return;
|
|
399
|
+
auto it = buffers.find(bufferId);
|
|
400
|
+
if (it != buffers.end()) node->buffer = it->second;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
void sourceStart(long nodeId, double when, bool loop) {
|
|
404
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
405
|
+
if (!ensureStartedLocked()) return;
|
|
406
|
+
auto node = nodeLocked(nodeId);
|
|
407
|
+
if (!node || node->started) return;
|
|
408
|
+
const double now = currentTime();
|
|
409
|
+
node->loop = node->kind == kNodeBufferSource ? (loop || node->loop) : false;
|
|
410
|
+
node->startTime = when > now ? when : now;
|
|
411
|
+
node->started = true;
|
|
412
|
+
node->finished = false;
|
|
413
|
+
node->playhead = 0.0;
|
|
414
|
+
node->phase = 0.0;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
void sourceStop(long nodeId, double when) {
|
|
418
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
419
|
+
auto node = nodeLocked(nodeId);
|
|
420
|
+
if (!node) return;
|
|
421
|
+
const double now = currentTime();
|
|
422
|
+
node->stopTime = when > now ? when : now;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// -- Decode ---------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
long loadBufferFile(const std::string &name) {
|
|
428
|
+
if (name.empty() || name.find('/') != std::string::npos || name.find("..") != std::string::npos) return 0;
|
|
429
|
+
#if defined(_WIN32)
|
|
430
|
+
if (name.find('\\') != std::string::npos || geaAudioAssetDirectory.empty()) return 0;
|
|
431
|
+
const int length = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name.data(), static_cast<int>(name.size()), nullptr, 0);
|
|
432
|
+
if (length <= 0) return 0;
|
|
433
|
+
std::wstring base(static_cast<size_t>(length), L'\0');
|
|
434
|
+
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, name.data(), static_cast<int>(name.size()), base.data(), length);
|
|
435
|
+
auto data = std::make_shared<AudioBufferData>();
|
|
436
|
+
data->name = name;
|
|
437
|
+
if (!geaUwpDecodeAudio(geaAudioAssetDirectory + L"\\" + base, data->channels, data->sampleRate)) {
|
|
438
|
+
geaAudioLog("decode FAILED: %s", name.c_str());
|
|
439
|
+
return 0;
|
|
440
|
+
}
|
|
441
|
+
return registerBuffer(std::move(data));
|
|
442
|
+
#else
|
|
443
|
+
@autoreleasepool {
|
|
444
|
+
NSString *base = [NSString stringWithUTF8String:name.c_str()];
|
|
445
|
+
NSMutableArray<NSString *> *candidates = [NSMutableArray array];
|
|
446
|
+
if (const char *dir = std::getenv("GEA_THREE_AUDIO_DIR")) {
|
|
447
|
+
[candidates addObject:[[NSString stringWithUTF8String:dir] stringByAppendingPathComponent:base]];
|
|
448
|
+
}
|
|
449
|
+
NSString *resources = [[NSBundle mainBundle] resourcePath];
|
|
450
|
+
if (resources) {
|
|
451
|
+
[candidates addObject:[[resources stringByAppendingPathComponent:@"Sounds"] stringByAppendingPathComponent:base]];
|
|
452
|
+
[candidates addObject:[resources stringByAppendingPathComponent:base]];
|
|
453
|
+
}
|
|
454
|
+
NSString *path = nil;
|
|
455
|
+
for (NSString *candidate in candidates) {
|
|
456
|
+
if ([[NSFileManager defaultManager] fileExistsAtPath:candidate]) {
|
|
457
|
+
path = candidate;
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (!path) {
|
|
462
|
+
geaAudioLog("decode MISS: %s (no file in GEA_THREE_AUDIO_DIR or Resources/Sounds)", name.c_str());
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
NSError *error = nil;
|
|
466
|
+
AVAudioFile *file = [[AVAudioFile alloc] initForReading:[NSURL fileURLWithPath:path]
|
|
467
|
+
commonFormat:AVAudioPCMFormatFloat32
|
|
468
|
+
interleaved:NO
|
|
469
|
+
error:&error];
|
|
470
|
+
if (!file) {
|
|
471
|
+
geaAudioLog("decode FAILED to open %s: %s", name.c_str(),
|
|
472
|
+
error ? error.localizedDescription.UTF8String : "unknown");
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
const AVAudioFrameCount capacity = static_cast<AVAudioFrameCount>(file.length);
|
|
476
|
+
AVAudioPCMBuffer *pcm = [[AVAudioPCMBuffer alloc] initWithPCMFormat:file.processingFormat
|
|
477
|
+
frameCapacity:capacity > 0 ? capacity : 1];
|
|
478
|
+
if (!pcm || ![file readIntoBuffer:pcm error:&error]) {
|
|
479
|
+
geaAudioLog("decode FAILED to read %s: %s", name.c_str(),
|
|
480
|
+
error ? error.localizedDescription.UTF8String : "unknown");
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
const AVAudioChannelCount channelCount = pcm.format.channelCount;
|
|
484
|
+
const AVAudioFrameCount frameCount = pcm.frameLength;
|
|
485
|
+
auto data = std::make_shared<AudioBufferData>();
|
|
486
|
+
data->sampleRate = pcm.format.sampleRate;
|
|
487
|
+
data->name = name;
|
|
488
|
+
const AVAudioChannelCount keep = channelCount > 2 ? 2 : channelCount;
|
|
489
|
+
data->channels.resize(keep);
|
|
490
|
+
float *const *planes = pcm.floatChannelData;
|
|
491
|
+
for (AVAudioChannelCount ch = 0; ch < keep; ch++) {
|
|
492
|
+
data->channels[ch].assign(planes[ch], planes[ch] + frameCount);
|
|
493
|
+
}
|
|
494
|
+
const long id = registerBuffer(std::move(data));
|
|
495
|
+
geaAudioLog("decoded %s -> buffer %ld (%u frames, %u ch, %.0f Hz)", name.c_str(), id,
|
|
496
|
+
static_cast<unsigned>(frameCount), static_cast<unsigned>(keep), pcm.format.sampleRate);
|
|
497
|
+
return id;
|
|
498
|
+
}
|
|
499
|
+
#endif
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// -- Render ---------------------------------------------------------------
|
|
503
|
+
|
|
504
|
+
#if !defined(_WIN32)
|
|
505
|
+
OSStatus render(BOOL *isSilence, AVAudioFrameCount frameCount, AudioBufferList *outputData) {
|
|
506
|
+
float *outL = outputData->mNumberBuffers > 0 ? static_cast<float *>(outputData->mBuffers[0].mData) : nullptr;
|
|
507
|
+
float *outR = outputData->mNumberBuffers > 1 ? static_cast<float *>(outputData->mBuffers[1].mData) : outL;
|
|
508
|
+
bool silence = true;
|
|
509
|
+
renderPlanar(&silence, frameCount, outL, outR);
|
|
510
|
+
*isSilence = silence ? YES : NO;
|
|
511
|
+
return noErr;
|
|
512
|
+
}
|
|
513
|
+
#endif
|
|
514
|
+
|
|
515
|
+
void renderPlanar(bool *isSilence, size_t frames, float *outL, float *outR) {
|
|
516
|
+
static bool firstRender = true;
|
|
517
|
+
if (firstRender) { firstRender = false; geaAudioLog("first render pull frames=%u", static_cast<unsigned>(frames)); }
|
|
518
|
+
if (!outL) return;
|
|
519
|
+
std::memset(outL, 0, frames * sizeof(float));
|
|
520
|
+
if (outR != outL) std::memset(outR, 0, frames * sizeof(float));
|
|
521
|
+
|
|
522
|
+
// Lets renderer/profile runs exercise the real in-game audio graph without
|
|
523
|
+
// emitting sound. The app's own mute control remains unchanged.
|
|
524
|
+
if (geaAudioMutedFromEnvironment()) {
|
|
525
|
+
advanceClock(frames, outL, outR);
|
|
526
|
+
*isSilence = true;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
std::lock_guard<std::mutex> guard(mutex);
|
|
531
|
+
const double qStart = currentTime();
|
|
532
|
+
const double dt = 1.0 / sampleRate;
|
|
533
|
+
|
|
534
|
+
// Live sources drive everything; nodes not reachable from one are skipped
|
|
535
|
+
// (finished one-shot voice chains cost nothing).
|
|
536
|
+
std::vector<AudioNodeState *> order;
|
|
537
|
+
order.reserve(nodes.size());
|
|
538
|
+
std::unordered_map<long, int> indegree;
|
|
539
|
+
{
|
|
540
|
+
// Forward reachability from active sources.
|
|
541
|
+
std::vector<long> stack;
|
|
542
|
+
std::unordered_map<long, bool> reachable;
|
|
543
|
+
for (auto &entry : nodes) {
|
|
544
|
+
AudioNodeState &n = *entry.second;
|
|
545
|
+
const bool activeSource = (n.kind == kNodeBufferSource || n.kind == kNodeOscillator) && n.started && !n.finished;
|
|
546
|
+
if (activeSource) {
|
|
547
|
+
stack.push_back(n.id);
|
|
548
|
+
reachable[n.id] = true;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
while (!stack.empty()) {
|
|
552
|
+
const long id = stack.back();
|
|
553
|
+
stack.pop_back();
|
|
554
|
+
auto node = nodeLocked(id);
|
|
555
|
+
if (!node) continue;
|
|
556
|
+
for (long out : node->outputs) {
|
|
557
|
+
if (!reachable[out]) {
|
|
558
|
+
reachable[out] = true;
|
|
559
|
+
stack.push_back(out);
|
|
560
|
+
}
|
|
561
|
+
indegree[out]++;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (reachable.empty()) {
|
|
565
|
+
advanceClock(frames, outL, outR);
|
|
566
|
+
*isSilence = true;
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
// Kahn over the reachable subgraph.
|
|
570
|
+
std::vector<long> ready;
|
|
571
|
+
for (auto &entry : reachable) {
|
|
572
|
+
if (indegree.find(entry.first) == indegree.end()) ready.push_back(entry.first);
|
|
573
|
+
}
|
|
574
|
+
std::unordered_map<long, int> remaining = indegree;
|
|
575
|
+
while (!ready.empty()) {
|
|
576
|
+
const long id = ready.back();
|
|
577
|
+
ready.pop_back();
|
|
578
|
+
auto node = nodeLocked(id);
|
|
579
|
+
if (!node) continue;
|
|
580
|
+
order.push_back(node.get());
|
|
581
|
+
for (long out : node->outputs) {
|
|
582
|
+
if (--remaining[out] == 0) ready.push_back(out);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Zero per-node scratch.
|
|
588
|
+
for (AudioNodeState *node : order) {
|
|
589
|
+
node->input.assign(frames * 2, 0.0f);
|
|
590
|
+
node->output.assign(frames * 2, 0.0f);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
for (AudioNodeState *node : order) {
|
|
594
|
+
processNode(*node, frames, qStart, dt);
|
|
595
|
+
// Fan out into downstream inputs.
|
|
596
|
+
for (long out : node->outputs) {
|
|
597
|
+
auto dst = nodeLocked(out);
|
|
598
|
+
if (!dst || dst->input.size() != frames * 2) continue;
|
|
599
|
+
for (size_t i = 0; i < frames * 2; i++) dst->input[i] += node->output[i];
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
auto destination = nodeLocked(0);
|
|
604
|
+
if (destination && destination->input.size() == frames * 2) {
|
|
605
|
+
std::memcpy(outL, destination->input.data(), frames * sizeof(float));
|
|
606
|
+
std::memcpy(outR, destination->input.data() + frames, frames * sizeof(float));
|
|
607
|
+
*isSilence = false;
|
|
608
|
+
} else {
|
|
609
|
+
*isSilence = true;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Reap finished one-shot sources (their private per-voice gain chains stop
|
|
613
|
+
// being processed once unreachable; the structs themselves are reclaimed
|
|
614
|
+
// when the TS wrappers drop — node maps only grow by finished voices).
|
|
615
|
+
for (auto it = nodes.begin(); it != nodes.end();) {
|
|
616
|
+
AudioNodeState &n = *it->second;
|
|
617
|
+
if ((n.kind == kNodeBufferSource || n.kind == kNodeOscillator) && n.finished) {
|
|
618
|
+
it = nodes.erase(it);
|
|
619
|
+
} else {
|
|
620
|
+
++it;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
advanceClock(frames, outL, outR);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
void advanceClock(size_t frames, const float *outL, const float *outR) {
|
|
629
|
+
if (geaAudioLogEnabled() && outL) {
|
|
630
|
+
for (size_t i = 0; i < frames; i++) {
|
|
631
|
+
const double l = outL[i];
|
|
632
|
+
const double r = outR ? outR[i] : l;
|
|
633
|
+
rmsAccum += l * l + r * r;
|
|
634
|
+
}
|
|
635
|
+
rmsCount += static_cast<long long>(frames) * 2;
|
|
636
|
+
const long long total = renderedFrames.load(std::memory_order_relaxed) + static_cast<long long>(frames);
|
|
637
|
+
if (total >= rmsNextLogFrame) {
|
|
638
|
+
const double rms = rmsCount > 0 ? std::sqrt(rmsAccum / static_cast<double>(rmsCount)) : 0.0;
|
|
639
|
+
geaAudioLog("t=%.1fs output rms=%.5f", static_cast<double>(total) / sampleRate, rms);
|
|
640
|
+
rmsAccum = 0.0;
|
|
641
|
+
rmsCount = 0;
|
|
642
|
+
rmsNextLogFrame = total + static_cast<long long>(sampleRate);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
renderedFrames.fetch_add(static_cast<long long>(frames), std::memory_order_relaxed);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
void processNode(AudioNodeState &node, size_t frames, double qStart, double dt) {
|
|
649
|
+
switch (node.kind) {
|
|
650
|
+
case kNodeBufferSource: renderBufferSource(node, frames, qStart, dt); break;
|
|
651
|
+
case kNodeOscillator: renderOscillator(node, frames, qStart, dt); break;
|
|
652
|
+
case kNodeGain: {
|
|
653
|
+
for (size_t i = 0; i < frames; i++) {
|
|
654
|
+
const float g = static_cast<float>(node.gain.valueAt(qStart + static_cast<double>(i) * dt));
|
|
655
|
+
node.output[i] = node.input[i] * g;
|
|
656
|
+
node.output[frames + i] = node.input[frames + i] * g;
|
|
657
|
+
}
|
|
658
|
+
break;
|
|
659
|
+
}
|
|
660
|
+
case kNodeBiquad: renderBiquad(node, frames, qStart, dt); break;
|
|
661
|
+
case kNodeDestination:
|
|
662
|
+
default:
|
|
663
|
+
break; // destination consumes its input directly
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
void renderBufferSource(AudioNodeState &node, size_t frames, double qStart, double dt) {
|
|
668
|
+
if (!node.buffer || node.buffer->frames() == 0) {
|
|
669
|
+
node.finished = true;
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (!node.startLogged) {
|
|
673
|
+
node.startLogged = true;
|
|
674
|
+
geaAudioLog("voice start: source %ld buffer=%s loop=%d rate=%.2f", node.id, node.buffer->name.c_str(),
|
|
675
|
+
node.loop ? 1 : 0, node.playbackRate.valueAt(qStart));
|
|
676
|
+
}
|
|
677
|
+
const AudioBufferData &buf = *node.buffer;
|
|
678
|
+
const size_t length = buf.frames();
|
|
679
|
+
const size_t channels = buf.channels.size();
|
|
680
|
+
const double rateScale = buf.sampleRate / sampleRate;
|
|
681
|
+
for (size_t i = 0; i < frames; i++) {
|
|
682
|
+
const double t = qStart + static_cast<double>(i) * dt;
|
|
683
|
+
if (t < node.startTime) continue;
|
|
684
|
+
if (t >= node.stopTime) {
|
|
685
|
+
node.finished = true;
|
|
686
|
+
break;
|
|
687
|
+
}
|
|
688
|
+
if (node.playhead >= static_cast<double>(length)) {
|
|
689
|
+
if (node.loop) {
|
|
690
|
+
node.playhead = std::fmod(node.playhead, static_cast<double>(length));
|
|
691
|
+
} else {
|
|
692
|
+
node.finished = true;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const size_t i0 = static_cast<size_t>(node.playhead);
|
|
697
|
+
const size_t i1 = i0 + 1 < length ? i0 + 1 : (node.loop ? 0 : i0);
|
|
698
|
+
const float frac = static_cast<float>(node.playhead - static_cast<double>(i0));
|
|
699
|
+
const float l = buf.channels[0][i0] + (buf.channels[0][i1] - buf.channels[0][i0]) * frac;
|
|
700
|
+
const float r = channels > 1 ? (buf.channels[1][i0] + (buf.channels[1][i1] - buf.channels[1][i0]) * frac) : l;
|
|
701
|
+
node.output[i] = l;
|
|
702
|
+
node.output[frames + i] = r;
|
|
703
|
+
node.playhead += node.playbackRate.valueAt(t) * rateScale;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
void renderOscillator(AudioNodeState &node, size_t frames, double qStart, double dt) {
|
|
708
|
+
if (!node.startLogged) {
|
|
709
|
+
node.startLogged = true;
|
|
710
|
+
geaAudioLog("voice start: oscillator %ld shape=%d f=%.1f", node.id, node.shape, node.frequency.valueAt(qStart));
|
|
711
|
+
}
|
|
712
|
+
for (size_t i = 0; i < frames; i++) {
|
|
713
|
+
const double t = qStart + static_cast<double>(i) * dt;
|
|
714
|
+
if (t < node.startTime) continue;
|
|
715
|
+
if (t >= node.stopTime) {
|
|
716
|
+
node.finished = true;
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
const double f = node.frequency.valueAt(t);
|
|
720
|
+
node.phase += f / sampleRate;
|
|
721
|
+
node.phase -= std::floor(node.phase);
|
|
722
|
+
float v = 0.0f;
|
|
723
|
+
switch (node.shape) {
|
|
724
|
+
case kOscSine: v = static_cast<float>(std::sin(node.phase * 2.0 * M_PI)); break;
|
|
725
|
+
case kOscSquare: v = node.phase < 0.5 ? 1.0f : -1.0f; break;
|
|
726
|
+
case kOscSawtooth: v = static_cast<float>(2.0 * node.phase - 1.0); break;
|
|
727
|
+
case kOscTriangle:
|
|
728
|
+
v = static_cast<float>(node.phase < 0.5 ? 4.0 * node.phase - 1.0 : 3.0 - 4.0 * node.phase);
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
node.output[i] = v;
|
|
732
|
+
node.output[frames + i] = v;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
void renderBiquad(AudioNodeState &node, size_t frames, double qStart, double dt) {
|
|
737
|
+
// Coefficients re-derived per 64-sample block so filter sweeps track their
|
|
738
|
+
// ramps; state carries across blocks.
|
|
739
|
+
constexpr size_t kBlock = 64;
|
|
740
|
+
for (size_t block = 0; block < frames; block += kBlock) {
|
|
741
|
+
const size_t end = std::min(frames, block + kBlock);
|
|
742
|
+
const double t = qStart + static_cast<double>(block) * dt;
|
|
743
|
+
double freq = node.frequency.valueAt(t);
|
|
744
|
+
double qual = node.q.valueAt(t);
|
|
745
|
+
const double nyquist = sampleRate * 0.5;
|
|
746
|
+
if (freq < 10.0) freq = 10.0;
|
|
747
|
+
if (freq > nyquist * 0.99) freq = nyquist * 0.99;
|
|
748
|
+
if (qual < 0.0001) qual = 0.0001;
|
|
749
|
+
if (freq != node.lastFreq || qual != node.lastQ) {
|
|
750
|
+
node.lastFreq = freq;
|
|
751
|
+
node.lastQ = qual;
|
|
752
|
+
const double w0 = 2.0 * M_PI * freq / sampleRate;
|
|
753
|
+
const double alpha = std::sin(w0) / (2.0 * qual);
|
|
754
|
+
const double cw = std::cos(w0);
|
|
755
|
+
const double a0 = 1.0 + alpha;
|
|
756
|
+
node.b0 = static_cast<float>(((1.0 - cw) / 2.0) / a0);
|
|
757
|
+
node.b1 = static_cast<float>((1.0 - cw) / a0);
|
|
758
|
+
node.b2 = node.b0;
|
|
759
|
+
node.a1 = static_cast<float>((-2.0 * cw) / a0);
|
|
760
|
+
node.a2 = static_cast<float>((1.0 - alpha) / a0);
|
|
761
|
+
}
|
|
762
|
+
for (int ch = 0; ch < 2; ch++) {
|
|
763
|
+
const float *in = node.input.data() + static_cast<size_t>(ch) * frames;
|
|
764
|
+
float *out = node.output.data() + static_cast<size_t>(ch) * frames;
|
|
765
|
+
float z1 = node.z1[ch];
|
|
766
|
+
float z2 = node.z2[ch];
|
|
767
|
+
for (size_t i = block; i < end; i++) {
|
|
768
|
+
const float x = in[i];
|
|
769
|
+
const float y = node.b0 * x + z1;
|
|
770
|
+
z1 = node.b1 * x - node.a1 * y + z2;
|
|
771
|
+
z2 = node.b2 * x - node.a2 * y;
|
|
772
|
+
out[i] = y;
|
|
773
|
+
}
|
|
774
|
+
node.z1[ch] = z1;
|
|
775
|
+
node.z2[ch] = z2;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
GeaAudioEngine &geaAudio() {
|
|
782
|
+
static GeaAudioEngine *engine = new GeaAudioEngine();
|
|
783
|
+
return *engine;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
} // namespace
|
|
787
|
+
|
|
788
|
+
// ---------------------------------------------------------------------------
|
|
789
|
+
// Host externs (see native-webgl-angle/geatsc-plugin.mjs + src/nativeAudioHost.ts)
|
|
790
|
+
// ---------------------------------------------------------------------------
|
|
791
|
+
|
|
792
|
+
extern "C" bool gea_three_audio_supported() {
|
|
793
|
+
return geaAudio().ensureStarted();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
extern "C" double gea_three_audio_current_time() {
|
|
797
|
+
return geaAudio().currentTime();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
extern "C" double gea_three_audio_sample_rate() {
|
|
801
|
+
return geaAudio().sampleRate;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
extern "C" void gea_three_audio_resume() {
|
|
805
|
+
geaAudio().ensureStarted();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
extern "C" double gea_three_audio_create_node(double kind) {
|
|
809
|
+
geaAudioLog("create_node kind=%d", static_cast<int>(kind));
|
|
810
|
+
return static_cast<double>(geaAudio().createNode(static_cast<int>(kind)));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
extern "C" void gea_three_audio_set_node_type(double node, std::string type) {
|
|
814
|
+
geaAudio().setNodeType(static_cast<long>(node), type);
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
extern "C" void gea_three_audio_connect(double src, double dst) {
|
|
818
|
+
geaAudioLog("connect %d->%d", static_cast<int>(src), static_cast<int>(dst));
|
|
819
|
+
geaAudio().connect(static_cast<long>(src), static_cast<long>(dst));
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
extern "C" void gea_three_audio_param_event(double node, double param, double method, double value, double time) {
|
|
823
|
+
geaAudioLog("param_event node=%d param=%d method=%d val=%.3f",
|
|
824
|
+
static_cast<int>(node), static_cast<int>(param), static_cast<int>(method), value);
|
|
825
|
+
geaAudio().paramEvent(static_cast<long>(node), static_cast<int>(param), static_cast<int>(method), value, time);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
extern "C" double gea_three_audio_param_get(double node, double param) {
|
|
829
|
+
return geaAudio().paramGet(static_cast<long>(node), static_cast<int>(param));
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
extern "C" double gea_three_audio_load_buffer(std::string name) {
|
|
833
|
+
geaAudioLog("load_buffer '%s'", name.c_str());
|
|
834
|
+
return static_cast<double>(geaAudio().loadBufferFile(name));
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
extern "C" double gea_three_audio_create_buffer(double channels, double length, double sampleRate) {
|
|
838
|
+
return static_cast<double>(
|
|
839
|
+
geaAudio().createBuffer(static_cast<int>(channels), static_cast<long>(length), sampleRate));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
extern "C" void gea_three_audio_buffer_channel_data(double buffer, double channel, const gea::TypedArray<float> &data) {
|
|
843
|
+
geaAudio().bufferChannelData(static_cast<long>(buffer), static_cast<int>(channel), data);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
extern "C" void gea_three_audio_source_set_buffer(double node, double buffer) {
|
|
847
|
+
geaAudio().sourceSetBuffer(static_cast<long>(node), static_cast<long>(buffer));
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
extern "C" void gea_three_audio_source_start(double node, double when, double loop) {
|
|
851
|
+
geaAudioLog("source_start node=%d loop=%d", static_cast<int>(node), loop != 0.0);
|
|
852
|
+
geaAudio().sourceStart(static_cast<long>(node), when, loop != 0.0);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
extern "C" void gea_three_audio_source_stop(double node, double when) {
|
|
856
|
+
geaAudio().sourceStop(static_cast<long>(node), when);
|
|
857
|
+
}
|