@audio/effect-graindelay 1.0.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/README.md +5 -0
- package/graindelay.js +55 -0
- package/package.json +29 -0
package/README.md
ADDED
package/graindelay.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Granular delay — grains read from the delay line with per-grain delay scatter and
|
|
3
|
+
* pitch transposition (Ableton Grain Delay class). Two staggered Hann-windowed heads
|
|
4
|
+
* retrigger every half grain; feedback writes the wet grain stream back into the line.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
let { sin, cos, PI } = Math
|
|
8
|
+
|
|
9
|
+
export default function graindelay (data, params) {
|
|
10
|
+
let time = params.time ?? 0.25 // base delay, s
|
|
11
|
+
let spray = params.spray ?? 0.02 // random extra delay per grain, s
|
|
12
|
+
let pitch = params.pitch ?? 0 // semitones, per-grain transposition
|
|
13
|
+
let jitter = params.jitter ?? 0 // random pitch scatter, semitones
|
|
14
|
+
let grain = params.grain ?? 0.08 // grain length, s
|
|
15
|
+
let feedback = params.feedback ?? 0.3
|
|
16
|
+
let mix = params.mix ?? 0.5
|
|
17
|
+
let fs = params.fs || 44100
|
|
18
|
+
|
|
19
|
+
let gN = Math.max(32, (grain * fs) | 0)
|
|
20
|
+
let size = Math.ceil((time + spray + grain) * fs * 2 ** Math.max(0, (pitch + jitter) / 12)) + gN + 2
|
|
21
|
+
if (!params._buf || params._buf.length < size) {
|
|
22
|
+
params._buf = new Float64Array(size)
|
|
23
|
+
params._w = 0
|
|
24
|
+
params._rnd = params.seed ?? 0x9e3779b9
|
|
25
|
+
// two heads, staggered by half a grain
|
|
26
|
+
params._heads = [{ n: 0 }, { n: -(gN >> 1) }].map(h => ({ ...h, start: 0, off: time * fs, rate: 1 }))
|
|
27
|
+
}
|
|
28
|
+
let buf = params._buf, w = params._w, heads = params._heads
|
|
29
|
+
let N = buf.length
|
|
30
|
+
let rnd = () => ((params._rnd = (params._rnd * 1664525 + 1013904223) >>> 0) / 4294967296)
|
|
31
|
+
|
|
32
|
+
for (let i = 0, l = data.length; i < l; i++) {
|
|
33
|
+
let x = data[i]
|
|
34
|
+
let wet = 0
|
|
35
|
+
for (let h of heads) {
|
|
36
|
+
if (h.n < 0) { h.n++; continue } // stagger warmup
|
|
37
|
+
if (h.n === 0) { // retrigger: new scatter + rate
|
|
38
|
+
h.start = w
|
|
39
|
+
h.off = (time + spray * rnd()) * fs
|
|
40
|
+
h.rate = 2 ** ((pitch + jitter * (rnd() * 2 - 1)) / 12)
|
|
41
|
+
}
|
|
42
|
+
let pos = h.start - h.off + h.n * h.rate
|
|
43
|
+
let base = Math.floor(pos), frac = pos - base
|
|
44
|
+
let a = buf[((base % N) + N) % N], b = buf[(((base + 1) % N) + N) % N]
|
|
45
|
+
let win = 0.5 - 0.5 * cos(2 * PI * h.n / gN)
|
|
46
|
+
wet += (a + frac * (b - a)) * win
|
|
47
|
+
if (++h.n >= gN) h.n = 0 // grain done → retrigger next sample
|
|
48
|
+
}
|
|
49
|
+
buf[w] = x + feedback * wet
|
|
50
|
+
w = (w + 1) % N
|
|
51
|
+
data[i] = x * (1 - mix) + wet * mix
|
|
52
|
+
}
|
|
53
|
+
params._w = w
|
|
54
|
+
return data
|
|
55
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/effect-graindelay",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Granular delay — per-grain pitch/time scatter (Ableton Grain Delay class)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"keywords": [
|
|
8
|
+
"audio",
|
|
9
|
+
"dsp",
|
|
10
|
+
"effect",
|
|
11
|
+
"graindelay"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Dmitry Iv. <dfcreative@gmail.com>",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"main": "graindelay.js",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./graindelay.js",
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"graindelay.js"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|