xbintsc 0.5.32 → 0.5.37
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/README.md +10 -7
- package/README.zh-CN.md +10 -8
- package/dist/scripts/check-version.d.ts +27 -0
- package/dist/scripts/check-version.js +71 -0
- package/dist/scripts/check-version.js.map +1 -0
- package/dist/scripts/package-platform.d.ts +58 -0
- package/dist/scripts/package-platform.js +142 -0
- package/dist/scripts/package-platform.js.map +1 -0
- package/dist/scripts/verify-platform-package.d.ts +34 -0
- package/dist/scripts/verify-platform-package.js +161 -0
- package/dist/scripts/verify-platform-package.js.map +1 -0
- package/dist/src/cli/help.d.ts +1 -1
- package/dist/src/cli/main.js +3 -1
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/driver/runtime-lib.d.ts +29 -4
- package/dist/src/driver/runtime-lib.js +68 -10
- package/dist/src/driver/runtime-lib.js.map +1 -1
- package/dist/src/driver/version.d.ts +1 -1
- package/dist/src/driver/version.js +1 -1
- package/dist/src/extensions/gui/index.js +18 -5
- package/dist/src/extensions/gui/index.js.map +1 -1
- package/dist/src/extensions/media/index.js +4 -3
- package/dist/src/extensions/media/index.js.map +1 -1
- package/dist/src/parser/context.d.ts +41 -0
- package/dist/src/parser/context.js +89 -4
- package/dist/src/parser/context.js.map +1 -1
- package/dist/src/parser/literals.js +6 -12
- package/dist/src/parser/literals.js.map +1 -1
- package/dist/tests/driver/platform-package.test.d.ts +1 -0
- package/dist/tests/driver/platform-package.test.js +49 -0
- package/dist/tests/driver/platform-package.test.js.map +1 -0
- package/dist/tests/driver/runtime-lib.test.js +51 -4
- package/dist/tests/driver/runtime-lib.test.js.map +1 -1
- package/dist/tests/e2e/gui-audio.test.d.ts +14 -0
- package/dist/tests/e2e/gui-audio.test.js +287 -0
- package/dist/tests/e2e/gui-audio.test.js.map +1 -0
- package/dist/tests/e2e/gui-browser.test.js +65 -0
- package/dist/tests/e2e/gui-browser.test.js.map +1 -1
- package/dist/tests/e2e/gui-canvas-args.test.d.ts +6 -0
- package/dist/tests/e2e/gui-canvas-args.test.js +217 -0
- package/dist/tests/e2e/gui-canvas-args.test.js.map +1 -0
- package/dist/tests/e2e/gui-canvas.test.js.map +1 -1
- package/dist/tests/e2e/gui-dom.test.js +215 -0
- package/dist/tests/e2e/gui-dom.test.js.map +1 -1
- package/dist/tests/e2e/gui-events.test.js +112 -0
- package/dist/tests/e2e/gui-events.test.js.map +1 -1
- package/dist/tests/e2e/gui-helpers.d.ts +7 -0
- package/dist/tests/e2e/gui-helpers.js +1 -0
- package/dist/tests/e2e/gui-helpers.js.map +1 -1
- package/dist/tests/e2e/gui-media.test.js +78 -0
- package/dist/tests/e2e/gui-media.test.js.map +1 -1
- package/dist/tests/e2e/gui-style.test.js +55 -0
- package/dist/tests/e2e/gui-style.test.js.map +1 -1
- package/dist/tests/e2e/gui-webgl.test.js +258 -0
- package/dist/tests/e2e/gui-webgl.test.js.map +1 -1
- package/dist/tests/e2e/gui-webgl2-3d.test.d.ts +4 -0
- package/dist/tests/e2e/gui-webgl2-3d.test.js +264 -0
- package/dist/tests/e2e/gui-webgl2-3d.test.js.map +1 -0
- package/dist/tests/e2e/gui-webgl2-blit.test.d.ts +10 -0
- package/dist/tests/e2e/gui-webgl2-blit.test.js +262 -0
- package/dist/tests/e2e/gui-webgl2-blit.test.js.map +1 -0
- package/dist/tests/e2e/gui-webgl2-formats.test.d.ts +6 -0
- package/dist/tests/e2e/gui-webgl2-formats.test.js +395 -0
- package/dist/tests/e2e/gui-webgl2-formats.test.js.map +1 -0
- package/dist/tests/e2e/gui-webgl2-mrt.test.d.ts +4 -0
- package/dist/tests/e2e/gui-webgl2-mrt.test.js +105 -0
- package/dist/tests/e2e/gui-webgl2-mrt.test.js.map +1 -0
- package/dist/tests/e2e/gui-webgl2-ubo.test.d.ts +8 -0
- package/dist/tests/e2e/gui-webgl2-ubo.test.js +112 -0
- package/dist/tests/e2e/gui-webgl2-ubo.test.js.map +1 -0
- package/dist/tests/e2e/gui-webgl2.test.js +45 -1
- package/dist/tests/e2e/gui-webgl2.test.js.map +1 -1
- package/dist/tests/extensions/gui-index.test.js +1 -0
- package/dist/tests/extensions/gui-index.test.js.map +1 -1
- package/dist/tests/extensions/media.test.js +1 -0
- package/dist/tests/extensions/media.test.js.map +1 -1
- package/dist/tests/parser/expressions.test.js +30 -0
- package/dist/tests/parser/expressions.test.js.map +1 -1
- package/dist/tests/platform-tarball.test.d.ts +1 -0
- package/dist/tests/platform-tarball.test.js +111 -0
- package/dist/tests/platform-tarball.test.js.map +1 -0
- package/dist/tests/version.test.js +6 -0
- package/dist/tests/version.test.js.map +1 -1
- package/doc/ai/contributing.md +11 -0
- package/doc/ai/extensions.md +7 -4
- package/doc/ai/troubleshooting.md +7 -5
- package/doc/ai/zh-CN/contributing.md +9 -0
- package/doc/ai/zh-CN/extensions.md +4 -2
- package/doc/ai/zh-CN/troubleshooting.md +5 -3
- package/doc/gui/api.md +99 -22
- package/doc/gui/build.md +11 -4
- package/doc/gui/canvas.md +49 -18
- package/doc/gui/dom.md +57 -7
- package/doc/gui/media.md +4 -0
- package/doc/gui/webgl.md +105 -46
- package/doc/gui.md +7 -3
- package/doc/npm-package.md +55 -27
- package/doc/requirements.md +6 -2
- package/doc/zh-CN/gui/api.md +84 -14
- package/doc/zh-CN/gui/build.md +7 -2
- package/doc/zh-CN/gui/canvas.md +44 -19
- package/doc/zh-CN/gui/dom.md +48 -4
- package/doc/zh-CN/gui/media.md +3 -0
- package/doc/zh-CN/gui/webgl.md +22 -16
- package/doc/zh-CN/gui.md +3 -2
- package/doc/zh-CN/npm-package.md +44 -23
- package/doc/zh-CN/requirements.md +4 -1
- package/package.json +17 -1
- package/runtime/ext_gui/audio.cpp +365 -153
- package/runtime/ext_gui/audio_buffer.cpp +166 -0
- package/runtime/ext_gui/audio_buffer.h +37 -0
- package/runtime/ext_gui/audio_decode.cpp +219 -0
- package/runtime/ext_gui/audio_decode.h +39 -0
- package/runtime/ext_gui/audio_device.cpp +87 -0
- package/runtime/ext_gui/audio_effects.cpp +163 -0
- package/runtime/ext_gui/audio_effects.h +25 -0
- package/runtime/ext_gui/audio_internal.h +30 -0
- package/runtime/ext_gui/audio_mixer.cpp +494 -0
- package/runtime/ext_gui/audio_mixer.h +238 -0
- package/runtime/ext_gui/audio_param.cpp +104 -0
- package/runtime/ext_gui/audio_param_object.cpp +155 -0
- package/runtime/ext_gui/browser.cpp +45 -50
- package/runtime/ext_gui/browser_abort.cpp +249 -0
- package/runtime/ext_gui/browser_dialogs.cpp +76 -0
- package/runtime/ext_gui/browser_media.cpp +176 -0
- package/runtime/ext_gui/browser_navigation.cpp +130 -0
- package/runtime/ext_gui/canvas.cpp +88 -30
- package/runtime/ext_gui/canvas.h +74 -9
- package/runtime/ext_gui/canvas_image_data.cpp +143 -0
- package/runtime/ext_gui/canvas_internal.h +8 -5
- package/runtime/ext_gui/canvas_pattern.cpp +90 -0
- package/runtime/ext_gui/canvas_raster.cpp +31 -9
- package/runtime/ext_gui/canvas_style.cpp +4 -2
- package/runtime/ext_gui/css_selector.cpp +21 -3
- package/runtime/ext_gui/dom.h +4 -0
- package/runtime/ext_gui/dom_api.cpp +1 -0
- package/runtime/ext_gui/dom_api_canvas.cpp +120 -114
- package/runtime/ext_gui/dom_api_canvas_extra.cpp +57 -2
- package/runtime/ext_gui/dom_api_canvas_image.cpp +239 -0
- package/runtime/ext_gui/dom_api_canvas_paths.cpp +179 -0
- package/runtime/ext_gui/dom_api_computed_style.cpp +124 -0
- package/runtime/ext_gui/dom_api_dataset.cpp +154 -0
- package/runtime/ext_gui/dom_api_document.cpp +58 -5
- package/runtime/ext_gui/dom_api_events.cpp +93 -17
- package/runtime/ext_gui/dom_api_internal.h +71 -2
- package/runtime/ext_gui/dom_api_mutation.cpp +159 -0
- package/runtime/ext_gui/dom_api_node.cpp +40 -19
- package/runtime/ext_gui/dom_api_node_read.cpp +37 -1
- package/runtime/ext_gui/dom_api_path2d.cpp +358 -0
- package/runtime/ext_gui/dom_api_proto.cpp +35 -0
- package/runtime/ext_gui/dom_api_style.cpp +223 -13
- package/runtime/ext_gui/dom_api_webgl.cpp +7 -0
- package/runtime/ext_gui/dom_api_webgl2.cpp +151 -36
- package/runtime/ext_gui/dom_api_webgl2_draw.cpp +3 -3
- package/runtime/ext_gui/dom_api_webgl_buffer.cpp +6 -2
- package/runtime/ext_gui/dom_api_webgl_compressed.cpp +167 -0
- package/runtime/ext_gui/dom_api_webgl_framebuffer.cpp +160 -0
- package/runtime/ext_gui/dom_api_webgl_shader.cpp +4 -4
- package/runtime/ext_gui/dom_api_webgl_source.cpp +161 -0
- package/runtime/ext_gui/dom_api_webgl_state.cpp +169 -23
- package/runtime/ext_gui/dom_api_webgl_texture.cpp +276 -236
- package/runtime/ext_gui/dom_api_webgl_ubo.cpp +81 -0
- package/runtime/ext_gui/gui.cpp +103 -14
- package/runtime/ext_gui/gui_engine.h +56 -4
- package/runtime/ext_gui/gui_events.cpp +80 -9
- package/runtime/ext_gui/image_bitmap.cpp +28 -0
- package/runtime/ext_gui/image_bitmap.h +26 -0
- package/runtime/ext_gui/image_bitmap_browser.cpp +136 -0
- package/runtime/ext_gui/listener_options.h +43 -0
- package/runtime/ext_gui/media.cpp +32 -0
- package/runtime/ext_gui/media.h +13 -0
- package/runtime/ext_gui/media_api.h +23 -0
- package/runtime/ext_gui/paint.cpp +28 -0
- package/runtime/ext_gui/paint.h +4 -0
- package/runtime/ext_gui/webgl.h +152 -18
- package/runtime/ext_gui/webgl_blit.cpp +95 -0
- package/runtime/ext_gui/webgl_buffer.cpp +178 -263
- package/runtime/ext_gui/webgl_buffer_objects.cpp +270 -0
- package/runtime/ext_gui/webgl_clear.cpp +142 -0
- package/runtime/ext_gui/webgl_compressed.cpp +180 -0
- package/runtime/ext_gui/webgl_context.cpp +241 -156
- package/runtime/ext_gui/webgl_draw.cpp +68 -3
- package/runtime/ext_gui/webgl_draw_buffers.cpp +75 -0
- package/runtime/ext_gui/webgl_glsl.h +40 -0
- package/runtime/ext_gui/webgl_glsl_compile.cpp +6 -0
- package/runtime/ext_gui/webgl_glsl_rewrite.cpp +18 -0
- package/runtime/ext_gui/webgl_glsl_scan.cpp +20 -11
- package/runtime/ext_gui/webgl_glsl_scan_block.cpp +126 -0
- package/runtime/ext_gui/webgl_glsl_tokens.h +28 -0
- package/runtime/ext_gui/webgl_pipeline.cpp +71 -19
- package/runtime/ext_gui/webgl_program_blocks.cpp +58 -0
- package/runtime/ext_gui/webgl_program_internal.h +6 -0
- package/runtime/ext_gui/webgl_shader.cpp +13 -1
- package/runtime/ext_gui/webgl_state.cpp +228 -0
- package/runtime/ext_gui/webgl_texture.cpp +144 -17
- package/runtime/ext_gui/webgl_texture_3d.cpp +215 -0
- package/runtime/ext_gui/webgl_uniform_block.h +31 -0
- package/runtime/ext_gui/webgl_webgl2.cpp +58 -5
- package/runtime/ext_gui/window.cpp +14 -65
- package/runtime/ext_gui/window_events.cpp +125 -0
- package/runtime/ext_media/decode_audio.cpp +308 -0
- package/runtime/ext_media/media_bridge.c +2 -0
- package/runtime/rt.h +43 -0
- package/runtime/rt_internal.h +4 -0
- package/runtime/xt_array_buffer.c +30 -0
- package/runtime/xt_builtins.c +0 -21
- package/runtime/xt_containers/access.inc +94 -2
- package/runtime/xt_containers/objects.inc +29 -31
- package/runtime/xt_typed_array/elements.inc +13 -0
- package/scripts/build-media.ts +14 -6
- package/scripts/build-runtime.ts +21 -16
- package/scripts/check-version.ts +39 -9
- package/scripts/package-platform.ts +183 -0
- package/scripts/set-platform-version.ts +62 -0
- package/scripts/verify-platform-package.ts +182 -0
- package/src/cli/main.ts +5 -1
- package/src/driver/runtime-lib.ts +73 -9
- package/src/driver/version.ts +1 -1
- package/src/extensions/gui/index.ts +18 -5
- package/src/extensions/media/index.ts +4 -3
- package/src/parser/context.ts +99 -4
- package/src/parser/literals.ts +6 -12
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — AudioBuffer value construction and sample transfer.
|
|
3
|
+
*
|
|
4
|
+
* The mixer never sees an `xt_value`: `audio_clip_from_buffer` copies the
|
|
5
|
+
* samples a BufferSource needs when it starts, so a later collection of the
|
|
6
|
+
* AudioBuffer (or a mutation of its Float32Arrays) cannot race the audio
|
|
7
|
+
* thread. `audio_buffer_create` fills a freshly decoded buffer through the
|
|
8
|
+
* runtime's bulk ArrayBuffer copy instead of one JS property write per sample.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
#include "audio_buffer.h"
|
|
12
|
+
|
|
13
|
+
#include <cstring>
|
|
14
|
+
|
|
15
|
+
#include "audio_mixer.h"
|
|
16
|
+
|
|
17
|
+
namespace xtgui {
|
|
18
|
+
|
|
19
|
+
namespace {
|
|
20
|
+
|
|
21
|
+
/* Byte endianness matches the host, which is what the runtime's typed array
|
|
22
|
+
* read/write also assumes (it memcpy's the element bytes). */
|
|
23
|
+
xt_value make_channel(const float *samples, uint32_t length) {
|
|
24
|
+
xt_value array_buffer = xt_array_buffer_alloc(length * static_cast<uint32_t>(sizeof(float)));
|
|
25
|
+
if (samples != nullptr && length > 0) {
|
|
26
|
+
xt_array_buffer_copy_bytes_in(array_buffer, 0,
|
|
27
|
+
reinterpret_cast<const uint8_t *>(samples),
|
|
28
|
+
length * static_cast<uint32_t>(sizeof(float)));
|
|
29
|
+
}
|
|
30
|
+
xt_value constructor_arg = array_buffer;
|
|
31
|
+
return xt_typed_array_ctor_by_name(xt_string_from_cstr("Float32Array"), 1, &constructor_arg);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
xt_value buffer_get_channel_data(xt_value self, xt_value env, int32_t argc, xt_value *argv);
|
|
35
|
+
|
|
36
|
+
xt_value empty_channel() {
|
|
37
|
+
xt_value zero = xt_number(0);
|
|
38
|
+
return xt_typed_array_ctor_by_name(xt_string_from_cstr("Float32Array"), 1, &zero);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A channel's bytes: the typed array's backing buffer plus its view range. */
|
|
42
|
+
bool channel_bytes(xt_value channel, std::vector<uint8_t> *out, uint32_t *length) {
|
|
43
|
+
if (!xt_is_typed_array(channel)) return false;
|
|
44
|
+
xt_value buffer = xt_typed_array_backing_buffer(channel);
|
|
45
|
+
uint32_t offset = xt_typed_array_view_offset(channel);
|
|
46
|
+
uint32_t byte_length =
|
|
47
|
+
static_cast<uint32_t>(xt_to_number(xt_object_get_cstr(channel, "byteLength")));
|
|
48
|
+
if (!XT_IS_OBJECT(buffer) || byte_length < sizeof(float)) return false;
|
|
49
|
+
out->resize(byte_length);
|
|
50
|
+
if (xt_array_buffer_copy_bytes_out(buffer, offset, out->data(), byte_length) != byte_length) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
*length = byte_length / static_cast<uint32_t>(sizeof(float));
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
} // namespace
|
|
58
|
+
|
|
59
|
+
xt_value audio_buffer_create(const std::vector<std::vector<float>> &channels, int sample_rate) {
|
|
60
|
+
uint32_t length = channels.empty() ? 0 : static_cast<uint32_t>(channels[0].size());
|
|
61
|
+
xt_value channel_array = xt_array_new(0, nullptr);
|
|
62
|
+
for (const std::vector<float> &channel : channels) {
|
|
63
|
+
xt_array_push(channel_array, make_channel(channel.data(),
|
|
64
|
+
static_cast<uint32_t>(channel.size())));
|
|
65
|
+
}
|
|
66
|
+
xt_value buffer = xt_object_new();
|
|
67
|
+
xt_object_set(buffer, xt_string_from_cstr("__xt_audio_channels"), channel_array);
|
|
68
|
+
xt_object_set(buffer, xt_string_from_cstr("getChannelData"),
|
|
69
|
+
xt_closure_new(reinterpret_cast<void *>(buffer_get_channel_data), 0, nullptr));
|
|
70
|
+
xt_object_set(buffer, xt_string_from_cstr("numberOfChannels"),
|
|
71
|
+
xt_number(static_cast<double>(channels.size())));
|
|
72
|
+
xt_object_set(buffer, xt_string_from_cstr("length"), xt_number(static_cast<double>(length)));
|
|
73
|
+
xt_object_set(buffer, xt_string_from_cstr("sampleRate"),
|
|
74
|
+
xt_number(static_cast<double>(sample_rate)));
|
|
75
|
+
xt_object_set(buffer, xt_string_from_cstr("duration"),
|
|
76
|
+
xt_number(sample_rate > 0 ? static_cast<double>(length) / sample_rate : 0.0));
|
|
77
|
+
xt_object_set(buffer, xt_string_from_cstr("__xt_audio_ready"), XT_TRUE);
|
|
78
|
+
return buffer;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
std::shared_ptr<AudioClip> audio_clip_from_buffer(xt_value buffer) {
|
|
82
|
+
xt_value channels = xt_object_get_cstr(buffer, "__xt_audio_channels");
|
|
83
|
+
if (!XT_IS_ARRAY(channels) && !XT_IS_OBJECT(channels)) return nullptr;
|
|
84
|
+
int count = xt_array_size(channels);
|
|
85
|
+
if (count <= 0) return nullptr;
|
|
86
|
+
if (count > 2) count = 2; /* the mixer is stereo; extra channels are dropped */
|
|
87
|
+
auto clip = std::make_shared<AudioClip>();
|
|
88
|
+
clip->sample_rate =
|
|
89
|
+
static_cast<int>(xt_to_number(xt_object_get_cstr(buffer, "sampleRate")));
|
|
90
|
+
if (clip->sample_rate <= 0) clip->sample_rate = kAudioMixRate;
|
|
91
|
+
for (int index = 0; index < count; index++) {
|
|
92
|
+
xt_value channel = xt_array_get(channels, xt_number(static_cast<double>(index)));
|
|
93
|
+
std::vector<uint8_t> bytes;
|
|
94
|
+
uint32_t length = 0;
|
|
95
|
+
if (!channel_bytes(channel, &bytes, &length)) return nullptr;
|
|
96
|
+
std::vector<float> samples(length);
|
|
97
|
+
if (length > 0) std::memcpy(samples.data(), bytes.data(), bytes.size());
|
|
98
|
+
if (index == 0) {
|
|
99
|
+
clip->length = length;
|
|
100
|
+
} else if (length != clip->length) {
|
|
101
|
+
/* Keep every channel the same length; the mixer indexes all of them with
|
|
102
|
+
* the first channel's length. */
|
|
103
|
+
if (length < clip->length) clip->length = length;
|
|
104
|
+
for (std::vector<float> &existing : clip->channels) existing.resize(clip->length);
|
|
105
|
+
samples.resize(clip->length);
|
|
106
|
+
}
|
|
107
|
+
clip->channels.push_back(std::move(samples));
|
|
108
|
+
}
|
|
109
|
+
if (clip->length == 0) return nullptr;
|
|
110
|
+
return clip;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
bool audio_bytes_from_value(xt_value value, std::vector<uint8_t> *out) {
|
|
114
|
+
/* `fs.readFileSync` returns a byte string in this runtime (there is no
|
|
115
|
+
* dedicated binary value type). Its storage is the file's raw bytes, so a
|
|
116
|
+
* decoded audio file can be handed over directly. */
|
|
117
|
+
if (XT_IS_STRING(value)) {
|
|
118
|
+
int32_t bytes = xt_string_length_value(value);
|
|
119
|
+
const char *data = xt_string_data(value);
|
|
120
|
+
if (data == nullptr || bytes <= 0) return false;
|
|
121
|
+
out->assign(data, data + bytes);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
xt_value buffer = value;
|
|
125
|
+
uint32_t offset = 0;
|
|
126
|
+
uint32_t length = 0;
|
|
127
|
+
if (xt_is_typed_array(value)) {
|
|
128
|
+
/* A typed array view: decode only the view's bytes. */
|
|
129
|
+
buffer = xt_typed_array_backing_buffer(value);
|
|
130
|
+
offset = xt_typed_array_view_offset(value);
|
|
131
|
+
length = static_cast<uint32_t>(xt_to_number(xt_object_get_cstr(value, "byteLength")));
|
|
132
|
+
} else {
|
|
133
|
+
length = static_cast<uint32_t>(xt_to_number(xt_object_get_cstr(value, "byteLength")));
|
|
134
|
+
}
|
|
135
|
+
if (XT_IS_OBJECT(buffer) && length > 0) {
|
|
136
|
+
out->resize(length);
|
|
137
|
+
return xt_array_buffer_copy_bytes_out(buffer, offset, out->data(), length) == length;
|
|
138
|
+
}
|
|
139
|
+
/* A Node `Buffer` is a plain object with numeric elements and a `length`
|
|
140
|
+
* (xbintsc has no dedicated binary value type). Node programs routinely pass
|
|
141
|
+
* `readFileSync(...)` straight to `decodeAudioData`, so accept that shape. */
|
|
142
|
+
xt_value count = xt_object_get_cstr(value, "length");
|
|
143
|
+
if (!XT_IS_OBJECT(value) || !XT_IS_NUMBER(count) || xt_to_number(count) <= 0) return false;
|
|
144
|
+
uint32_t bytes = static_cast<uint32_t>(xt_to_number(count));
|
|
145
|
+
out->resize(bytes);
|
|
146
|
+
for (uint32_t index = 0; index < bytes; index++) {
|
|
147
|
+
(*out)[index] = static_cast<uint8_t>(
|
|
148
|
+
xt_to_number(xt_object_get(value, xt_number(static_cast<double>(index)))));
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
namespace {
|
|
154
|
+
|
|
155
|
+
xt_value buffer_get_channel_data(xt_value self, xt_value env, int32_t argc, xt_value *argv) {
|
|
156
|
+
(void)env;
|
|
157
|
+
xt_value channels = xt_object_get_cstr(self, "__xt_audio_channels");
|
|
158
|
+
int index = static_cast<int>(xt_to_number(xt_arg(argc, argv, 0)));
|
|
159
|
+
if (!XT_IS_ARRAY(channels) && !XT_IS_OBJECT(channels)) return empty_channel();
|
|
160
|
+
if (index < 0 || index >= xt_array_size(channels)) return empty_channel();
|
|
161
|
+
return xt_array_get(channels, xt_number(static_cast<double>(index)));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
} // namespace
|
|
165
|
+
|
|
166
|
+
} // namespace xtgui
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — the AudioBuffer bridge.
|
|
3
|
+
*
|
|
4
|
+
* An AudioBuffer is a JavaScript object whose channels are real
|
|
5
|
+
* `Float32Array`s, so a page can read and write samples with the normal typed
|
|
6
|
+
* array API. These helpers convert between that representation and the native
|
|
7
|
+
* `AudioClip` the mixer reads, and between an ArrayBuffer/typed-array argument
|
|
8
|
+
* and the bytes a decoder consumes.
|
|
9
|
+
*/
|
|
10
|
+
#ifndef XT_GUI_AUDIO_BUFFER_H
|
|
11
|
+
#define XT_GUI_AUDIO_BUFFER_H
|
|
12
|
+
|
|
13
|
+
#include <cstdint>
|
|
14
|
+
#include <memory>
|
|
15
|
+
#include <vector>
|
|
16
|
+
|
|
17
|
+
#include "rt.h"
|
|
18
|
+
|
|
19
|
+
namespace xtgui {
|
|
20
|
+
|
|
21
|
+
struct AudioClip;
|
|
22
|
+
|
|
23
|
+
/** Build an AudioBuffer value from planar float channels. The channel count and
|
|
24
|
+
* length come from the vectors; `sample_rate` is the buffer's own rate. */
|
|
25
|
+
xt_value audio_buffer_create(const std::vector<std::vector<float>> &channels, int sample_rate);
|
|
26
|
+
|
|
27
|
+
/** Copy an AudioBuffer value's channels into a mixer clip. Returns null when the
|
|
28
|
+
* value is not an AudioBuffer or has no samples. */
|
|
29
|
+
std::shared_ptr<AudioClip> audio_clip_from_buffer(xt_value buffer);
|
|
30
|
+
|
|
31
|
+
/** Read the bytes behind an ArrayBuffer or a typed array view (for
|
|
32
|
+
* `decodeAudioData`). */
|
|
33
|
+
bool audio_bytes_from_value(xt_value value, std::vector<uint8_t> *out);
|
|
34
|
+
|
|
35
|
+
} // namespace xtgui
|
|
36
|
+
|
|
37
|
+
#endif /* XT_GUI_AUDIO_BUFFER_H */
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — Web Audio byte decoding.
|
|
3
|
+
*
|
|
4
|
+
* `AudioContext.decodeAudioData` accepts an ArrayBuffer. WAV (PCM, IEEE float,
|
|
5
|
+
* A-law and µ-law) is decoded here with no dependency, which keeps the common
|
|
6
|
+
* "generate a WAV in TypeScript / play a .wav file" case working with `gui`
|
|
7
|
+
* alone. Every other container and codec is delegated to the optional `media`
|
|
8
|
+
* extension, whose FFmpeg build already carries MP3, AAC, Vorbis, Opus and
|
|
9
|
+
* FLAC (see `xt_media_decode_audio` in `media.h`).
|
|
10
|
+
*
|
|
11
|
+
* The output is interleaved, which is what the FFmpeg bridge naturally
|
|
12
|
+
* produces; `audio.cpp` deinterleaves it into the AudioBuffer's channels and
|
|
13
|
+
* the mixer's planar clip.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
#include "audio_decode.h"
|
|
17
|
+
|
|
18
|
+
#include <cmath>
|
|
19
|
+
#include <cstring>
|
|
20
|
+
|
|
21
|
+
#include "media.h"
|
|
22
|
+
|
|
23
|
+
namespace xtgui {
|
|
24
|
+
|
|
25
|
+
namespace {
|
|
26
|
+
|
|
27
|
+
uint16_t read_u16(const uint8_t *data) {
|
|
28
|
+
return static_cast<uint16_t>(data[0] | (data[1] << 8));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
uint32_t read_u32(const uint8_t *data) {
|
|
32
|
+
return static_cast<uint32_t>(data[0]) | (static_cast<uint32_t>(data[1]) << 8) |
|
|
33
|
+
(static_cast<uint32_t>(data[2]) << 16) | (static_cast<uint32_t>(data[3]) << 24);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
bool fail(std::string *error, const char *message) {
|
|
37
|
+
if (error != nullptr) *error = message;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** G.711 A-law, as stored in a WAV `ALAW` stream: after the 0x55 XOR the sign
|
|
42
|
+
* bit (0x80) is set for positive samples. */
|
|
43
|
+
double decode_alaw(uint8_t value) {
|
|
44
|
+
value ^= 0x55;
|
|
45
|
+
int sign = value & 0x80 ? 1 : -1;
|
|
46
|
+
int exponent = (value >> 4) & 0x07;
|
|
47
|
+
int mantissa = value & 0x0f;
|
|
48
|
+
int magnitude = exponent == 0 ? (mantissa << 4) + 8
|
|
49
|
+
: ((mantissa << 4) + 0x108) << (exponent - 1);
|
|
50
|
+
return sign * magnitude / 32768.0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** G.711 µ-law; the WAV variant is the complement of the raw bit stream. */
|
|
54
|
+
double decode_mulaw(uint8_t value) {
|
|
55
|
+
value = static_cast<uint8_t>(~value);
|
|
56
|
+
int sign = value & 0x80 ? -1 : 1;
|
|
57
|
+
int exponent = (value >> 4) & 0x07;
|
|
58
|
+
int mantissa = value & 0x0f;
|
|
59
|
+
int magnitude = (((mantissa << 3) + 0x84) << exponent) - 0x84;
|
|
60
|
+
return sign * magnitude / 32768.0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
struct WavFormat {
|
|
64
|
+
uint16_t tag = 0;
|
|
65
|
+
uint16_t channels = 0;
|
|
66
|
+
uint32_t sample_rate = 0;
|
|
67
|
+
uint16_t block_align = 0;
|
|
68
|
+
uint16_t bits = 0;
|
|
69
|
+
uint32_t data_offset = 0;
|
|
70
|
+
uint32_t data_size = 0;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
bool parse_wav(const uint8_t *data, size_t size, WavFormat *format, std::string *error) {
|
|
74
|
+
if (size < 12 || std::memcmp(data, "RIFF", 4) != 0 || std::memcmp(data + 8, "WAVE", 4) != 0) {
|
|
75
|
+
return fail(error, "not a WAV file");
|
|
76
|
+
}
|
|
77
|
+
size_t pos = 12;
|
|
78
|
+
bool have_format = false;
|
|
79
|
+
bool have_data = false;
|
|
80
|
+
while (pos + 8 <= size) {
|
|
81
|
+
const uint8_t *header = data + pos;
|
|
82
|
+
uint32_t chunk = read_u32(header + 4);
|
|
83
|
+
size_t body = pos + 8;
|
|
84
|
+
if (body > size) break;
|
|
85
|
+
if (std::memcmp(header, "fmt ", 4) == 0) {
|
|
86
|
+
if (chunk < 16 || body + 16 > size) return fail(error, "truncated WAV format chunk");
|
|
87
|
+
format->tag = read_u16(data + body);
|
|
88
|
+
format->channels = read_u16(data + body + 2);
|
|
89
|
+
format->sample_rate = read_u32(data + body + 4);
|
|
90
|
+
format->block_align = read_u16(data + body + 12);
|
|
91
|
+
format->bits = read_u16(data + body + 14);
|
|
92
|
+
/* WAVE_FORMAT_EXTENSIBLE: the real codec is the first two bytes of the
|
|
93
|
+
* SubFormat GUID, after cbSize/validBits/channelMask. */
|
|
94
|
+
if (format->tag == 0xfffe && chunk >= 40 && body + 26 <= size) {
|
|
95
|
+
format->tag = read_u16(data + body + 24);
|
|
96
|
+
}
|
|
97
|
+
have_format = true;
|
|
98
|
+
} else if (std::memcmp(header, "data", 4) == 0) {
|
|
99
|
+
if (body > size) break;
|
|
100
|
+
format->data_offset = static_cast<uint32_t>(body);
|
|
101
|
+
uint64_t available = size - body;
|
|
102
|
+
format->data_size = static_cast<uint32_t>(chunk < available ? chunk : available);
|
|
103
|
+
have_data = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
pos = body + chunk + (chunk & 1u);
|
|
107
|
+
}
|
|
108
|
+
if (!have_format) return fail(error, "WAV file has no format chunk");
|
|
109
|
+
if (!have_data) return fail(error, "WAV file has no data chunk");
|
|
110
|
+
if (format->channels == 0 || format->channels > 32) return fail(error, "invalid WAV channel count");
|
|
111
|
+
if (format->sample_rate < 8000 || format->sample_rate > 192000) {
|
|
112
|
+
return fail(error, "invalid WAV sample rate");
|
|
113
|
+
}
|
|
114
|
+
bool supported = false;
|
|
115
|
+
switch (format->tag) {
|
|
116
|
+
case 1: /* PCM */
|
|
117
|
+
supported = format->bits == 8 || format->bits == 16 || format->bits == 24 ||
|
|
118
|
+
format->bits == 32;
|
|
119
|
+
break;
|
|
120
|
+
case 3: /* IEEE float */
|
|
121
|
+
supported = format->bits == 32 || format->bits == 64;
|
|
122
|
+
break;
|
|
123
|
+
case 6: /* A-law */
|
|
124
|
+
case 7: /* µ-law */
|
|
125
|
+
supported = format->bits == 8;
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
if (!supported) return fail(error, "unsupported WAV codec");
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
double pcm_sample(const uint8_t *data, uint16_t bits) {
|
|
135
|
+
switch (bits) {
|
|
136
|
+
case 8:
|
|
137
|
+
return (static_cast<int>(data[0]) - 128) / 128.0;
|
|
138
|
+
case 16:
|
|
139
|
+
return static_cast<int16_t>(data[0] | (data[1] << 8)) / 32768.0;
|
|
140
|
+
case 24: {
|
|
141
|
+
int32_t value = data[0] | (data[1] << 8) | (data[2] << 16);
|
|
142
|
+
if (value & 0x800000) value |= ~0xffffff;
|
|
143
|
+
return value / 8388608.0;
|
|
144
|
+
}
|
|
145
|
+
case 32: {
|
|
146
|
+
int32_t value = static_cast<int32_t>(read_u32(data));
|
|
147
|
+
return value / 2147483648.0;
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
return 0.0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
bool decode_wav(const uint8_t *data, size_t size, DecodedAudio *out, std::string *error) {
|
|
155
|
+
WavFormat format;
|
|
156
|
+
if (!parse_wav(data, size, &format, error)) return false;
|
|
157
|
+
uint32_t frame_bytes;
|
|
158
|
+
switch (format.tag) {
|
|
159
|
+
case 1:
|
|
160
|
+
frame_bytes = format.channels * (format.bits / 8u);
|
|
161
|
+
break;
|
|
162
|
+
case 3:
|
|
163
|
+
frame_bytes = format.channels * (format.bits / 8u);
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
frame_bytes = format.channels;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (format.block_align > frame_bytes) frame_bytes = format.block_align;
|
|
170
|
+
if (frame_bytes == 0) return fail(error, "invalid WAV frame size");
|
|
171
|
+
uint32_t frames = format.data_size / frame_bytes;
|
|
172
|
+
if (frames == 0) return fail(error, "WAV file has no samples");
|
|
173
|
+
|
|
174
|
+
out->sample_rate = static_cast<int>(format.sample_rate);
|
|
175
|
+
out->channels = format.channels;
|
|
176
|
+
out->frames = frames;
|
|
177
|
+
out->interleaved.assign(static_cast<size_t>(frames) * format.channels, 0.0f);
|
|
178
|
+
for (uint32_t frame = 0; frame < frames; frame++) {
|
|
179
|
+
const uint8_t *source = data + format.data_offset + static_cast<size_t>(frame) * frame_bytes;
|
|
180
|
+
for (uint16_t channel = 0; channel < format.channels; channel++) {
|
|
181
|
+
const uint8_t *sample = source + channel * (format.bits / 8u);
|
|
182
|
+
double value;
|
|
183
|
+
if (format.tag == 3) {
|
|
184
|
+
if (format.bits == 32) {
|
|
185
|
+
float narrowed;
|
|
186
|
+
std::memcpy(&narrowed, sample, sizeof(narrowed));
|
|
187
|
+
value = narrowed;
|
|
188
|
+
} else {
|
|
189
|
+
double wide;
|
|
190
|
+
std::memcpy(&wide, sample, sizeof(wide));
|
|
191
|
+
value = wide;
|
|
192
|
+
}
|
|
193
|
+
} else if (format.tag == 6) {
|
|
194
|
+
value = decode_alaw(sample[0]);
|
|
195
|
+
} else if (format.tag == 7) {
|
|
196
|
+
value = decode_mulaw(sample[0]);
|
|
197
|
+
} else {
|
|
198
|
+
value = pcm_sample(sample, format.bits);
|
|
199
|
+
}
|
|
200
|
+
out->interleaved[static_cast<size_t>(frame) * format.channels + channel] =
|
|
201
|
+
static_cast<float>(value);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
} // namespace
|
|
208
|
+
|
|
209
|
+
bool decode_audio_bytes(const uint8_t *data, size_t size, DecodedAudio *out, std::string *error) {
|
|
210
|
+
if (data == nullptr || size == 0) return fail(error, "empty audio data");
|
|
211
|
+
std::string wav_error;
|
|
212
|
+
if (decode_wav(data, size, out, &wav_error)) return true;
|
|
213
|
+
std::string media_error;
|
|
214
|
+
if (xt_media_decode_audio(data, size, out, &media_error)) return true;
|
|
215
|
+
if (error != nullptr) *error = media_error.empty() ? wav_error : media_error;
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
} // namespace xtgui
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — Web Audio byte decoding.
|
|
3
|
+
*
|
|
4
|
+
* `decodeAudioBytes` is what `AudioContext.decodeAudioData` calls. WAV is
|
|
5
|
+
* decoded by the engine itself; every other container/codec is delegated to
|
|
6
|
+
* the optional `media` extension, so `gui` alone still has a working decoder
|
|
7
|
+
* for the format a page usually generates.
|
|
8
|
+
*/
|
|
9
|
+
#ifndef XT_GUI_AUDIO_DECODE_H
|
|
10
|
+
#define XT_GUI_AUDIO_DECODE_H
|
|
11
|
+
|
|
12
|
+
#include <cstddef>
|
|
13
|
+
#include <cstdint>
|
|
14
|
+
#include <string>
|
|
15
|
+
#include <vector>
|
|
16
|
+
|
|
17
|
+
namespace xtgui {
|
|
18
|
+
|
|
19
|
+
/** Decoded PCM, interleaved, `frames * channels` floats. */
|
|
20
|
+
struct DecodedAudio {
|
|
21
|
+
int channels = 0;
|
|
22
|
+
int sample_rate = 0;
|
|
23
|
+
uint32_t frames = 0;
|
|
24
|
+
std::vector<float> interleaved;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Decode `size` bytes of `data`.
|
|
29
|
+
*
|
|
30
|
+
* WAV is handled in-engine (PCM 8/16/24/32, IEEE float 32/64, A-law, µ-law).
|
|
31
|
+
* Anything else asks the `media` extension's FFmpeg decoder; without that
|
|
32
|
+
* extension the call fails with an actionable message. Returns false and fills
|
|
33
|
+
* `*error` on failure. `data` is not retained.
|
|
34
|
+
*/
|
|
35
|
+
bool decode_audio_bytes(const uint8_t *data, size_t size, DecodedAudio *out, std::string *error);
|
|
36
|
+
|
|
37
|
+
} // namespace xtgui
|
|
38
|
+
|
|
39
|
+
#endif /* XT_GUI_AUDIO_DECODE_H */
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* xbintsc GUI engine — the SDL playback device behind an AudioContext.
|
|
3
|
+
*
|
|
4
|
+
* The mixer renders on demand (`AudioMixer::render`); this unit is the only
|
|
5
|
+
* place that opens a device and pumps it. SDL calls `stream_callback` on its
|
|
6
|
+
* audio thread, which renders the requested amount into the stream. A headless
|
|
7
|
+
* host simply has no device: `start_device` fails once and playback stays
|
|
8
|
+
* silent, while the graph and its clock remain usable (the offline render hook
|
|
9
|
+
* exercises the same code path).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
#include "audio_mixer.h"
|
|
13
|
+
|
|
14
|
+
#include <SDL3/SDL.h>
|
|
15
|
+
|
|
16
|
+
#include <vector>
|
|
17
|
+
|
|
18
|
+
namespace xtgui {
|
|
19
|
+
|
|
20
|
+
namespace {
|
|
21
|
+
|
|
22
|
+
void SDLCALL stream_callback(void *userdata, SDL_AudioStream *stream, int additional_amount,
|
|
23
|
+
int total_amount) {
|
|
24
|
+
(void)total_amount;
|
|
25
|
+
if (additional_amount <= 0) return;
|
|
26
|
+
int frames = additional_amount / static_cast<int>(sizeof(float) * kAudioMixChannels);
|
|
27
|
+
if (frames <= 0) return;
|
|
28
|
+
std::vector<float> buffer(static_cast<size_t>(frames) * kAudioMixChannels);
|
|
29
|
+
static_cast<AudioMixer *>(userdata)->render(buffer.data(), frames);
|
|
30
|
+
SDL_PutAudioStreamData(stream, buffer.data(), static_cast<int>(buffer.size() * sizeof(float)));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
} // namespace
|
|
34
|
+
|
|
35
|
+
bool AudioMixer::start_device() {
|
|
36
|
+
if (offline_) return false;
|
|
37
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
38
|
+
if (!running_) return false;
|
|
39
|
+
if (stream_ != nullptr) return true;
|
|
40
|
+
if (device_tried_) return false;
|
|
41
|
+
device_tried_ = true;
|
|
42
|
+
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) return false;
|
|
43
|
+
SDL_AudioSpec spec;
|
|
44
|
+
spec.format = SDL_AUDIO_F32;
|
|
45
|
+
spec.channels = kAudioMixChannels;
|
|
46
|
+
spec.freq = kAudioMixRate;
|
|
47
|
+
stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, stream_callback,
|
|
48
|
+
this);
|
|
49
|
+
if (stream_ == nullptr) return false;
|
|
50
|
+
SDL_ResumeAudioStreamDevice(stream_);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void AudioMixer::suspend_device() {
|
|
55
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
56
|
+
running_ = false;
|
|
57
|
+
if (stream_ != nullptr) SDL_PauseAudioStreamDevice(stream_);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
void AudioMixer::resume_device() {
|
|
61
|
+
{
|
|
62
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
63
|
+
running_ = true;
|
|
64
|
+
}
|
|
65
|
+
if (start_device()) {
|
|
66
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
67
|
+
if (stream_ != nullptr) SDL_ResumeAudioStreamDevice(stream_);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
void AudioMixer::close_device() {
|
|
72
|
+
SDL_AudioStream *stream = nullptr;
|
|
73
|
+
{
|
|
74
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
75
|
+
stream = stream_;
|
|
76
|
+
stream_ = nullptr;
|
|
77
|
+
running_ = false;
|
|
78
|
+
}
|
|
79
|
+
if (stream != nullptr) SDL_DestroyAudioStream(stream);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
bool AudioMixer::has_device() {
|
|
83
|
+
std::lock_guard<std::mutex> lock(mutex_);
|
|
84
|
+
return stream_ != nullptr;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
} // namespace xtgui
|