node-web-audio-api 0.21.4 → 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## v0.21.5 (23/12/2024)
4
+
5
+ - Fix: Use module import for `AudioWorklet#addModule`
6
+ - Feat: Resolve `AudioWorkletNode` when installed in `node_modules`
7
+ - Ensure support of `AudioWorkletNode` that use Web Assembly
8
+
3
9
  ## v0.21.4 (16/12/2024)
4
10
 
5
11
  - Update upstream crate to [v1.1.0](https://github.com/orottier/web-audio-api-rs/blob/main/CHANGELOG.md#version-110-2024-12-11)
package/Cross.toml ADDED
@@ -0,0 +1,17 @@
1
+ [target.aarch64-unknown-linux-gnu]
2
+ pre-build = [
3
+ "dpkg --add-architecture $CROSS_DEB_ARCH",
4
+ "apt-get update && apt-get --assume-yes install libasound2-dev:$CROSS_DEB_ARCH libjack-jackd2-dev:$CROSS_DEB_ARCH"
5
+ ]
6
+
7
+ [target.armv7-unknown-linux-gnueabihf]
8
+ pre-build = [
9
+ "dpkg --add-architecture $CROSS_DEB_ARCH",
10
+ "apt-get update && apt-get --assume-yes install libasound2-dev:$CROSS_DEB_ARCH libjack-jackd2-dev:$CROSS_DEB_ARCH"
11
+ ]
12
+
13
+ [target.x86_64-unknown-linux-gnu]
14
+ pre-build = [
15
+ "dpkg --add-architecture $CROSS_DEB_ARCH",
16
+ "apt-get update && apt-get --assume-yes install libasound2-dev:$CROSS_DEB_ARCH libjack-jackd2-dev:$CROSS_DEB_ARCH"
17
+ ]
package/all-checks.sh CHANGED
@@ -7,9 +7,9 @@ echo "-----------------------------------------------"
7
7
  cargo fmt -- --check --color always
8
8
 
9
9
  echo "-----------------------------------------------"
10
- echo "> cargo clippy --all-targets --features cpal -- -D warnings"
10
+ echo "> cargo clippy --all-targets -- -D warnings"
11
11
  echo "-----------------------------------------------"
12
- cargo clippy --all-targets --features cpal -- -D warnings
12
+ cargo clippy --all-targets -- -D warnings
13
13
 
14
14
  echo "-----------------------------------------------"
15
15
  echo "> Run js tests"
@@ -5,6 +5,7 @@ const {
5
5
  kOnUpdate,
6
6
  } = require('./lib/symbols.js');
7
7
  const {
8
+ isFunction,
8
9
  kEnumerableProperty,
9
10
  } = require('./lib/utils.js');
10
11
  const {
@@ -70,7 +71,7 @@ class AudioRenderCapacity extends EventTarget {
70
71
  }
71
72
 
72
73
  targetOptions.updateInterval = conversions['double'](options.updateInterval, {
73
- context: `Failed to execute 'start' on 'AudioRenderCapacity': Failed to read the 'updateInterval' property on 'AudioRenderCapacityOptions': The provided value ()`
74
+ context: `Failed to execute 'start' on 'AudioRenderCapacity': Failed to read the 'updateInterval' property on 'AudioRenderCapacityOptions': The provided value ()`,
74
75
  });
75
76
  } else {
76
77
  targetOptions.updateInterval = 1;
@@ -108,7 +109,7 @@ Object.defineProperties(AudioRenderCapacity.prototype, {
108
109
  },
109
110
 
110
111
  onupdate: kEnumerableProperty,
111
- stop: kEnumerableProperty,
112
+ start: kEnumerableProperty,
112
113
  stop: kEnumerableProperty,
113
114
  });
114
115
 
@@ -1,8 +1,9 @@
1
1
  const {
2
- resolveObjectURL
2
+ resolveObjectURL,
3
3
  } = require('node:buffer');
4
- const fs = require('node:fs').promises;
5
- const { existsSync } = require('node:fs');
4
+ const {
5
+ existsSync,
6
+ } = require('node:fs');
6
7
  const path = require('node:path');
7
8
  const {
8
9
  Worker,
@@ -23,85 +24,76 @@ const {
23
24
 
24
25
  const caller = require('caller');
25
26
  // cf. https://www.npmjs.com/package/node-fetch#commonjs
26
- const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
27
+ const fetch = (...args) => import('node-fetch').then(({ default: fetch }) => fetch(...args));
27
28
 
28
29
  /**
29
30
  * Retrieve code with different module resolution strategies
30
31
  * - file - absolute or relative to cwd path
31
- * - URL
32
- * - Blob
32
+ *
33
+ * - URL - do not support import within module
34
+ * - Blob - do not support import within module
33
35
  * - fallback: relative to caller site
34
- * + in fs
36
+ * + in fs - support import within module
35
37
  * + caller site is url - required for wpt, probably no other use case
36
38
  */
37
39
  const resolveModule = async (moduleUrl) => {
38
- let code;
40
+ let code = null;
41
+ let absPathname = null;
39
42
 
40
43
  if (existsSync(moduleUrl)) {
41
- const pathname = moduleUrl;
42
-
43
- try {
44
- const buffer = await fs.readFile(pathname);
45
- code = buffer.toString();
46
- } catch (err) {
47
- throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
44
+ if (path.isAbsolute(moduleUrl)) {
45
+ absPathname = moduleUrl;
46
+ } else { // moduleUrl is relative to process.cwd();
47
+ absPathname = path.join(process.cwd(), moduleUrl);
48
48
  }
49
49
  } else if (moduleUrl.startsWith('http')) {
50
50
  try {
51
- const res = await fetch(moduleUrl);
52
- code = await res.text();
53
- } catch (err) {
54
- throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
55
- }
51
+ const res = await fetch(moduleUrl);
52
+ code = await res.text();
53
+ } catch (err) {
54
+ throw new DOMException(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`, 'AbortError');
55
+ }
56
56
  } else if (moduleUrl.startsWith('blob:')) {
57
57
  try {
58
58
  const blob = resolveObjectURL(moduleUrl);
59
59
  code = await blob.text();
60
60
  } catch (err) {
61
- throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
61
+ throw new DOMException(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`, 'AbortError');
62
62
  }
63
63
  } else {
64
- // get caller site from error stack trace
65
64
  const callerSite = caller(2);
66
65
 
67
66
  if (callerSite.startsWith('http')) { // this branch exists for wpt where caller site is an url
68
- let url;
69
- // handle origin relative and caller path relative URLs
70
- if (moduleUrl.startsWith('/')) {
71
- const origin = new URL(baseUrl).origin;
72
- url = origin + moduleUrl;
73
- } else {
74
- // we know separators are '/'
75
- const baseUrl = callerSite.substr(0, callerSite.lastIndexOf('/'));
76
- url = baseUrl + '/' + moduleUrl;
77
- }
67
+ const baseUrl = callerSite.substring(0, callerSite.lastIndexOf('/'));
68
+ const url = baseUrl + '/' + moduleUrl;
78
69
 
79
70
  try {
80
71
  const res = await fetch(url);
81
72
  code = await res.text();
82
73
  } catch (err) {
83
- throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
74
+ throw new DOMException(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`, 'AbortError');
84
75
  }
85
76
  } else {
86
- const dirname = callerSite.substr(0, callerSite.lastIndexOf(path.sep));
77
+ // filesystem, relative to caller site or in node_modules
78
+ const dirname = callerSite.substring(0, callerSite.lastIndexOf(path.sep));
87
79
  const absDirname = dirname.replace('file://', '');
88
80
  const pathname = path.join(absDirname, moduleUrl);
89
81
 
90
- if (existsSync(pathname)) {
82
+ if (existsSync(pathname)) { // relative to caller site
83
+ absPathname = pathname;
84
+ } else {
91
85
  try {
92
- const buffer = await fs.readFile(pathname);
93
- code = buffer.toString();
86
+ // try resolve according to process.cwd()
87
+ absPathname = require.resolve(moduleUrl, { paths: [process.cwd()] });
94
88
  } catch (err) {
95
- throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': ${err.message}`);
89
+ throw new DOMException(`Failed to execute 'addModule' on 'AudioWorklet': Cannot resolve module ${moduleUrl}`, 'AbortError');
96
90
  }
97
- } else {
98
- throw new Error(`Failed to execute 'addModule' on 'AudioWorklet': Cannot resolve module ${moduleUrl}`);
99
91
  }
100
92
  }
101
93
  }
102
94
 
103
- return code;
104
- }
95
+ return { absPathname, code };
96
+ };
105
97
 
106
98
  class AudioWorklet {
107
99
  #workletId = null;
@@ -125,6 +117,9 @@ class AudioWorklet {
125
117
  }
126
118
 
127
119
  #bindEvents() {
120
+ // @todo
121
+ // - better error handling, stack trace, etc.
122
+ // - handle 'node-web-audio-api:worklet:ctor-error' message
128
123
  this.#port.on('message', event => {
129
124
  switch (event.cmd) {
130
125
  case 'node-web-audio-api:worklet:module-added': {
@@ -135,10 +130,9 @@ class AudioWorklet {
135
130
  break;
136
131
  }
137
132
  case 'node-web-audio-api:worklet:add-module-failed': {
138
- const { promiseId, ctor, name, message } = event;
133
+ const { promiseId, err } = event;
139
134
  const { reject } = this.#idPromiseMap.get(promiseId);
140
135
  this.#idPromiseMap.delete(promiseId);
141
- const err = new globalThis[ctor](message, name);
142
136
  reject(err);
143
137
  break;
144
138
  }
@@ -161,7 +155,9 @@ class AudioWorklet {
161
155
  }
162
156
 
163
157
  async addModule(moduleUrl) {
164
- const code = await resolveModule(moduleUrl);
158
+ // @important - `resolveModule` must be called first because it uses `caller`
159
+ // which will return `null` if this is not in the first line...
160
+ const resolved = await resolveModule(moduleUrl);
165
161
 
166
162
  // launch Worker if not exists
167
163
  if (!this.#port) {
@@ -187,7 +183,8 @@ class AudioWorklet {
187
183
 
188
184
  this.#port.postMessage({
189
185
  cmd: 'node-web-audio-api:worklet:add-module',
190
- code,
186
+ moduleUrl: resolved.absPathname,
187
+ code: resolved.code,
191
188
  promiseId,
192
189
  });
193
190
  });
@@ -196,6 +193,7 @@ class AudioWorklet {
196
193
  // For OfflineAudioContext only, check that all processors have been properly
197
194
  // created before actual `startRendering`
198
195
  async [kCheckProcessorsCreated]() {
196
+ // eslint-disable-next-line no-async-promise-executor
199
197
  return new Promise(async resolve => {
200
198
  while (this.#pendingCreateProcessors.size !== 0) {
201
199
  // we need a microtask to ensure message can be received
@@ -87,7 +87,7 @@ globalThis[kWorkletRecycleBuffer1] = buffer => pool1.recycle(buffer);
87
87
  globalThis[kWorkletMarkAsUntransferable] = obj => {
88
88
  markAsUntransferable(obj);
89
89
  return obj;
90
- }
90
+ };
91
91
 
92
92
  function isIterable(obj) {
93
93
  // checks for null and undefined
@@ -114,7 +114,7 @@ function runLoop() {
114
114
  runLoopImmediateId = setImmediate(runLoop);
115
115
  }
116
116
 
117
- globalThis.currentTime = 0
117
+ globalThis.currentTime = 0;
118
118
  globalThis.currentFrame = 0;
119
119
  globalThis.sampleRate = sampleRate;
120
120
  // @todo - implement in upstream crate
@@ -153,7 +153,7 @@ globalThis.AudioWorkletProcessor = class AudioWorkletProcessor {
153
153
  this[kWorkletParamsCache][desc.name] = [
154
154
  pool128.get(), // should be globalThis.renderQuantumSize
155
155
  pool1.get(),
156
- ]
156
+ ];
157
157
  });
158
158
 
159
159
  this.#port = port;
@@ -170,7 +170,7 @@ globalThis.AudioWorkletProcessor = class AudioWorkletProcessor {
170
170
  [kWorkletQueueTask](cmd, err) {
171
171
  this.#port.postMessage({ cmd, err });
172
172
  }
173
- }
173
+ };
174
174
 
175
175
  // follow algorithm from:
176
176
  // https://webaudio.github.io/web-audio-api/#dom-audioworkletglobalscope-registerprocessor
@@ -298,12 +298,8 @@ globalThis.registerProcessor = function registerProcessor(name, processorCtor) {
298
298
  // process.stdout.write('closing worklet');
299
299
  // });
300
300
 
301
- parentPort.on('message', event => {
301
+ parentPort.on('message', async event => {
302
302
  switch (event.cmd) {
303
- case 'node-web-audio-api:worklet:init': {
304
- const { workletId, processors, promiseId } = event;
305
- break;
306
- }
307
303
  case 'node-web-audio-api:worklet:exit': {
308
304
  clearImmediate(runLoopImmediateId);
309
305
  // properly exit audio worklet on rust side
@@ -313,11 +309,18 @@ parentPort.on('message', event => {
313
309
  break;
314
310
  }
315
311
  case 'node-web-audio-api:worklet:add-module': {
316
- const { code, promiseId } = event;
317
- const func = new Function('AudioWorkletProcessor', 'registerProcessor', code);
312
+ const { moduleUrl, code, promiseId } = event;
318
313
 
319
314
  try {
320
- func(AudioWorkletProcessor, registerProcessor);
315
+ // 1. If given module is a "real" file, we can import it as is,
316
+ // 2. If module is a blob or loaded from an URL, we use the raw text as
317
+ // input. In this case, if the module uses `import` it will crash
318
+ if (moduleUrl !== null) {
319
+ await import(moduleUrl);
320
+ } else {
321
+ await import(`data:text/javascript;base64,${btoa(unescape(encodeURIComponent(code)))}`);
322
+ }
323
+
321
324
  // send registered param descriptors on main thread and resolve Promise
322
325
  parentPort.postMessage({
323
326
  cmd: 'node-web-audio-api:worklet:module-added',
@@ -327,9 +330,7 @@ parentPort.on('message', event => {
327
330
  parentPort.postMessage({
328
331
  cmd: 'node-web-audio-api:worklet:add-module-failed',
329
332
  promiseId,
330
- ctor: err.constructor.name,
331
- name: err.name,
332
- message: err.message,
333
+ err,
333
334
  });
334
335
  }
335
336
  break;
@@ -338,7 +339,7 @@ parentPort.on('message', event => {
338
339
  const { name, id, options, port } = event;
339
340
  const ctor = nameProcessorCtorMap.get(name);
340
341
 
341
- // rewrap options of interest for the AudioWorkletNodeBaseClass
342
+ // re-wrap options of interest for the AudioWorkletNodeBaseClass
342
343
  pendingProcessorConstructionData = {
343
344
  port,
344
345
  numberOfInputs: options.numberOfInputs,
@@ -352,6 +353,7 @@ parentPort.on('message', event => {
352
353
  instance = new ctor(options);
353
354
  } catch (err) {
354
355
  port.postMessage({ cmd: 'node-web-audio-api:worklet:ctor-error', err });
356
+ return;
355
357
  }
356
358
 
357
359
  pendingProcessorConstructionData = null;
@@ -156,7 +156,7 @@ module.exports = (jsExport, nativeBinding) => {
156
156
  // if we delegate this check to Rust, this can poison a Mutex
157
157
  // (probably the `audio_param_descriptor_channel` one)
158
158
  if (parsedOptions.channelCount <= 0 || parsedOptions.channelCount > IMPLEMENTATION_MAX_NUMBER_OF_CHANNELS) {
159
- throw new DOMException(`Failed to construct 'AudioWorkletNode': Invalid 'channelCount' property: Number of channels: ${parsedOptions.channelCount} is outside range [1, 32]`, 'NotSupportedError')
159
+ throw new DOMException(`Failed to construct 'AudioWorkletNode': Invalid 'channelCount' property: Number of channels: ${parsedOptions.channelCount} is outside range [1, 32]`, 'NotSupportedError');
160
160
  }
161
161
  }
162
162
 
@@ -222,7 +222,8 @@ module.exports = (jsExport, nativeBinding) => {
222
222
  );
223
223
 
224
224
  this.#port.on('message', msg => {
225
- // ErrorEvent named processorerror
225
+ // Handle 'processorerror' ErrorEvent
226
+ // cf. https://webaudio.github.io/web-audio-api/#dom-audioworkletnode-onprocessorerror
226
227
  switch (msg.cmd) {
227
228
  case 'node-web-audio-api:worklet:ctor-error': {
228
229
  const message = `Failed to construct '${parsedName}' AudioWorkletProcessor: ${msg.err.message}`;
@@ -138,10 +138,12 @@ module.exports = function patchOfflineAudioContext(jsExport, nativeBinding) {
138
138
  // ensure all AudioWorkletProcessor have finished their instanciation
139
139
  await this.audioWorklet[kCheckProcessorsCreated]();
140
140
 
141
- let nativeAudioBuffer;
141
+ // keep this to highlight the workaround w/ the oncomplete event
142
+ let _nativeAudioBuffer;
142
143
 
143
144
  try {
144
- nativeAudioBuffer = await this[kNapiObj].startRendering();
145
+ // eslint-disable-next-line no-unused-vars
146
+ _nativeAudioBuffer = await this[kNapiObj].startRendering();
145
147
  } catch (err) {
146
148
  throwSanitizedError(err);
147
149
  }
package/load-native.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ const fs = require('node:fs');
1
2
  const { platform, arch } = process;
2
3
 
3
4
  let nativeBinding = null;
@@ -21,7 +22,7 @@ switch (platform) {
21
22
  }
22
23
  break;
23
24
  default:
24
- throw new Error(`Unsupported architecture on Windows: ${arch}`);
25
+ loadError = new Error(`Unsupported architecture on Windows: ${arch}`);
25
26
  }
26
27
  break;
27
28
  case 'darwin':
@@ -41,11 +42,15 @@ switch (platform) {
41
42
  }
42
43
  break;
43
44
  default:
44
- throw new Error(`Unsupported architecture on macOS: ${arch}`);
45
+ loadError = new Error(`Unsupported architecture on macOS: ${arch}`);
45
46
  }
46
47
  break;
48
+ // case 'freebsd': x64 only
47
49
  case 'linux':
48
50
  switch (arch) {
51
+ // @todo
52
+ // - support riscv64 arch
53
+ // - support musl C lib
49
54
  case 'x64':
50
55
  try {
51
56
  nativeBinding = require('./node-web-audio-api.linux-x64-gnu.node');
@@ -68,11 +73,20 @@ switch (platform) {
68
73
  }
69
74
  break;
70
75
  default:
71
- throw new Error(`Unsupported architecture on Linux: ${arch}`);
76
+ loadError = new Error(`Unsupported architecture on Linux: ${arch}`);
72
77
  }
73
78
  break;
74
79
  default:
75
- throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`);
80
+ loadError = new Error(`Unsupported OS: ${platform}, architecture: ${arch}`);
81
+ }
82
+
83
+ // use local build if exists
84
+ if (fs.existsSync('node-web-audio-api.build-release.node')) {
85
+ nativeBinding = require('./node-web-audio-api.build-release.node');
86
+ }
87
+
88
+ if (fs.existsSync('node-web-audio-api.build-debug.node')) {
89
+ nativeBinding = require('./node-web-audio-api.build-debug.node');
76
90
  }
77
91
 
78
92
  if (!nativeBinding) {
Binary file
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "node-web-audio-api",
3
- "version": "0.21.4",
3
+ "version": "1.0.0-beta.0",
4
4
  "author": "Benjamin Matuszewski",
5
- "description": "Node.js bindings for web-audio-api-rs using napi-rs",
5
+ "description": "Web Audio API implementation for Node.js",
6
6
  "exports": {
7
7
  "import": "./index.mjs",
8
8
  "require": "./index.cjs",
@@ -21,7 +21,7 @@
21
21
  "music",
22
22
  "dsp",
23
23
  "rust",
24
- "n-api"
24
+ "node-api"
25
25
  ],
26
26
  "engines": {
27
27
  "node": ">= 14"
@@ -35,17 +35,17 @@
35
35
  "access": "public"
36
36
  },
37
37
  "scripts": {
38
- "artifacts": "napi artifacts",
39
- "build": "npm run generate && napi build --platform --release",
40
- "build:jack": "npm run generate && napi build --platform --features jack --release",
41
- "build:debug": "npm run generate && napi build --platform",
42
- "build:only": "napi build --platform --release",
38
+ "build": "npm run generate && cargo build --release && node ./.scripts/move-artifact.mjs --release",
39
+ "build:jack": "npm run generate && cargo build --features jack --release && node ./.scripts/move-artifact.mjs --release",
40
+ "build:debug": "npm run generate && cargo build && node ./.scripts/move-artifact.mjs",
41
+ "build:only": "cargo build --release && node ./.scripts/move-artifact.mjs --release",
43
42
  "check": "cargo fmt && cargo clippy",
44
43
  "generate": "node generator/index.mjs && cargo fmt",
45
44
  "lint": "npx eslint index.cjs index.mjs && npx eslint js/*.js && npx eslint examples/*.mjs",
46
- "preversion": "yarn install && npm run generate",
45
+ "preversion": "npm install && npm run generate",
47
46
  "postversion": "cargo bump $npm_package_version && git commit -am \"v$npm_package_version\" && node .scripts/check-changelog.mjs",
48
47
  "test": "mocha tests/*.spec.mjs",
48
+ "test:ci": "mocha tests/*.spec.mjs -- --ci",
49
49
  "test:only": "mocha",
50
50
  "wpt": "npm run build && node ./.scripts/wpt-harness.mjs",
51
51
  "wpt:only": "node ./.scripts/wpt-harness.mjs"
@@ -73,8 +73,6 @@
73
73
  "wpt-runner": "^5.0.0"
74
74
  },
75
75
  "dependencies": {
76
- "@napi-rs/cli": "^2.14.3",
77
- "@node-rs/helper": "^1.3.3",
78
76
  "caller": "^1.1.0",
79
77
  "node-fetch": "^3.3.2",
80
78
  "webidl-conversions": "^7.0.0"