@grame/faustwasm 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -281,6 +281,33 @@ process = ba.pulsen(1, 10000) : pm.djembe(60, 0.3, 0.4, 1);
281
281
  })();
282
282
  ```
283
283
 
284
+ ### Sample-accurate scheduling
285
+
286
+ Every control method on a Faust AudioWorklet node takes an optional `time` argument as its last parameter: `setParamValue(path, value, time)`, `keyOn(channel, pitch, velocity, time)`, `keyOff(channel, pitch, velocity, time)`, `midiMessage(data, time)`, `ctrlChange(channel, ctrl, value, time)`, `pitchWheel(channel, wheel, time)`. `time` is in AudioContext seconds — the same clock as `audioCtx.currentTime` and `AudioParam.setValueAtTime` — and the action lands on the exact sample that instant names, inside the audio block that contains it. The unit is the second (a JavaScript double), the granularity is the sample: the processor converts it with `Math.round(time * sampleRate)` to a frame on the audio clock, so an instant is honoured to the nearest sample (about 21 µs at 48 kHz):
287
+
288
+ ```JavaScript
289
+ const t0 = audioCtx.currentTime + 0.1;
290
+
291
+ // A parameter change on the exact sample
292
+ node.setParamValue("/mydsp/gate", 1, t0);
293
+ node.setParamValue("/mydsp/gate", 0, t0 + 0.05);
294
+
295
+ // A polyphonic pattern scheduled ahead of time
296
+ node.keyOn(0, 60, 100, t0);
297
+ node.keyOff(0, 60, 0, t0 + 0.5);
298
+ node.keyOn(0, 63, 100, t0 + 0.25);
299
+ ```
300
+
301
+ Regular `AudioParam` automation on the node's parameters (`setValueAtTime`, `linearRampToValueAtTime`, ...) is followed sample-accurately as well, so a note and a parameter ramp can be scheduled against one another. Timestamps carried by Web Audio Modules (WAM) events are honoured the same way.
302
+
303
+ Left out, `time` behaves as before: the message is applied on arrival, at the start of the next audio block. A `time` already in the past is applied as soon as possible, in order. Notes:
304
+
305
+ - the FFT processor honours `time` at block granularity rather than sample granularity;
306
+ - ScriptProcessor nodes (`sp: true`) accept the argument and apply the message on arrival;
307
+ - `setParamValue` with a negative `time` throws (it is refused by `AudioParam.setValueAtTime`).
308
+
309
+ The page `test/web/poly-schedule.html` demonstrates the API, and `test/timing/measure.mjs` measures the sample accuracy (see below).
310
+
284
311
  ### Running the tests
285
312
 
286
313
  Unit tests live in `test/unit` and run in plain Node (no browser needed). This builds the ESM bundle first, then runs every `*.test.mjs` file with Node's built-in test runner:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grame/faustwasm",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "WebAssembly version of Faust Compiler",
5
5
  "main": "dist/cjs/index.js",
6
6
  "types": "dist/esm/index.d.ts",
@@ -0,0 +1,97 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Sample-accurate scheduling test</title>
6
+ <script src="../../dist/cjs-bundle/index.js"></script>
7
+ </head>
8
+ <body style="font-family: sans-serif; margin: 1.5rem">
9
+ <h1>Sample-accurate scheduling</h1>
10
+ <p>
11
+ Compares the same arpeggio played twice: first scheduled with
12
+ <code>setTimeout</code> (the pre-timestamp way, jitter of up to a
13
+ block plus whatever the main thread is doing), then with
14
+ <code>keyOn(channel, pitch, velocity, time)</code> against the
15
+ AudioContext clock (each note lands on its exact sample). A gate
16
+ retrigger inside a single block, inaudible before, closes the demo.
17
+ </p>
18
+ <button id="start">Start</button>
19
+ <pre id="log" style="background: #f6f6f6; padding: 1rem; border-radius: 4px"></pre>
20
+ </body>
21
+ <script>
22
+ const polycode = `
23
+ import("stdfaust.lib");
24
+ process = ba.pulsen(1, ba.hz2midikey(freq) * 1000) : pm.marimba(freq, 0, 7000, 0.5, 0.8) * gate * gain with {
25
+ freq = hslider("freq", 440, 40, 8000, 1);
26
+ gain = hslider("gain", 0.5, 0, 1, 0.01);
27
+ gate = button("gate");
28
+ };
29
+ effect = dm.freeverb_demo;`;
30
+
31
+ const PATTERN = [60, 64, 67, 71, 72, 71, 67, 64];
32
+ const STEP = 0.125; // seconds between notes
33
+ const log = (msg) => {
34
+ document.getElementById("log").textContent += msg + "\n";
35
+ console.log(msg);
36
+ };
37
+
38
+ // The old way: the main thread fires each note when its timer
39
+ // happens to run.
40
+ const playWithTimeouts = (node) => {
41
+ log("Pattern 1: setTimeout scheduling (jitter audible on a busy page)");
42
+ PATTERN.forEach((pitch, i) => {
43
+ setTimeout(() => {
44
+ node.keyOn(0, pitch, 100);
45
+ setTimeout(() => node.keyOff(0, pitch, 0), STEP * 900);
46
+ }, i * STEP * 1000);
47
+ });
48
+ };
49
+
50
+ // The new way: everything is scheduled up front against the
51
+ // AudioContext clock, and each note lands on its exact sample.
52
+ const playWithTimestamps = (node, audioCtx) => {
53
+ log("Pattern 2: keyOn(..., time) scheduling (sample-accurate)");
54
+ const t0 = audioCtx.currentTime + 0.1;
55
+ PATTERN.forEach((pitch, i) => {
56
+ node.keyOn(0, pitch, 100, t0 + i * STEP);
57
+ node.keyOff(0, pitch, 0, t0 + (i + 0.9) * STEP);
58
+ });
59
+ return t0 + PATTERN.length * STEP;
60
+ };
61
+
62
+ // A 1 -> 0 -> 1 gate retrigger inside one 128-frame block: both
63
+ // edges land, so the note audibly restarts. Before timestamps the
64
+ // whole excursion was lost.
65
+ const retriggerInsideOneBlock = (node, audioCtx) => {
66
+ const gatePath = node.getParams().find((p) => p.endsWith("/gate"));
67
+ log(`Retrigger: ${gatePath} 1 -> 0 -> 1 within a single block`);
68
+ const t0 = audioCtx.currentTime + 0.1;
69
+ const frame = 1 / audioCtx.sampleRate;
70
+ node.keyOn(0, 55, 100, t0);
71
+ node.keyOff(0, 55, 0, t0 + 1.0);
72
+ node.setParamValue(gatePath, 0, t0 + 0.5);
73
+ node.setParamValue(gatePath, 1, t0 + 0.5 + 32 * frame);
74
+ };
75
+
76
+ const { instantiateFaustModule, LibFaust, FaustCompiler, FaustPolyDspGenerator } = faustwasm;
77
+ document.getElementById("start").onclick = async () => {
78
+ const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
79
+ await audioCtx.resume();
80
+ const faustModule = await instantiateFaustModule();
81
+ const libFaust = new LibFaust(faustModule);
82
+ const compiler = new FaustCompiler(libFaust);
83
+ const generator = new FaustPolyDspGenerator();
84
+ await generator.compile(compiler, "dsp", polycode, "");
85
+ const node = await generator.createNode(audioCtx, 4);
86
+ node.connect(audioCtx.destination);
87
+ log("Node ready");
88
+
89
+ playWithTimeouts(node);
90
+ setTimeout(() => {
91
+ const end = playWithTimestamps(node, audioCtx);
92
+ const msLeft = (end - audioCtx.currentTime) * 1000 + 500;
93
+ setTimeout(() => retriggerInsideOneBlock(node, audioCtx), msLeft);
94
+ }, PATTERN.length * STEP * 1000 + 500);
95
+ };
96
+ </script>
97
+ </html>