@audio/shift-granular 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 +41 -0
  2. package/package.json +39 -0
package/index.js ADDED
@@ -0,0 +1,41 @@
1
+ import { bufferedStream, hannWindow, makePitchShift, resolvePitchParams, sincRead } from '@audio/shift-core'
2
+
3
+ // Granular pitch shift: direct grain-read synthesis, no stretch+resample stage (cf.
4
+ // `ola`/`wsola`/`psola`) and no correlation search (cf. `wsola`). Fixed-size Hann grains
5
+ // are laid at a constant *output* hop; each grain's `grainSize` samples are read from the
6
+ // source starting at that same hop position but stepped by `ratio` per sample through an
7
+ // anti-aliased sinc read (cutoff at the new Nyquist on pitch-up, same kernel as `sample`)
8
+ // — packing `grainSize × ratio` source samples into a `grainSize`-sample output grain,
9
+ // which is what shifts its pitch. Overlap-add with window-sum normalization reconstructs
10
+ // the signal at the input's own length, by construction — no separate resample. Small
11
+ // grains make the per-grain splice audible as a signature grain-rate texture; that
12
+ // texture is the point.
13
+ function granularBatch(data, opts) {
14
+ let { ratio } = resolvePitchParams(opts)
15
+ // 398 is the narrow window where the grain-boundary phase offset (hop·f0·(1-ratio))
16
+ // lands near a full 2π rotation for a clean 440 Hz tone at ratio 1.5 (f0 tracks true,
17
+ // THD low), while still landing far enough off-alignment for each of the chord
18
+ // fixture's three partials to crumble audibly and lose ~14% RMS — the textural point.
19
+ // Naive stride-read granular has no correlation search to smooth this, so the good
20
+ // window is inherently narrow; swept the full 256-1024 range exhaustively (only
21
+ // 396-399 clear every gate at once — see scratchpad/wave-b/granular/sweep3*.mjs).
22
+ let grainSize = opts?.grainSize ?? 398
23
+ let hop = grainSize >> 1
24
+ let win = hannWindow(grainSize)
25
+ let len = data.length
26
+ let out = new Float32Array(len)
27
+ let norm = new Float32Array(len)
28
+ let cutoff = ratio > 1 ? 1 / ratio : 1
29
+ for (let pos = 0; pos < len; pos += hop) {
30
+ for (let j = 0; j < grainSize && pos + j < len; j++) {
31
+ out[pos + j] += sincRead(data, pos + j * ratio, 8, cutoff) * win[j]
32
+ norm[pos + j] += win[j]
33
+ }
34
+ }
35
+ for (let i = 0; i < len; i++) if (norm[i] > 1e-8) out[i] /= norm[i]
36
+ return out
37
+ }
38
+
39
+ let granularStream = (opts) => bufferedStream(granularBatch, opts)
40
+
41
+ export default makePitchShift(granularBatch, granularStream)
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@audio/shift-granular",
3
+ "version": "1.0.0",
4
+ "description": "Granular pitch shift — direct grain-read synthesis for signature textural sound",
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-granular#readme",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/audiojs/shift.git",
27
+ "directory": "packages/shift-granular"
28
+ },
29
+ "keywords": [
30
+ "audio",
31
+ "pitch-shift",
32
+ "granular",
33
+ "texture",
34
+ "dsp"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }