xbintsc 0.5.32 → 0.5.36

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.
Files changed (173) hide show
  1. package/README.md +10 -7
  2. package/README.zh-CN.md +10 -8
  3. package/dist/scripts/check-version.d.ts +27 -0
  4. package/dist/scripts/check-version.js +71 -0
  5. package/dist/scripts/check-version.js.map +1 -0
  6. package/dist/scripts/package-platform.d.ts +58 -0
  7. package/dist/scripts/package-platform.js +142 -0
  8. package/dist/scripts/package-platform.js.map +1 -0
  9. package/dist/scripts/verify-platform-package.d.ts +34 -0
  10. package/dist/scripts/verify-platform-package.js +161 -0
  11. package/dist/scripts/verify-platform-package.js.map +1 -0
  12. package/dist/src/cli/help.d.ts +1 -1
  13. package/dist/src/cli/main.js +3 -1
  14. package/dist/src/cli/main.js.map +1 -1
  15. package/dist/src/driver/runtime-lib.d.ts +29 -4
  16. package/dist/src/driver/runtime-lib.js +68 -10
  17. package/dist/src/driver/runtime-lib.js.map +1 -1
  18. package/dist/src/driver/version.d.ts +1 -1
  19. package/dist/src/driver/version.js +1 -1
  20. package/dist/src/extensions/gui/index.js +6 -5
  21. package/dist/src/extensions/gui/index.js.map +1 -1
  22. package/dist/src/extensions/media/index.js +4 -3
  23. package/dist/src/extensions/media/index.js.map +1 -1
  24. package/dist/src/parser/context.d.ts +41 -0
  25. package/dist/src/parser/context.js +89 -4
  26. package/dist/src/parser/context.js.map +1 -1
  27. package/dist/src/parser/literals.js +6 -12
  28. package/dist/src/parser/literals.js.map +1 -1
  29. package/dist/tests/driver/platform-package.test.d.ts +1 -0
  30. package/dist/tests/driver/platform-package.test.js +49 -0
  31. package/dist/tests/driver/platform-package.test.js.map +1 -0
  32. package/dist/tests/driver/runtime-lib.test.js +51 -4
  33. package/dist/tests/driver/runtime-lib.test.js.map +1 -1
  34. package/dist/tests/e2e/gui-audio.test.d.ts +14 -0
  35. package/dist/tests/e2e/gui-audio.test.js +287 -0
  36. package/dist/tests/e2e/gui-audio.test.js.map +1 -0
  37. package/dist/tests/e2e/gui-helpers.d.ts +7 -0
  38. package/dist/tests/e2e/gui-helpers.js +1 -0
  39. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  40. package/dist/tests/e2e/gui-media.test.js +78 -0
  41. package/dist/tests/e2e/gui-media.test.js.map +1 -1
  42. package/dist/tests/e2e/gui-style.test.js +55 -0
  43. package/dist/tests/e2e/gui-style.test.js.map +1 -1
  44. package/dist/tests/e2e/gui-webgl.test.js +258 -0
  45. package/dist/tests/e2e/gui-webgl.test.js.map +1 -1
  46. package/dist/tests/e2e/gui-webgl2-3d.test.d.ts +4 -0
  47. package/dist/tests/e2e/gui-webgl2-3d.test.js +264 -0
  48. package/dist/tests/e2e/gui-webgl2-3d.test.js.map +1 -0
  49. package/dist/tests/e2e/gui-webgl2-blit.test.d.ts +10 -0
  50. package/dist/tests/e2e/gui-webgl2-blit.test.js +262 -0
  51. package/dist/tests/e2e/gui-webgl2-blit.test.js.map +1 -0
  52. package/dist/tests/e2e/gui-webgl2-formats.test.d.ts +6 -0
  53. package/dist/tests/e2e/gui-webgl2-formats.test.js +395 -0
  54. package/dist/tests/e2e/gui-webgl2-formats.test.js.map +1 -0
  55. package/dist/tests/e2e/gui-webgl2-mrt.test.d.ts +4 -0
  56. package/dist/tests/e2e/gui-webgl2-mrt.test.js +105 -0
  57. package/dist/tests/e2e/gui-webgl2-mrt.test.js.map +1 -0
  58. package/dist/tests/e2e/gui-webgl2-ubo.test.d.ts +8 -0
  59. package/dist/tests/e2e/gui-webgl2-ubo.test.js +112 -0
  60. package/dist/tests/e2e/gui-webgl2-ubo.test.js.map +1 -0
  61. package/dist/tests/e2e/gui-webgl2.test.js +45 -1
  62. package/dist/tests/e2e/gui-webgl2.test.js.map +1 -1
  63. package/dist/tests/extensions/gui-index.test.js +1 -0
  64. package/dist/tests/extensions/gui-index.test.js.map +1 -1
  65. package/dist/tests/extensions/media.test.js +1 -0
  66. package/dist/tests/extensions/media.test.js.map +1 -1
  67. package/dist/tests/parser/expressions.test.js +30 -0
  68. package/dist/tests/parser/expressions.test.js.map +1 -1
  69. package/dist/tests/platform-tarball.test.d.ts +1 -0
  70. package/dist/tests/platform-tarball.test.js +111 -0
  71. package/dist/tests/platform-tarball.test.js.map +1 -0
  72. package/dist/tests/version.test.js +6 -0
  73. package/dist/tests/version.test.js.map +1 -1
  74. package/doc/ai/contributing.md +11 -0
  75. package/doc/ai/extensions.md +7 -4
  76. package/doc/ai/troubleshooting.md +7 -5
  77. package/doc/ai/zh-CN/contributing.md +9 -0
  78. package/doc/ai/zh-CN/extensions.md +4 -2
  79. package/doc/ai/zh-CN/troubleshooting.md +5 -3
  80. package/doc/gui/api.md +61 -13
  81. package/doc/gui/build.md +11 -4
  82. package/doc/gui/dom.md +6 -3
  83. package/doc/gui/media.md +4 -0
  84. package/doc/gui/webgl.md +105 -46
  85. package/doc/gui.md +7 -3
  86. package/doc/npm-package.md +55 -27
  87. package/doc/requirements.md +6 -2
  88. package/doc/zh-CN/gui/api.md +51 -10
  89. package/doc/zh-CN/gui/build.md +7 -2
  90. package/doc/zh-CN/gui/dom.md +3 -1
  91. package/doc/zh-CN/gui/media.md +3 -0
  92. package/doc/zh-CN/gui/webgl.md +22 -16
  93. package/doc/zh-CN/gui.md +3 -2
  94. package/doc/zh-CN/npm-package.md +44 -23
  95. package/doc/zh-CN/requirements.md +4 -1
  96. package/package.json +17 -1
  97. package/runtime/ext_gui/audio.cpp +365 -153
  98. package/runtime/ext_gui/audio_buffer.cpp +166 -0
  99. package/runtime/ext_gui/audio_buffer.h +37 -0
  100. package/runtime/ext_gui/audio_decode.cpp +219 -0
  101. package/runtime/ext_gui/audio_decode.h +39 -0
  102. package/runtime/ext_gui/audio_device.cpp +87 -0
  103. package/runtime/ext_gui/audio_effects.cpp +163 -0
  104. package/runtime/ext_gui/audio_effects.h +25 -0
  105. package/runtime/ext_gui/audio_internal.h +30 -0
  106. package/runtime/ext_gui/audio_mixer.cpp +494 -0
  107. package/runtime/ext_gui/audio_mixer.h +238 -0
  108. package/runtime/ext_gui/audio_param.cpp +104 -0
  109. package/runtime/ext_gui/audio_param_object.cpp +155 -0
  110. package/runtime/ext_gui/browser.cpp +11 -1
  111. package/runtime/ext_gui/css_selector.cpp +21 -3
  112. package/runtime/ext_gui/dom_api_internal.h +16 -0
  113. package/runtime/ext_gui/dom_api_webgl.cpp +7 -0
  114. package/runtime/ext_gui/dom_api_webgl2.cpp +151 -36
  115. package/runtime/ext_gui/dom_api_webgl2_draw.cpp +3 -3
  116. package/runtime/ext_gui/dom_api_webgl_buffer.cpp +6 -2
  117. package/runtime/ext_gui/dom_api_webgl_compressed.cpp +167 -0
  118. package/runtime/ext_gui/dom_api_webgl_framebuffer.cpp +160 -0
  119. package/runtime/ext_gui/dom_api_webgl_shader.cpp +4 -4
  120. package/runtime/ext_gui/dom_api_webgl_source.cpp +161 -0
  121. package/runtime/ext_gui/dom_api_webgl_state.cpp +169 -23
  122. package/runtime/ext_gui/dom_api_webgl_texture.cpp +276 -236
  123. package/runtime/ext_gui/dom_api_webgl_ubo.cpp +81 -0
  124. package/runtime/ext_gui/gui.cpp +44 -9
  125. package/runtime/ext_gui/gui_engine.h +9 -4
  126. package/runtime/ext_gui/image_bitmap.cpp +28 -0
  127. package/runtime/ext_gui/image_bitmap.h +26 -0
  128. package/runtime/ext_gui/image_bitmap_browser.cpp +136 -0
  129. package/runtime/ext_gui/media.cpp +32 -0
  130. package/runtime/ext_gui/media.h +13 -0
  131. package/runtime/ext_gui/media_api.h +23 -0
  132. package/runtime/ext_gui/webgl.h +152 -18
  133. package/runtime/ext_gui/webgl_blit.cpp +95 -0
  134. package/runtime/ext_gui/webgl_buffer.cpp +178 -263
  135. package/runtime/ext_gui/webgl_buffer_objects.cpp +270 -0
  136. package/runtime/ext_gui/webgl_clear.cpp +142 -0
  137. package/runtime/ext_gui/webgl_compressed.cpp +180 -0
  138. package/runtime/ext_gui/webgl_context.cpp +241 -156
  139. package/runtime/ext_gui/webgl_draw.cpp +68 -3
  140. package/runtime/ext_gui/webgl_draw_buffers.cpp +75 -0
  141. package/runtime/ext_gui/webgl_glsl.h +40 -0
  142. package/runtime/ext_gui/webgl_glsl_compile.cpp +6 -0
  143. package/runtime/ext_gui/webgl_glsl_rewrite.cpp +18 -0
  144. package/runtime/ext_gui/webgl_glsl_scan.cpp +20 -11
  145. package/runtime/ext_gui/webgl_glsl_scan_block.cpp +126 -0
  146. package/runtime/ext_gui/webgl_glsl_tokens.h +28 -0
  147. package/runtime/ext_gui/webgl_pipeline.cpp +71 -19
  148. package/runtime/ext_gui/webgl_program_blocks.cpp +58 -0
  149. package/runtime/ext_gui/webgl_program_internal.h +6 -0
  150. package/runtime/ext_gui/webgl_shader.cpp +13 -1
  151. package/runtime/ext_gui/webgl_state.cpp +228 -0
  152. package/runtime/ext_gui/webgl_texture.cpp +144 -17
  153. package/runtime/ext_gui/webgl_texture_3d.cpp +215 -0
  154. package/runtime/ext_gui/webgl_uniform_block.h +31 -0
  155. package/runtime/ext_gui/webgl_webgl2.cpp +58 -5
  156. package/runtime/ext_media/decode_audio.cpp +308 -0
  157. package/runtime/ext_media/media_bridge.c +2 -0
  158. package/runtime/rt.h +17 -0
  159. package/runtime/xt_array_buffer.c +30 -0
  160. package/runtime/xt_typed_array/elements.inc +13 -0
  161. package/scripts/build-media.ts +14 -6
  162. package/scripts/build-runtime.ts +21 -16
  163. package/scripts/check-version.ts +39 -9
  164. package/scripts/package-platform.ts +183 -0
  165. package/scripts/set-platform-version.ts +62 -0
  166. package/scripts/verify-platform-package.ts +182 -0
  167. package/src/cli/main.ts +5 -1
  168. package/src/driver/runtime-lib.ts +73 -9
  169. package/src/driver/version.ts +1 -1
  170. package/src/extensions/gui/index.ts +6 -5
  171. package/src/extensions/media/index.ts +4 -3
  172. package/src/parser/context.ts +99 -4
  173. package/src/parser/literals.ts +6 -12
