@audio/shift-delay 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/index.js +64 -0
- package/package.json +40 -0
package/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { bufferedStream, makePitchShift, resolveRatio, sincRead } from '@audio/shift-core'
|
|
2
|
+
|
|
3
|
+
// Canonical delay-line (harmonizer) pitch shift — the method of hardware harmonizers
|
|
4
|
+
// (Eventide H910 lineage, Lexicon "rotating tape head"). Two read taps sweep a delay
|
|
5
|
+
// window at rate `ratio`; each tap's lag ramps sawtooth-wise across the window and the
|
|
6
|
+
// taps alternate through a Hann crossfade (half-cycle offset, amplitudes summing to
|
|
7
|
+
// unity) so a tap only wraps while silent. Each wrap splices at the lag offset that
|
|
8
|
+
// maximally correlates with the live tap ("intelligent splicing") — an unaligned splice
|
|
9
|
+
// phase-slips the carrier and smears the spectral peak; an aligned one keeps the taps
|
|
10
|
+
// coherent, which is also why the plain (not equal-power) crossfade is the right law.
|
|
11
|
+
// Real-time capable with `window` samples of latency by design; the residual artifact
|
|
12
|
+
// is mild flutter at the crossfade rate on wideband material. `window` (samples,
|
|
13
|
+
// default 2048) trades flutter rate against transient smear; `tolerance` (default
|
|
14
|
+
// window/4) bounds the splice search.
|
|
15
|
+
function delayBatch(data, opts) {
|
|
16
|
+
let { ratio, ratioFn } = resolveRatio(opts)
|
|
17
|
+
let n = data.length
|
|
18
|
+
let W = opts?.window ?? 2048
|
|
19
|
+
let L = opts?.tolerance ?? (W >> 2)
|
|
20
|
+
let sr = opts?.sampleRate || 44100
|
|
21
|
+
let out = new Float32Array(n)
|
|
22
|
+
if (!n) return out
|
|
23
|
+
let cutoff = 1
|
|
24
|
+
let pos = [0, 0]
|
|
25
|
+
let phase = [0, 0.5]
|
|
26
|
+
// Best-correlated splice offset: align the wrapping tap's future read with the live
|
|
27
|
+
// tap's — plain dot product over K samples, once per wrap.
|
|
28
|
+
let splice = (target, live) => {
|
|
29
|
+
let K = Math.min(256, n)
|
|
30
|
+
let best = 0, bestC = -Infinity
|
|
31
|
+
for (let o = -L; o <= L; o += 2) {
|
|
32
|
+
let c = 0
|
|
33
|
+
for (let j = 0; j < K; j += 2) {
|
|
34
|
+
let a = live + j, b = target + o + j
|
|
35
|
+
if (b < 0 || b >= n || a >= n) continue
|
|
36
|
+
c += data[a | 0] * data[b | 0]
|
|
37
|
+
}
|
|
38
|
+
if (c > bestC) { bestC = c; best = o }
|
|
39
|
+
}
|
|
40
|
+
return target + best
|
|
41
|
+
}
|
|
42
|
+
for (let i = 0; i < n; i++) {
|
|
43
|
+
let r = ratioFn ? ratioFn(i / sr) : ratio
|
|
44
|
+
if (!(r > 0) || !Number.isFinite(r)) r = ratio
|
|
45
|
+
cutoff = r > 1 ? 1 / r : 1
|
|
46
|
+
for (let t = 0; t < 2; t++) {
|
|
47
|
+
// Tap wraps (re-centers on the write head) exactly at its crossfade null.
|
|
48
|
+
if (phase[t] >= 1) { phase[t] -= 1; pos[t] = splice(i, pos[1 - t]) }
|
|
49
|
+
// Plain Hann crossfade: splicing phase-aligns the taps, so amplitudes — not
|
|
50
|
+
// powers — must sum to unity (equal-power would overshoot correlated content).
|
|
51
|
+
let w = 0.5 - 0.5 * Math.cos(2 * Math.PI * phase[t])
|
|
52
|
+
if (w > 1e-6) out[i] += w * sincRead(data, pos[t], 8, cutoff)
|
|
53
|
+
pos[t] += r
|
|
54
|
+
// Lag drifts by (r - 1) per sample; a full window of drift is one saw cycle.
|
|
55
|
+
phase[t] += Math.abs(r - 1) / W
|
|
56
|
+
}
|
|
57
|
+
// Identity-adjacent ratios never wrap; taps stay put and the Hann pair sums to 1.
|
|
58
|
+
}
|
|
59
|
+
return out
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let delayStream = (opts) => bufferedStream(delayBatch, opts)
|
|
63
|
+
|
|
64
|
+
export default makePitchShift(delayBatch, delayStream)
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@audio/shift-delay",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Delay-line (harmonizer) pitch shift — dual crossfading taps sweeping a modulated delay window",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./index.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@audio/shift-core": "^1.0.0"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "audiojs",
|
|
23
|
+
"homepage": "https://github.com/audiojs/shift/tree/main/packages/shift-delay#readme",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/audiojs/shift.git",
|
|
27
|
+
"directory": "packages/shift-delay"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"audio",
|
|
31
|
+
"pitch-shift",
|
|
32
|
+
"harmonizer",
|
|
33
|
+
"delay-line",
|
|
34
|
+
"doppler",
|
|
35
|
+
"dsp"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|