@grame/faustwasm 0.18.1 → 0.18.2

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.
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The hand-over on a stolen voice fades in as well as out.
3
+ *
4
+ * `renderStolenVoices` plays the outgoing note over the first half of the
5
+ * block and fades that half out through the mixer. The new note then plays
6
+ * the second half from the DSP's live state -- `keyOn` does not reset a
7
+ * voice, and an envelope that never reached silence is still up -- so that
8
+ * half has to be faded in too, or every stolen note starts with a step.
9
+ */
10
+ import { test } from 'node:test';
11
+ import assert from 'node:assert/strict';
12
+ import { FaustPolyWebAudioDsp } from '../../dist/esm/index.js';
13
+
14
+ const BLOCK = 128;
15
+
16
+ /**
17
+ * A polyphonic DSP whose single voice renders a constant 1.
18
+ *
19
+ * The mixer is inert -- `fadeOut` is the wasm side's business -- so what
20
+ * ends up in the mixing buffer is the voice's output shaped only by the
21
+ * JavaScript side of the hand-over.
22
+ */
23
+ function polyDsp(block = BLOCK) {
24
+ const memory = new WebAssembly.Memory({ initial: 1 });
25
+ const meta = {
26
+ name: 'probe',
27
+ inputs: 0,
28
+ outputs: 1,
29
+ size: 8,
30
+ ui: [
31
+ {
32
+ type: 'vgroup',
33
+ label: 'probe',
34
+ items: [
35
+ { type: 'hslider', label: 'freq', address: '/probe/freq', index: 0, init: 440, min: 20, max: 2000, step: 1 },
36
+ { type: 'hslider', label: 'gain', address: '/probe/gain', index: 4, init: 0.5, min: 0, max: 1, step: 0.01 },
37
+ { type: 'button', label: 'gate', address: '/probe/gate', index: 8 }
38
+ ]
39
+ }
40
+ ]
41
+ };
42
+ const voiceAPI = {
43
+ init: () => {},
44
+ instanceInit: () => {},
45
+ instanceClear: () => {},
46
+ instanceConstants: () => {},
47
+ instanceResetUserInterface: () => {},
48
+ setParamValue: () => {},
49
+ getParamValue: () => 0,
50
+ compute: (dsp, count, $inputs, $outputs) => {
51
+ const HEAP32 = new Int32Array(memory.buffer);
52
+ const HEAPF = new Float32Array(memory.buffer);
53
+ const start = HEAP32[$outputs >> 2] >> 2;
54
+ for (let i = 0; i < count; i++) HEAPF[start + i] = 1;
55
+ },
56
+ getNumInputs: () => 0,
57
+ getNumOutputs: () => 1
58
+ };
59
+ const instance = {
60
+ memory,
61
+ voices: 1,
62
+ voiceAPI,
63
+ effectAPI: undefined,
64
+ effectJSON: undefined,
65
+ mixerAPI: {
66
+ clearOutput: () => {},
67
+ mixCheckVoice: () => 1,
68
+ fadeOut: () => {}
69
+ },
70
+ voiceJSON: JSON.stringify(meta)
71
+ };
72
+ const dsp = new FaustPolyWebAudioDsp(instance, 48000, 4, block, []);
73
+ return { dsp, memory };
74
+ }
75
+
76
+ /** The mixing buffer of channel 0 after a block. */
77
+ function mixing(dsp, memory, block = BLOCK) {
78
+ const HEAP32 = new Int32Array(memory.buffer);
79
+ const HEAPF = new Float32Array(memory.buffer);
80
+ const start = HEAP32[dsp.fAudioMixing >> 2] >> 2;
81
+ return Array.from(HEAPF.subarray(start, start + block));
82
+ }
83
+
84
+ test('the second half of a stolen block fades in from 1/count', () => {
85
+ const { dsp, memory } = polyDsp();
86
+ dsp.start();
87
+ const out = [new Float32Array(BLOCK)];
88
+ dsp.keyOn(0, 60, 100);
89
+ dsp.compute([], out);
90
+ // The pool has one voice, so the next note steals it.
91
+ dsp.keyOn(0, 67, 100);
92
+ dsp.compute([], out);
93
+
94
+ const half = BLOCK >> 1;
95
+ const buf = mixing(dsp, memory);
96
+ // First half: the outgoing note, left to the mixer's fadeOut (inert here).
97
+ assert.ok(buf.slice(0, half).every((v) => v === 1), 'first half untouched by the JS side');
98
+ // Second half: a ramp from 1/64 to 1, the mirror of fadeOut.
99
+ const ramp = buf.slice(half);
100
+ assert.equal(ramp.length, BLOCK - half);
101
+ assert.ok(Math.abs(ramp[0] - 1 / ramp.length) < 1e-6, `starts at 1/count, got ${ramp[0]}`);
102
+ assert.equal(ramp[ramp.length - 1], 1, 'ends at 1');
103
+ for (let i = 1; i < ramp.length; i++) {
104
+ assert.ok(ramp[i] > ramp[i - 1], `rises at frame ${i}`);
105
+ }
106
+ });
107
+
108
+ test('an odd block gives the extra frame to the second half, and the ramp covers it', () => {
109
+ const block = 127;
110
+ const { dsp, memory } = polyDsp(block);
111
+ dsp.start();
112
+ const out = [new Float32Array(block)];
113
+ dsp.keyOn(0, 60, 100);
114
+ dsp.compute([], out);
115
+ dsp.keyOn(0, 67, 100);
116
+ dsp.compute([], out);
117
+
118
+ // computeLegato splits at count >> 1 = 63 and renders the remaining 64
119
+ // frames as the second half; the fade-in is measured from the split.
120
+ const half = block >> 1;
121
+ const buf = mixing(dsp, memory, block);
122
+ assert.ok(buf.slice(0, half).every((v) => v === 1), 'first half untouched by the JS side');
123
+ const ramp = buf.slice(half);
124
+ assert.equal(ramp.length, block - half);
125
+ assert.ok(Math.abs(ramp[0] - 1 / ramp.length) < 1e-6, `starts at 1/count, got ${ramp[0]}`);
126
+ assert.equal(ramp[ramp.length - 1], 1, 'ends at 1 on the last frame of the block');
127
+ for (let i = 1; i < ramp.length; i++) {
128
+ assert.ok(ramp[i] > ramp[i - 1], `rises at frame ${i}`);
129
+ }
130
+ });
131
+
132
+ test('a note on a free voice is not faded', () => {
133
+ const { dsp, memory } = polyDsp();
134
+ dsp.start();
135
+ const out = [new Float32Array(BLOCK)];
136
+ dsp.keyOn(0, 60, 100);
137
+ dsp.compute([], out);
138
+ assert.ok(mixing(dsp, memory).every((v) => v === 1), 'no ramp on a fresh voice');
139
+ });