@hraness/dawg 0.5.0 → 0.6.1

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 (124) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/DAWG.md +592 -49
  3. package/README.md +4 -4
  4. package/core/chords.ts +492 -4
  5. package/core/diff.ts +9 -0
  6. package/core/expression.ts +143 -6
  7. package/core/fx.ts +689 -2
  8. package/core/granular.ts +619 -0
  9. package/core/instruments.ts +442 -0
  10. package/core/keys.ts +955 -0
  11. package/core/resonators.ts +862 -0
  12. package/core/score.ts +529 -8
  13. package/core/sdk/eval-child.ts +2 -0
  14. package/core/sdk/print.ts +247 -16
  15. package/core/sdk/sync-instruments.ts +58 -0
  16. package/core/sdk/v1.ts +2417 -40
  17. package/core/sections.ts +44 -10
  18. package/core/strings.ts +1080 -0
  19. package/core/winds.ts +652 -0
  20. package/guides/automation.md +1 -0
  21. package/guides/chords.md +3 -1
  22. package/guides/effects.md +5 -2
  23. package/guides/media.md +1 -1
  24. package/guides/performance.md +3 -0
  25. package/guides/resample.md +26 -0
  26. package/guides/sounds.md +7 -2
  27. package/guides/tempo.md +1 -0
  28. package/package.json +1 -1
  29. package/src/agent/agent.ts +13 -0
  30. package/src/agent/chord-tools.ts +153 -1
  31. package/src/agent/expression-tools.ts +91 -0
  32. package/src/agent/granular-tools.ts +138 -0
  33. package/src/agent/models.ts +4 -4
  34. package/src/agent/ops.ts +42 -2
  35. package/src/agent/preview-tool.ts +7 -1
  36. package/src/agent/resample-tool.ts +131 -0
  37. package/src/agent/tools.ts +726 -17
  38. package/src/agent/xcb-agent.ts +13 -0
  39. package/src/audio/arrange.ts +42 -3
  40. package/src/audio/dsp/bank.ts +233 -0
  41. package/src/audio/dsp/envelope.ts +161 -0
  42. package/src/audio/dsp/fft.ts +6 -0
  43. package/src/audio/dsp/filters.ts +57 -0
  44. package/src/audio/dsp/interp.ts +112 -0
  45. package/src/audio/dsp/modal.ts +684 -0
  46. package/src/audio/dsp/onset.ts +197 -0
  47. package/src/audio/dsp/oversample.ts +202 -0
  48. package/src/audio/dsp/rng.ts +37 -0
  49. package/src/audio/dsp/shape.ts +81 -0
  50. package/src/audio/dsp/shift.ts +256 -0
  51. package/src/audio/dsp/stft.ts +73 -0
  52. package/src/audio/dsp/window.ts +77 -0
  53. package/src/audio/effects/chain.ts +23 -3
  54. package/src/audio/effects/common.ts +14 -0
  55. package/src/audio/effects/convolution.ts +106 -1
  56. package/src/audio/effects/gaze.ts +354 -0
  57. package/src/audio/effects/rig/cab.ts +99 -0
  58. package/src/audio/effects/rig/filters.ts +152 -0
  59. package/src/audio/effects/rig/gate.ts +39 -0
  60. package/src/audio/effects/rig/head.ts +487 -0
  61. package/src/audio/effects/rig/index.ts +170 -0
  62. package/src/audio/effects/rig/section.ts +78 -0
  63. package/src/audio/effects/rig/stomp.ts +238 -0
  64. package/src/audio/engine.ts +56 -6
  65. package/src/audio/fit.ts +447 -0
  66. package/src/audio/granular.ts +995 -0
  67. package/src/audio/instrument-check.ts +137 -0
  68. package/src/audio/instruments.ts +140 -0
  69. package/src/audio/keys/dsp.ts +323 -0
  70. package/src/audio/keys/electric.ts +420 -0
  71. package/src/audio/keys/engine.ts +485 -0
  72. package/src/audio/keys/organ.ts +1335 -0
  73. package/src/audio/keys/piano.ts +476 -0
  74. package/src/audio/keys/sympathetic.ts +127 -0
  75. package/src/audio/live-worker.ts +56 -0
  76. package/src/audio/live.ts +361 -15
  77. package/src/audio/loudness.ts +65 -20
  78. package/src/audio/preview.ts +17 -1
  79. package/src/audio/resample.ts +285 -0
  80. package/src/audio/resonators.ts +295 -0
  81. package/src/audio/sampler.ts +328 -35
  82. package/src/audio/samples.ts +56 -8
  83. package/src/audio/strings/body.ts +263 -0
  84. package/src/audio/strings/bow.ts +656 -0
  85. package/src/audio/strings/engine.ts +615 -0
  86. package/src/audio/strings/loop.ts +119 -0
  87. package/src/audio/strings/measure.test-helpers.ts +200 -0
  88. package/src/audio/strings/pluck.ts +354 -0
  89. package/src/audio/warp.ts +61 -0
  90. package/src/audio/wav.ts +191 -19
  91. package/src/audio/winds/engine.ts +302 -0
  92. package/src/audio/winds/filters.ts +153 -0
  93. package/src/audio/winds/pitch.ts +104 -0
  94. package/src/audio/winds/trim.ts +80 -0
  95. package/src/audio/winds/trims.ts +917 -0
  96. package/src/audio/winds/voice.ts +479 -0
  97. package/src/commands/expression.ts +111 -23
  98. package/src/commands/fit.ts +135 -0
  99. package/src/commands/fx.ts +35 -1
  100. package/src/commands/granular.ts +432 -0
  101. package/src/commands/help.ts +159 -5
  102. package/src/commands/keys.ts +629 -0
  103. package/src/commands/modal.ts +310 -0
  104. package/src/commands/resample.ts +281 -0
  105. package/src/commands/rig.ts +264 -0
  106. package/src/commands/sample.ts +52 -2
  107. package/src/commands/shift.ts +119 -0
  108. package/src/commands/string.ts +201 -0
  109. package/src/commands/strum.ts +473 -0
  110. package/src/commands/time.ts +4 -1
  111. package/src/commands/wind.ts +244 -0
  112. package/src/main.ts +339 -15
  113. package/src/project/check.ts +17 -0
  114. package/src/render.ts +49 -4
  115. package/src/session/presence.ts +16 -3
  116. package/src/tui/audition.ts +2 -2
  117. package/src/tui/granular-menu.ts +278 -0
  118. package/src/tui/menu.ts +934 -9
  119. package/src/tui/modal-menu.ts +145 -0
  120. package/src/tui/performance-menu.ts +42 -0
  121. package/src/tui/play-chords.ts +104 -3
  122. package/src/tui/play-mode.ts +1 -0
  123. package/src/tui/play-session.ts +70 -2
  124. package/src/tui/wind-menu.ts +144 -0
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Onset detection: SuperFlux (Böck and Widmer, "Maximum filter vibrato
3
+ * suppression for onset detection", DAFx 2013): positive log-magnitude flux
4
+ * of each frame against a 3-bin maximum-filtered previous frame, peak-picked
5
+ * against a local median plus the global mean, then refined to the start of
6
+ * the energy rise. Deterministic; positions are sample frames.
7
+ *
8
+ * A hit at t = 0 has no previous frame to rise from, so it is reported when
9
+ * the signal starts loud (first 5 ms above 10 % of the peak).
10
+ */
11
+ import { analyse, frameCount, hann, nextPow2 } from "./stft.ts";
12
+
13
+ /** Hits closer than this merge into the first (a flam). */
14
+ export const ONSET_MIN_GAP_SECONDS = 0.03;
15
+ /** A refined onset sits this far before the energy rise. */
16
+ export const ONSET_LEAD_SECONDS = 0.001;
17
+
18
+ export function detectOnsets(
19
+ x: ArrayLike<number>,
20
+ sampleRate: number,
21
+ sensitivity = 1,
22
+ ): number[] {
23
+ const n = nextPow2(0.021 * sampleRate);
24
+ const hop = n / 4;
25
+ const half = n / 2;
26
+ const w = hann(n);
27
+ const frames = frameCount(x.length, n, hop);
28
+ const flux = new Float64Array(frames);
29
+ let prev = new Float64Array(half + 1);
30
+ let cur = new Float64Array(half + 1);
31
+ const re = new Float64Array(n);
32
+ const im = new Float64Array(n);
33
+ for (let m = 0; m < frames; m += 1) {
34
+ analyse(x, m * hop, w, re, im);
35
+ let f = 0;
36
+ for (let k = 0; k <= half; k += 1) {
37
+ cur[k] = Math.log1p(100 * Math.hypot(re[k]!, im[k]!));
38
+ const lo = k > 0 ? prev[k - 1]! : prev[k]!;
39
+ const hi = k < half ? prev[k + 1]! : prev[k]!;
40
+ const d = cur[k]! - Math.max(lo, prev[k]!, hi);
41
+ if (d > 0) f += d;
42
+ }
43
+ flux[m] = m === 0 ? 0 : f;
44
+ const swap = prev;
45
+ prev = cur;
46
+ cur = swap;
47
+ }
48
+ let mean = 0;
49
+ for (let m = 0; m < frames; m += 1) mean += flux[m]!;
50
+ mean /= Math.max(1, frames);
51
+ const onsets: number[] = [];
52
+ const minGap = Math.round(ONSET_MIN_GAP_SECONDS * sampleRate);
53
+ const radius = 8;
54
+ const local: number[] = [];
55
+ const startsLoud = loudStart(x, sampleRate);
56
+ if (startsLoud) onsets.push(0);
57
+ for (let m = 1; m < frames - 1; m += 1) {
58
+ const v = flux[m]!;
59
+ let isPeak = true;
60
+ for (let j = Math.max(0, m - 3); j <= Math.min(frames - 1, m + 3); j += 1)
61
+ if (flux[j]! > v || (flux[j] === v && j < m)) {
62
+ isPeak = false;
63
+ break;
64
+ }
65
+ if (!isPeak) continue;
66
+ local.length = 0;
67
+ for (
68
+ let j = Math.max(0, m - radius);
69
+ j <= Math.min(frames - 1, m + radius);
70
+ j += 1
71
+ )
72
+ local.push(flux[j]!);
73
+ local.sort((a, b) => a - b);
74
+ const median = local[local.length >> 1]!;
75
+ if (v < (median * 1.5 + mean * 0.5) / sensitivity) continue;
76
+ const at = refineOnset(x, sampleRate, m * hop + half);
77
+ if (onsets.length > 0 && at - onsets[onsets.length - 1]! < minGap) continue;
78
+ onsets.push(at);
79
+ }
80
+ return onsets;
81
+ }
82
+
83
+ function loudStart(x: ArrayLike<number>, sampleRate: number): boolean {
84
+ let peak = 0;
85
+ for (let i = 0; i < x.length; i += 1) peak = Math.max(peak, Math.abs(x[i]!));
86
+ if (peak === 0) return false;
87
+ const head = Math.min(x.length, Math.round(0.005 * sampleRate));
88
+ for (let i = 0; i < head; i += 1)
89
+ if (Math.abs(x[i]!) > 0.1 * peak) return true;
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * The start of the energy rise near `coarse`, less 1 ms. Within -30..+15 ms
95
+ * of the coarse frame, 1 ms envelope blocks of the signal and of its first
96
+ * difference (which favours the clicks of a transient over the low ring of a
97
+ * previous kick) are compared with the loudest block of the 10 ms before
98
+ * them; the block that rises most over that background wins, and the onset
99
+ * is its first sample above the background plus a quarter of the rise. A
100
+ * hit in silence is unchanged; one in the previous hit's ring-out no longer
101
+ * latches onto the tail.
102
+ */
103
+ function refineOnset(
104
+ x: ArrayLike<number>,
105
+ sampleRate: number,
106
+ coarse: number,
107
+ ): number {
108
+ const from = Math.max(1, Math.round(coarse - 0.03 * sampleRate));
109
+ const to = Math.min(x.length, Math.round(coarse + 0.015 * sampleRate));
110
+ if (to <= from) return coarse;
111
+ const block = Math.max(1, Math.round(0.001 * sampleRate));
112
+ const back = 10;
113
+ const start = Math.max(1, from - back * block);
114
+ const blocks = Math.ceil((to - start) / block);
115
+ const raw = new Float64Array(blocks);
116
+ const diff = new Float64Array(blocks);
117
+ for (let i = start; i < to; i += 1) {
118
+ const b = Math.floor((i - start) / block);
119
+ raw[b] = Math.max(raw[b]!, Math.abs(x[i]!));
120
+ diff[b] = Math.max(diff[b]!, Math.abs(x[i]! - x[i - 1]!));
121
+ }
122
+ let peakRaw = 0;
123
+ let peakDiff = 0;
124
+ for (let b = 0; b < blocks; b += 1) {
125
+ peakRaw = Math.max(peakRaw, raw[b]!);
126
+ peakDiff = Math.max(peakDiff, diff[b]!);
127
+ }
128
+ if (peakRaw === 0) return coarse;
129
+ const first = Math.floor((from - start) / block);
130
+ let best = -1;
131
+ let bestScore = 0;
132
+ let bestDiff = false;
133
+ let bestBg = 0;
134
+ for (let b = Math.max(1, first); b < blocks; b += 1) {
135
+ let bgRaw = 0;
136
+ let bgDiff = 0;
137
+ for (let k = Math.max(0, b - back); k < b; k += 1) {
138
+ bgRaw = Math.max(bgRaw, raw[k]!);
139
+ bgDiff = Math.max(bgDiff, diff[k]!);
140
+ }
141
+ const rawScore = raw[b]! / (bgRaw + 0.02 * peakRaw);
142
+ const diffScore = peakDiff > 0 ? diff[b]! / (bgDiff + 0.02 * peakDiff) : 0;
143
+ const score = Math.max(rawScore, diffScore);
144
+ if (score > bestScore) {
145
+ bestScore = score;
146
+ best = b;
147
+ bestDiff = diffScore > rawScore;
148
+ bestBg = bestDiff ? bgDiff : bgRaw;
149
+ }
150
+ }
151
+ if (best < 0) return coarse;
152
+ const level = bestDiff ? diff[best]! : raw[best]!;
153
+ const threshold = bestBg + 0.25 * (level - bestBg);
154
+ const lo = Math.max(1, start + (best - 1) * block);
155
+ const hi = Math.min(to, start + (best + 1) * block);
156
+ let at = start + best * block;
157
+ for (let i = lo; i < hi; i += 1) {
158
+ const v = bestDiff ? Math.abs(x[i]! - x[i - 1]!) : Math.abs(x[i]!);
159
+ if (v > threshold) {
160
+ at = i;
161
+ break;
162
+ }
163
+ }
164
+ return Math.max(0, at - Math.round(ONSET_LEAD_SECONDS * sampleRate));
165
+ }
166
+
167
+ /**
168
+ * Crest factor (peak over RMS) and onset rate (per second) of a window:
169
+ * the content probe that suggests a fit mode.
170
+ */
171
+ export function contentProbe(
172
+ x: ArrayLike<number>,
173
+ sampleRate: number,
174
+ ): { crest: number; onsetRate: number } {
175
+ let peak = 0;
176
+ let sum = 0;
177
+ for (let i = 0; i < x.length; i += 1) {
178
+ const v = x[i]!;
179
+ peak = Math.max(peak, Math.abs(v));
180
+ sum += v * v;
181
+ }
182
+ const rms = Math.sqrt(sum / Math.max(1, x.length));
183
+ const seconds = x.length / sampleRate;
184
+ return {
185
+ crest: rms > 0 ? peak / rms : 0,
186
+ onsetRate: seconds > 0 ? detectOnsets(x, sampleRate).length / seconds : 0,
187
+ };
188
+ }
189
+
190
+ /** Drums and speech (crest > 5 and > 2 onsets/s) fit as `beats`, the rest as `tones`. */
191
+ export function suggestFitMode(
192
+ x: ArrayLike<number>,
193
+ sampleRate: number,
194
+ ): "beats" | "tones" {
195
+ const { crest, onsetRate } = contentProbe(x, sampleRate);
196
+ return crest > 5 && onsetRate > 2 ? "beats" : "tones";
197
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Polyphase half-band oversampling for nonlinear stages (guitar rig, high
3
+ * bowed notes). Kaiser-windowed half-band FIRs: 47 taps, beta 8 for 2x and
4
+ * the inner stage of 4x; the 4x final stage (the one at the base rate, where
5
+ * aliases land in the audible band) is 127 taps, beta 10. `Decimate2` is the
6
+ * 47-tap downsampler alone (strings run some voices at 2x and decimate).
7
+ */
8
+
9
+ function besselI0(x: number): number {
10
+ let sum = 1;
11
+ let term = 1;
12
+ for (let k = 1; k < 50; k += 1) {
13
+ term *= (x / (2 * k)) ** 2;
14
+ sum += term;
15
+ if (term < 1e-12 * sum) break;
16
+ }
17
+ return sum;
18
+ }
19
+
20
+ /** Kaiser-windowed half-band lowpass taps (odd length, cutoff fs/4, DC gain 1). */
21
+ export function halfbandTaps(length = 47, beta = 8): Float64Array {
22
+ const taps = new Float64Array(length);
23
+ const mid = (length - 1) / 2;
24
+ const i0b = besselI0(beta);
25
+ for (let n = 0; n < length; n += 1) {
26
+ const k = n - mid;
27
+ const sinc = k === 0 ? 0.5 : Math.sin((Math.PI * k) / 2) / (Math.PI * k);
28
+ const r = k / mid;
29
+ const w = besselI0(beta * Math.sqrt(Math.max(0, 1 - r * r))) / i0b;
30
+ // Even offsets are true zeros of a half-band filter; keep them exact.
31
+ taps[n] = k !== 0 && k % 2 === 0 ? 0 : sinc * w;
32
+ }
33
+ return taps;
34
+ }
35
+
36
+ /** Nonzero taps of one polyphase branch: coefficients and their input offsets. */
37
+ type Phase = Readonly<{ taps: Float64Array; offsets: Int32Array }>;
38
+
39
+ function phaseOf(taps: Float64Array, parity: 0 | 1): Phase {
40
+ const kept: number[] = [];
41
+ const offsets: number[] = [];
42
+ for (let n = parity; n < taps.length; n += 2) {
43
+ if (taps[n] === 0) continue;
44
+ kept.push(taps[n]!);
45
+ offsets.push((n - parity) / 2);
46
+ }
47
+ return { taps: Float64Array.from(kept), offsets: Int32Array.from(offsets) };
48
+ }
49
+
50
+ function ringSize(length: number): number {
51
+ let size = 4;
52
+ while (size < length + 2) size *= 2;
53
+ return size;
54
+ }
55
+
56
+ /** Half-band 2:1 decimator: two input samples in, one out. */
57
+ export class Decimate2 {
58
+ private readonly taps: Float64Array;
59
+ private readonly offsets: Int32Array;
60
+ private readonly history: Float64Array;
61
+ private readonly mask: number;
62
+ private write = 0;
63
+
64
+ constructor(taps = halfbandTaps(47, 8)) {
65
+ const kept: number[] = [];
66
+ const offsets: number[] = [];
67
+ for (let n = 0; n < taps.length; n += 1) {
68
+ if (taps[n] === 0) continue;
69
+ kept.push(taps[n]!);
70
+ offsets.push(n);
71
+ }
72
+ this.taps = Float64Array.from(kept);
73
+ this.offsets = Int32Array.from(offsets);
74
+ this.history = new Float64Array(ringSize(taps.length));
75
+ this.mask = this.history.length - 1;
76
+ }
77
+
78
+ /** `a` then `b` at the high rate → one sample at the base rate. */
79
+ process(a: number, b: number): number {
80
+ const h = this.history;
81
+ const m = this.mask;
82
+ // Output is aligned to `a`, so a 2x round trip delays by whole samples.
83
+ h[this.write] = a;
84
+ const newest = this.write;
85
+ this.write = (this.write + 1) & m;
86
+ h[this.write] = b;
87
+ this.write = (this.write + 1) & m;
88
+ let acc = 0;
89
+ for (let i = 0; i < this.taps.length; i += 1)
90
+ acc += this.taps[i]! * h[(newest - this.offsets[i]!) & m]!;
91
+ return acc;
92
+ }
93
+
94
+ reset(): void {
95
+ this.history.fill(0);
96
+ }
97
+ }
98
+
99
+ /** Half-band 1:2 interpolator: one input sample in, two out. */
100
+ export class Interpolate2 {
101
+ private readonly even: Phase;
102
+ private readonly odd: Phase;
103
+ private readonly history: Float64Array;
104
+ private readonly mask: number;
105
+ private write = 0;
106
+
107
+ constructor(taps = halfbandTaps(47, 8)) {
108
+ this.even = phaseOf(taps, 0);
109
+ this.odd = phaseOf(taps, 1);
110
+ this.history = new Float64Array(ringSize((taps.length + 1) >> 1));
111
+ this.mask = this.history.length - 1;
112
+ }
113
+
114
+ /** Writes the two high-rate samples for input `x` into out[0], out[1]. */
115
+ process(x: number, out: Float64Array): void {
116
+ const h = this.history;
117
+ const m = this.mask;
118
+ h[this.write] = x;
119
+ const newest = this.write;
120
+ this.write = (this.write + 1) & m;
121
+ out[0] = 2 * branch(this.even, h, newest, m);
122
+ out[1] = 2 * branch(this.odd, h, newest, m);
123
+ }
124
+
125
+ reset(): void {
126
+ this.history.fill(0);
127
+ }
128
+ }
129
+
130
+ function branch(
131
+ phase: Phase,
132
+ history: Float64Array,
133
+ newest: number,
134
+ mask: number,
135
+ ): number {
136
+ let acc = 0;
137
+ const { taps, offsets } = phase;
138
+ for (let i = 0; i < taps.length; i += 1)
139
+ acc += taps[i]! * history[(newest - offsets[i]!) & mask]!;
140
+ return acc;
141
+ }
142
+
143
+ /** Runs a per-sample function at twice the rate, band-limited both ways. */
144
+ export class Oversample2x {
145
+ private readonly up: Interpolate2;
146
+ private readonly down: Decimate2;
147
+ private readonly pair = new Float64Array(2);
148
+
149
+ constructor(taps = halfbandTaps(47, 8)) {
150
+ this.up = new Interpolate2(taps);
151
+ this.down = new Decimate2(taps);
152
+ }
153
+
154
+ /** Latency in base-rate samples (linear phase, up plus down). */
155
+ static latency(taps = 47): number {
156
+ return (taps - 1) / 2;
157
+ }
158
+
159
+ process(x: number, fn: (x: number) => number): number {
160
+ const pair = this.pair;
161
+ this.up.process(x, pair);
162
+ return this.down.process(fn(pair[0]!), fn(pair[1]!));
163
+ }
164
+
165
+ reset(): void {
166
+ this.up.reset();
167
+ this.down.reset();
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Runs a per-sample function at four times the rate: a 2x cascade whose
173
+ * base-rate stage is the steep 127-tap, beta-10 half-band and whose inner
174
+ * stage is the 47-tap, beta-8 one (its transition band is already above
175
+ * the base Nyquist).
176
+ */
177
+ export class Oversample4x {
178
+ private readonly outerUp: Interpolate2;
179
+ private readonly outerDown: Decimate2;
180
+ private readonly inner: Oversample2x;
181
+ private readonly pair = new Float64Array(2);
182
+
183
+ constructor(outer = halfbandTaps(127, 10), inner = halfbandTaps(47, 8)) {
184
+ this.outerUp = new Interpolate2(outer);
185
+ this.outerDown = new Decimate2(outer);
186
+ this.inner = new Oversample2x(inner);
187
+ }
188
+
189
+ process(x: number, fn: (x: number) => number): number {
190
+ const pair = this.pair;
191
+ this.outerUp.process(x, pair);
192
+ const a = this.inner.process(pair[0]!, fn);
193
+ const b = this.inner.process(pair[1]!, fn);
194
+ return this.outerDown.process(a, b);
195
+ }
196
+
197
+ reset(): void {
198
+ this.outerUp.reset();
199
+ this.outerDown.reset();
200
+ this.inner.reset();
201
+ }
202
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Seeded randomness for 0.6 instruments. `seededRandom` is the existing
3
+ * sequential generator (src/audio/random.ts, unchanged); `unit` is a
4
+ * counter-based draw that is a pure function of (seed, index, slot), so one
5
+ * grain's or voice's draws never depend on how many draws came before it.
6
+ * Integer-only, identical on every platform. Never use Math.random.
7
+ */
8
+ export { seededRandom } from "../random.ts";
9
+
10
+ /** FNV-1a of a string: turns a stored seed plus a note id into a 32-bit key. */
11
+ export function seedHash(text: string): number {
12
+ let hash = 0x811c9dc5;
13
+ for (let index = 0; index < text.length; index += 1) {
14
+ hash ^= text.charCodeAt(index);
15
+ hash = Math.imul(hash, 0x01000193) >>> 0;
16
+ }
17
+ return hash >>> 0;
18
+ }
19
+
20
+ /** MurmurHash3 32-bit finaliser. */
21
+ function fmix32(h: number): number {
22
+ h ^= h >>> 16;
23
+ h = Math.imul(h, 0x85ebca6b);
24
+ h ^= h >>> 13;
25
+ h = Math.imul(h, 0xc2b2ae35);
26
+ h ^= h >>> 16;
27
+ return h >>> 0;
28
+ }
29
+
30
+ /**
31
+ * Counter-based random in [0, 1): draw `slot` of item `index` under `seed`.
32
+ * Changing one knob (say a reverse probability) does not reshuffle the rest.
33
+ */
34
+ export function unit(seed: number, index: number, slot: number): number {
35
+ const a = fmix32((seed ^ Math.imul(index | 0, 0x9e3779b1)) >>> 0);
36
+ return fmix32((a ^ Math.imul(slot + 1, 0x7feb352d)) >>> 0) / 4_294_967_296;
37
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * First-order antiderivative antialiasing (ADAA; Parker et al. DAFx-16,
3
+ * Bilbao et al. IEEE SPL 2017): y = (F(x) - F(x1)) / (x - x1) for a shaper
4
+ * f with antiderivative F, falling back to f at the midpoint when the step
5
+ * is tiny. Stateful: one instance per signal path.
6
+ */
7
+
8
+ /** log(cosh(x)) without overflow: the antiderivative of tanh. */
9
+ export function logCosh(x: number): number {
10
+ const a = Math.abs(x);
11
+ return a + Math.log1p(Math.exp(-2 * a)) - Math.LN2;
12
+ }
13
+
14
+ /** Hard clip to [-1, 1]. */
15
+ export function hardclip(x: number): number {
16
+ return x < -1 ? -1 : x > 1 ? 1 : x;
17
+ }
18
+
19
+ /** Antiderivative of `hardclip`, continuous at ±1. */
20
+ export function hardclipF(x: number): number {
21
+ return Math.abs(x) <= 1 ? 0.5 * x * x : Math.abs(x) - 0.5;
22
+ }
23
+
24
+ /**
25
+ * Asymmetric soft clip: tanh(x + bias) - tanh(bias). Even harmonics come
26
+ * from the offset; the subtraction keeps silence at zero.
27
+ */
28
+ export function asym(x: number, bias = 0.3): number {
29
+ return Math.tanh(x + bias) - Math.tanh(bias);
30
+ }
31
+
32
+ /** Antiderivative of `asym`. */
33
+ export function asymF(x: number, bias = 0.3): number {
34
+ return logCosh(x + bias) - x * Math.tanh(bias);
35
+ }
36
+
37
+ /** Generic first-order ADAA around a shaper and its antiderivative. */
38
+ export class Adaa1 {
39
+ x1 = 0;
40
+ F1: number;
41
+
42
+ constructor(
43
+ readonly f: (x: number) => number,
44
+ readonly F: (x: number) => number,
45
+ ) {
46
+ this.F1 = F(0);
47
+ }
48
+
49
+ process(x: number): number {
50
+ const Fx = this.F(x);
51
+ const dx = x - this.x1;
52
+ const y =
53
+ Math.abs(dx) < 1e-5 ? this.f(0.5 * (x + this.x1)) : (Fx - this.F1) / dx;
54
+ this.x1 = x;
55
+ this.F1 = Fx;
56
+ return y;
57
+ }
58
+
59
+ reset(): void {
60
+ this.x1 = 0;
61
+ this.F1 = this.F(0);
62
+ }
63
+ }
64
+
65
+ /** ADAA tanh. */
66
+ export function adaaTanh(): Adaa1 {
67
+ return new Adaa1(Math.tanh, logCosh);
68
+ }
69
+
70
+ /** ADAA hard clip. */
71
+ export function adaaHardclip(): Adaa1 {
72
+ return new Adaa1(hardclip, hardclipF);
73
+ }
74
+
75
+ /** ADAA asymmetric soft clip with a fixed bias. */
76
+ export function adaaAsym(bias = 0.3): Adaa1 {
77
+ return new Adaa1(
78
+ (x) => asym(x, bias),
79
+ (x) => asymF(x, bias),
80
+ );
81
+ }