@@ -0,0 +1,494 @@
1
+ /*
2
+ * xbintsc GUI engine — Web Audio graph rendering.
3
+ *
4
+ * See `audio_mixer.h` for the ownership and threading model. This file is the
5
+ * DSP: oscillator/buffer/constant sources, gain, biquad, compressor, stereo
6
+ * panner and delay, plus the per-sample AudioParam evaluation they all share.
7
+ */
8
+
9
+ #include "audio_mixer.h"
10
+
11
+ #include "audio_effects.h"
12
+
13
+ #include <algorithm>
14
+ #include <cmath>
15
+ #include <cstring>
16
+
17
+ namespace xtgui {
18
+
19
+ namespace {
20
+
21
+ constexpr double kPi = 3.14159265358979323846;
22
+ constexpr int kSamples = kAudioQuantum * kAudioMixChannels;
23
+
24
+ double clamp_value(double value, double low, double high) {
25
+ if (!(value > low)) return low;
26
+ if (value > high) return high;
27
+ return value;
28
+ }
29
+
30
+ double notes_to_rate(double detune_cents) {
31
+ return std::pow(2.0, detune_cents / 1200.0);
32
+ }
33
+
34
+ /* One quantized waveform sample at `phase` in [0, 1). */
35
+ double waveform(AudioOscType type, double phase) {
36
+ switch (type) {
37
+ case kAudioOscSquare:
38
+ return phase < 0.5 ? 1.0 : -1.0;
39
+ case kAudioOscSawtooth:
40
+ return 2.0 * phase - 1.0;
41
+ case kAudioOscTriangle:
42
+ return phase < 0.5 ? 4.0 * phase - 1.0 : 3.0 - 4.0 * phase;
43
+ case kAudioOscSine:
44
+ default:
45
+ return std::sin(phase * 2.0 * kPi);
46
+ }
47
+ }
48
+
49
+ } // namespace
50
+
51
+ /* -- AudioMixer: graph ---------------------------------------------------- */
52
+
53
+ AudioMixer::AudioMixer(bool offline) : offline_(offline) {
54
+ pending_.reserve(kSamples);
55
+ }
56
+
57
+ AudioMixer::~AudioMixer() { close_device(); }
58
+
59
+ AudioNode *AudioMixer::node_locked(uint64_t id) {
60
+ auto it = nodes_.find(id);
61
+ return it == nodes_.end() ? nullptr : it->second.get();
62
+ }
63
+
64
+ uint64_t AudioMixer::create_node(AudioNodeKind kind) {
65
+ std::lock_guard<std::mutex> lock(mutex_);
66
+ auto node = std::make_shared<AudioNode>();
67
+ node->id = next_node_id_++;
68
+ node->kind = kind;
69
+ node->quantum.assign(kSamples, 0.0f);
70
+ switch (kind) {
71
+ case kAudioGain:
72
+ node->param_count = 1;
73
+ node->params[0].reset(1.0);
74
+ break;
75
+ case kAudioOscillator:
76
+ node->param_count = 2;
77
+ node->params[0].reset(440.0);
78
+ node->params[1].reset(0.0);
79
+ break;
80
+ case kAudioBufferSource:
81
+ node->param_count = 2;
82
+ node->params[0].reset(1.0);
83
+ node->params[1].reset(0.0);
84
+ break;
85
+ case kAudioBiquad:
86
+ node->param_count = 4;
87
+ node->params[0].reset(350.0);
88
+ node->params[1].reset(0.0);
89
+ node->params[2].reset(1.0);
90
+ node->params[3].reset(0.0);
91
+ break;
92
+ case kAudioCompressor:
93
+ node->param_count = 5;
94
+ node->params[0].reset(-24.0);
95
+ node->params[1].reset(30.0);
96
+ node->params[2].reset(12.0);
97
+ node->params[3].reset(0.003);
98
+ node->params[4].reset(0.25);
99
+ break;
100
+ case kAudioStereoPanner:
101
+ node->param_count = 1;
102
+ node->params[0].reset(0.0);
103
+ break;
104
+ case kAudioConstantSource:
105
+ node->param_count = 1;
106
+ node->params[0].reset(1.0);
107
+ break;
108
+ case kAudioDelay:
109
+ node->param_count = 1;
110
+ node->params[0].reset(0.0);
111
+ break;
112
+ case kAudioDestination:
113
+ default:
114
+ node->param_count = 0;
115
+ break;
116
+ }
117
+ nodes_[node->id] = node;
118
+ if (kind == kAudioDestination) destination_id_ = node->id;
119
+ return node->id;
120
+ }
121
+
122
+ void AudioMixer::connect(uint64_t source, uint64_t target) {
123
+ std::lock_guard<std::mutex> lock(mutex_);
124
+ AudioNode *from = node_locked(source);
125
+ AudioNode *to = node_locked(target);
126
+ if (from == nullptr || to == nullptr) return;
127
+ for (uint64_t id : to->inputs) {
128
+ if (id == source) return;
129
+ }
130
+ to->inputs.push_back(source);
131
+ }
132
+
133
+ void AudioMixer::disconnect(uint64_t source, uint64_t target) {
134
+ std::lock_guard<std::mutex> lock(mutex_);
135
+ AudioNode *to = node_locked(target);
136
+ if (to == nullptr) return;
137
+ to->inputs.erase(std::remove(to->inputs.begin(), to->inputs.end(), source), to->inputs.end());
138
+ }
139
+
140
+ void AudioMixer::disconnect_all(uint64_t source) {
141
+ std::lock_guard<std::mutex> lock(mutex_);
142
+ for (auto &entry : nodes_) {
143
+ std::vector<uint64_t> &inputs = entry.second->inputs;
144
+ inputs.erase(std::remove(inputs.begin(), inputs.end(), source), inputs.end());
145
+ }
146
+ }
147
+
148
+ void AudioMixer::set_osc_type(uint64_t node, AudioOscType type) {
149
+ std::lock_guard<std::mutex> lock(mutex_);
150
+ AudioNode *target = node_locked(node);
151
+ if (target != nullptr) target->osc_type = type;
152
+ }
153
+
154
+ void AudioMixer::set_biquad_type(uint64_t node, AudioBiquadType type) {
155
+ std::lock_guard<std::mutex> lock(mutex_);
156
+ AudioNode *target = node_locked(node);
157
+ if (target != nullptr) target->biquad_type = type;
158
+ }
159
+
160
+ void AudioMixer::set_delay_max(uint64_t node, double seconds) {
161
+ std::lock_guard<std::mutex> lock(mutex_);
162
+ AudioNode *target = node_locked(node);
163
+ if (target == nullptr) return;
164
+ seconds = clamp_value(seconds, 0.001, 60.0);
165
+ target->delay_max = seconds;
166
+ target->delay_size = static_cast<size_t>(seconds * kAudioMixRate) + 2;
167
+ target->delay_write = 0;
168
+ for (int channel = 0; channel < kAudioMixChannels; channel++) {
169
+ target->delay[channel].assign(target->delay_size, 0.0f);
170
+ }
171
+ }
172
+
173
+ void AudioMixer::set_buffer(uint64_t node, std::shared_ptr<AudioClip> clip, bool loop,
174
+ double loop_start, double loop_end) {
175
+ std::lock_guard<std::mutex> lock(mutex_);
176
+ AudioNode *target = node_locked(node);
177
+ if (target == nullptr) return;
178
+ target->clip = std::move(clip);
179
+ target->loop = loop;
180
+ target->loop_start = loop_start;
181
+ target->loop_end = loop_end;
182
+ }
183
+
184
+ void AudioMixer::start_source(uint64_t node, double when, double offset, double duration) {
185
+ std::lock_guard<std::mutex> lock(mutex_);
186
+ AudioNode *target = node_locked(node);
187
+ if (target == nullptr) return;
188
+ double now = static_cast<double>(frames_rendered_) / kAudioMixRate;
189
+ target->started = true;
190
+ target->began = false;
191
+ target->ended = false;
192
+ target->start_time = when > now ? when : now;
193
+ target->stop_time = -1.0;
194
+ target->clip_offset = offset > 0.0 ? offset : 0.0;
195
+ target->clip_duration = duration;
196
+ target->clip_pos = target->clip_offset * (target->clip ? target->clip->sample_rate
197
+ : kAudioMixRate);
198
+ }
199
+
200
+ void AudioMixer::stop_source(uint64_t node, double when) {
201
+ std::lock_guard<std::mutex> lock(mutex_);
202
+ AudioNode *target = node_locked(node);
203
+ if (target == nullptr) return;
204
+ double now = static_cast<double>(frames_rendered_) / kAudioMixRate;
205
+ target->stop_time = when > now ? when : now;
206
+ }
207
+
208
+ /* -- AudioMixer: parameters ----------------------------------------------- */
209
+
210
+ double AudioMixer::param_value(uint64_t node, int index) {
211
+ std::lock_guard<std::mutex> lock(mutex_);
212
+ AudioNode *target = node_locked(node);
213
+ if (target == nullptr || index < 0 || index >= target->param_count) return 0.0;
214
+ double now = static_cast<double>(frames_rendered_) / kAudioMixRate;
215
+ return target->params[index].value_at(now);
216
+ }
217
+
218
+ void AudioMixer::param_set(uint64_t node, int index, double value) {
219
+ std::lock_guard<std::mutex> lock(mutex_);
220
+ AudioNode *target = node_locked(node);
221
+ if (target == nullptr || index < 0 || index >= target->param_count) return;
222
+ double now = static_cast<double>(frames_rendered_) / kAudioMixRate;
223
+ target->params[index].set_value(value, now);
224
+ }
225
+
226
+ void AudioMixer::param_set_at(uint64_t node, int index, double value, double time) {
227
+ std::lock_guard<std::mutex> lock(mutex_);
228
+ AudioNode *target = node_locked(node);
229
+ if (target != nullptr && index >= 0 && index < target->param_count) {
230
+ target->params[index].set_value_at_time(value, time);
231
+ }
232
+ }
233
+
234
+ void AudioMixer::param_linear_ramp(uint64_t node, int index, double value, double time) {
235
+ std::lock_guard<std::mutex> lock(mutex_);
236
+ AudioNode *target = node_locked(node);
237
+ if (target != nullptr && index >= 0 && index < target->param_count) {
238
+ target->params[index].linear_ramp(value, time);
239
+ }
240
+ }
241
+
242
+ void AudioMixer::param_exponential_ramp(uint64_t node, int index, double value, double time) {
243
+ std::lock_guard<std::mutex> lock(mutex_);
244
+ AudioNode *target = node_locked(node);
245
+ if (target != nullptr && index >= 0 && index < target->param_count) {
246
+ target->params[index].exponential_ramp(value, time);
247
+ }
248
+ }
249
+
250
+ void AudioMixer::param_set_target(uint64_t node, int index, double value, double time,
251
+ double time_constant) {
252
+ std::lock_guard<std::mutex> lock(mutex_);
253
+ AudioNode *target = node_locked(node);
254
+ if (target != nullptr && index >= 0 && index < target->param_count) {
255
+ target->params[index].set_target(value, time, time_constant);
256
+ }
257
+ }
258
+
259
+ void AudioMixer::param_cancel(uint64_t node, int index, double time, bool hold) {
260
+ std::lock_guard<std::mutex> lock(mutex_);
261
+ AudioNode *target = node_locked(node);
262
+ if (target != nullptr && index >= 0 && index < target->param_count) {
263
+ if (hold) {
264
+ target->params[index].cancel_and_hold(time);
265
+ } else {
266
+ target->params[index].cancel_scheduled(time);
267
+ }
268
+ }
269
+ }
270
+
271
+ /* -- AudioMixer: clock ---------------------------------------------------- */
272
+
273
+ double AudioMixer::current_time() {
274
+ std::lock_guard<std::mutex> lock(mutex_);
275
+ return static_cast<double>(frames_rendered_) / kAudioMixRate;
276
+ }
277
+
278
+ void AudioMixer::shutdown() {
279
+ close_device();
280
+ std::lock_guard<std::mutex> lock(mutex_);
281
+ nodes_.clear();
282
+ destination_id_ = 0;
283
+ }
284
+
285
+ /* -- AudioMixer: render --------------------------------------------------- */
286
+
287
+ void AudioMixer::render_locked(float *out, int frames) {
288
+ int done = 0;
289
+ while (done < frames) {
290
+ if (pending_offset_ >= pending_.size()) {
291
+ render_quantum_locked();
292
+ pending_offset_ = 0;
293
+ }
294
+ size_t available = pending_.size() - pending_offset_;
295
+ size_t wanted = static_cast<size_t>(frames - done) * kAudioMixChannels;
296
+ size_t take = std::min(available, wanted);
297
+ std::memcpy(out + static_cast<size_t>(done) * kAudioMixChannels,
298
+ pending_.data() + pending_offset_, take * sizeof(float));
299
+ pending_offset_ += take;
300
+ done += static_cast<int>(take / kAudioMixChannels);
301
+ }
302
+ }
303
+
304
+ void AudioMixer::render(float *out, int frames) {
305
+ std::lock_guard<std::mutex> lock(mutex_);
306
+ render_locked(out, frames);
307
+ }
308
+
309
+ void AudioMixer::render_offline(float *out, int frames) {
310
+ if (!offline_) return;
311
+ std::lock_guard<std::mutex> lock(mutex_);
312
+ render_locked(out, frames);
313
+ }
314
+
315
+ void AudioMixer::render_quantum_locked() {
316
+ serial_++;
317
+ pending_.assign(kSamples, 0.0f);
318
+ pending_offset_ = 0;
319
+ AudioNode *destination = node_locked(destination_id_);
320
+ if (destination != nullptr) {
321
+ const float *mixed = pull_locked(destination);
322
+ if (mixed != nullptr) std::memcpy(pending_.data(), mixed, sizeof(float) * kSamples);
323
+ }
324
+ frames_rendered_ += kAudioQuantum;
325
+ }
326
+
327
+ const float *AudioMixer::pull_locked(AudioNode *node) {
328
+ if (node == nullptr) return nullptr;
329
+ if (node->serial == serial_) return node->quantum.data();
330
+ if (node->rendering) {
331
+ std::fill(node->quantum.begin(), node->quantum.end(), 0.0f);
332
+ return node->quantum.data();
333
+ }
334
+ node->rendering = true;
335
+ std::fill(node->quantum.begin(), node->quantum.end(), 0.0f);
336
+
337
+ double quantum_start = static_cast<double>(frames_rendered_) / kAudioMixRate;
338
+ double step = 1.0 / kAudioMixRate;
339
+
340
+ if (node->kind == kAudioDestination || node->kind == kAudioGain ||
341
+ node->kind == kAudioBiquad || node->kind == kAudioCompressor ||
342
+ node->kind == kAudioStereoPanner || node->kind == kAudioDelay) {
343
+ float input[kSamples];
344
+ std::memset(input, 0, sizeof(input));
345
+ for (uint64_t id : node->inputs) {
346
+ const float *source = pull_locked(node_locked(id));
347
+ if (source == nullptr) continue;
348
+ for (int i = 0; i < kSamples; i++) input[i] += source[i];
349
+ }
350
+ if (node->kind == kAudioDestination) {
351
+ std::memcpy(node->quantum.data(), input, sizeof(input));
352
+ } else if (node->kind == kAudioGain) {
353
+ for (int frame = 0; frame < kAudioQuantum; frame++) {
354
+ double gain = node->params[0].value_at(quantum_start + frame * step);
355
+ node->quantum[frame * 2] = static_cast<float>(input[frame * 2] * gain);
356
+ node->quantum[frame * 2 + 1] = static_cast<float>(input[frame * 2 + 1] * gain);
357
+ }
358
+ } else if (node->kind == kAudioStereoPanner) {
359
+ for (int frame = 0; frame < kAudioQuantum; frame++) {
360
+ double pan = clamp_value(node->params[0].value_at(quantum_start + frame * step), -1.0, 1.0);
361
+ double angle = (pan + 1.0) * kPi / 4.0;
362
+ node->quantum[frame * 2] = static_cast<float>(input[frame * 2] * std::cos(angle));
363
+ node->quantum[frame * 2 + 1] =
364
+ static_cast<float>(input[frame * 2 + 1] * std::sin(angle));
365
+ }
366
+ } else if (node->kind == kAudioDelay) {
367
+ if (node->delay_size > 1) {
368
+ double maximum = static_cast<double>(node->delay_size - 2);
369
+ for (int frame = 0; frame < kAudioQuantum; frame++) {
370
+ double seconds = node->params[0].value_at(quantum_start + frame * step);
371
+ double samples = clamp_value(seconds, 0.0, node->delay_max) * kAudioMixRate;
372
+ if (samples > maximum) samples = maximum;
373
+ node->delay[0][node->delay_write] = input[frame * 2];
374
+ node->delay[1][node->delay_write] = input[frame * 2 + 1];
375
+ double read = static_cast<double>(node->delay_write) - samples;
376
+ while (read < 0.0) read += static_cast<double>(node->delay_size);
377
+ size_t lower = static_cast<size_t>(read);
378
+ double fraction = read - static_cast<double>(lower);
379
+ size_t upper = lower + 1 < node->delay_size ? lower + 1 : 0;
380
+ for (int channel = 0; channel < kAudioMixChannels; channel++) {
381
+ double delayed = node->delay[channel][lower] * (1.0 - fraction) +
382
+ node->delay[channel][upper] * fraction;
383
+ node->quantum[frame * 2 + channel] = static_cast<float>(delayed);
384
+ }
385
+ node->delay_write = (node->delay_write + 1) % node->delay_size;
386
+ }
387
+ } else {
388
+ std::memcpy(node->quantum.data(), input, sizeof(input));
389
+ }
390
+ } else if (node->kind == kAudioBiquad) {
391
+ render_biquad(node, input, quantum_start);
392
+ } else {
393
+ render_compressor(node, input, quantum_start);
394
+ }
395
+ } else if (node->kind == kAudioConstantSource) {
396
+ if (node->started) {
397
+ for (int frame = 0; frame < kAudioQuantum; frame++) {
398
+ double time = quantum_start + frame * step;
399
+ if (time < node->start_time) continue;
400
+ if (node->stop_time >= 0.0 && time >= node->stop_time) {
401
+ node->ended = true;
402
+ break;
403
+ }
404
+ double value = node->params[0].value_at(time);
405
+ node->quantum[frame * 2] = static_cast<float>(value);
406
+ node->quantum[frame * 2 + 1] = static_cast<float>(value);
407
+ }
408
+ }
409
+ } else if (node->kind == kAudioOscillator) {
410
+ if (node->started) {
411
+ for (int frame = 0; frame < kAudioQuantum; frame++) {
412
+ double time = quantum_start + frame * step;
413
+ if (time < node->start_time) continue;
414
+ if (node->stop_time >= 0.0 && time >= node->stop_time) {
415
+ node->ended = true;
416
+ break;
417
+ }
418
+ if (!node->began) {
419
+ node->began = true;
420
+ node->phase = 0.0;
421
+ }
422
+ double frequency = node->params[0].value_at(time) *
423
+ notes_to_rate(node->params[1].value_at(time));
424
+ frequency = std::max(frequency, 0.0);
425
+ double sample = waveform(node->osc_type, node->phase);
426
+ node->quantum[frame * 2] = static_cast<float>(sample);
427
+ node->quantum[frame * 2 + 1] = static_cast<float>(sample);
428
+ node->phase += frequency / kAudioMixRate;
429
+ if (node->phase >= 1.0) node->phase -= std::floor(node->phase);
430
+ }
431
+ }
432
+ } else if (node->kind == kAudioBufferSource) {
433
+ std::shared_ptr<AudioClip> clip = node->clip;
434
+ if (node->started && clip && clip->length > 0) {
435
+ double ratio = static_cast<double>(clip->sample_rate) / kAudioMixRate;
436
+ size_t length = clip->length;
437
+ for (int frame = 0; frame < kAudioQuantum; frame++) {
438
+ double time = quantum_start + frame * step;
439
+ if (time < node->start_time) continue;
440
+ if (node->stop_time >= 0.0 && time >= node->stop_time) {
441
+ node->ended = true;
442
+ break;
443
+ }
444
+ if (node->clip_duration > 0.0 && time - node->start_time >= node->clip_duration) {
445
+ node->ended = true;
446
+ break;
447
+ }
448
+ if (!node->began) node->began = true;
449
+ double playback = node->params[0].value_at(time) *
450
+ notes_to_rate(node->params[1].value_at(time));
451
+ if (playback < 0.0) playback = 0.0;
452
+ double position = node->clip_pos;
453
+ if (node->loop) {
454
+ double loop_end = node->loop_end > node->loop_start
455
+ ? node->loop_end * clip->sample_rate
456
+ : static_cast<double>(length);
457
+ double loop_start = node->loop_start * clip->sample_rate;
458
+ loop_end = std::min(loop_end, static_cast<double>(length));
459
+ if (loop_end > loop_start && position >= loop_end) {
460
+ double span = loop_end - loop_start;
461
+ position = loop_start + std::fmod(position - loop_start, span);
462
+ node->clip_pos = position;
463
+ }
464
+ }
465
+ if (!node->loop && position >= static_cast<double>(length)) {
466
+ node->ended = true;
467
+ break;
468
+ }
469
+ if (position < 0.0) position = 0.0;
470
+ size_t index = static_cast<size_t>(position);
471
+ if (index >= length) index = length - 1;
472
+ size_t next = index + 1 < length ? index + 1 : index;
473
+ double fraction = position - static_cast<double>(index);
474
+ double left = clip->channels[0][index] +
475
+ (clip->channels[0][next] - clip->channels[0][index]) * fraction;
476
+ double right = left;
477
+ if (clip->channels.size() > 1) {
478
+ right = clip->channels[1][index] +
479
+ (clip->channels[1][next] - clip->channels[1][index]) * fraction;
480
+ }
481
+ node->quantum[frame * 2] = static_cast<float>(left);
482
+ node->quantum[frame * 2 + 1] = static_cast<float>(right);
483
+ node->clip_pos += playback * ratio;
484
+ }
485
+ if (node->ended && !node->loop) node->clip.reset();
486
+ }
487
+ }
488
+
489
+ node->rendering = false;
490
+ node->serial = serial_;
491
+ return node->quantum.data();
492
+ }
493
+
494
+ } // namespace xtgui