@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.
Files changed (2) hide show
  1. package/index.js +64 -0
  2. 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
+ }