polyphon 0.0.0-stage → 0.1.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 (174) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -2
  3. package/README.zh-TW.md +375 -0
  4. package/dist/engine/automation.d.ts +24 -0
  5. package/dist/engine/automation.d.ts.map +1 -0
  6. package/dist/engine/automation.js +55 -0
  7. package/dist/engine/automation.js.map +1 -0
  8. package/dist/engine/compile.d.ts +36 -0
  9. package/dist/engine/compile.d.ts.map +1 -0
  10. package/dist/engine/compile.js +103 -0
  11. package/dist/engine/compile.js.map +1 -0
  12. package/dist/engine/jukebox.d.ts +113 -0
  13. package/dist/engine/jukebox.d.ts.map +1 -0
  14. package/dist/engine/jukebox.js +287 -0
  15. package/dist/engine/jukebox.js.map +1 -0
  16. package/dist/engine/mixer.d.ts +29 -0
  17. package/dist/engine/mixer.d.ts.map +1 -0
  18. package/dist/engine/mixer.js +141 -0
  19. package/dist/engine/mixer.js.map +1 -0
  20. package/dist/engine/output.d.ts +14 -0
  21. package/dist/engine/output.d.ts.map +1 -0
  22. package/dist/engine/output.js +51 -0
  23. package/dist/engine/output.js.map +1 -0
  24. package/dist/engine/playback.d.ts +97 -0
  25. package/dist/engine/playback.d.ts.map +1 -0
  26. package/dist/engine/playback.js +311 -0
  27. package/dist/engine/playback.js.map +1 -0
  28. package/dist/engine/prepare.d.ts +6 -0
  29. package/dist/engine/prepare.d.ts.map +1 -0
  30. package/dist/engine/prepare.js +12 -0
  31. package/dist/engine/prepare.js.map +1 -0
  32. package/dist/engine/render.d.ts +14 -0
  33. package/dist/engine/render.d.ts.map +1 -0
  34. package/dist/engine/render.js +60 -0
  35. package/dist/engine/render.js.map +1 -0
  36. package/dist/index.d.ts +17 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +25 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/instruments/bowed.d.ts +357 -0
  41. package/dist/instruments/bowed.d.ts.map +1 -0
  42. package/dist/instruments/bowed.js +502 -0
  43. package/dist/instruments/bowed.js.map +1 -0
  44. package/dist/instruments/builtin.d.ts +261 -0
  45. package/dist/instruments/builtin.d.ts.map +1 -0
  46. package/dist/instruments/builtin.js +160 -0
  47. package/dist/instruments/builtin.js.map +1 -0
  48. package/dist/instruments/definition.d.ts +46 -0
  49. package/dist/instruments/definition.d.ts.map +1 -0
  50. package/dist/instruments/definition.js +5 -0
  51. package/dist/instruments/definition.js.map +1 -0
  52. package/dist/instruments/dsp/ambience.d.ts +17 -0
  53. package/dist/instruments/dsp/ambience.d.ts.map +1 -0
  54. package/dist/instruments/dsp/ambience.js +146 -0
  55. package/dist/instruments/dsp/ambience.js.map +1 -0
  56. package/dist/instruments/dsp/core.d.ts +22 -0
  57. package/dist/instruments/dsp/core.d.ts.map +1 -0
  58. package/dist/instruments/dsp/core.js +100 -0
  59. package/dist/instruments/dsp/core.js.map +1 -0
  60. package/dist/instruments/dsp/drums.d.ts +112 -0
  61. package/dist/instruments/dsp/drums.d.ts.map +1 -0
  62. package/dist/instruments/dsp/drums.js +566 -0
  63. package/dist/instruments/dsp/drums.js.map +1 -0
  64. package/dist/instruments/dsp/modal.d.ts +30 -0
  65. package/dist/instruments/dsp/modal.d.ts.map +1 -0
  66. package/dist/instruments/dsp/modal.js +54 -0
  67. package/dist/instruments/dsp/modal.js.map +1 -0
  68. package/dist/instruments/dsp/piano.d.ts +9 -0
  69. package/dist/instruments/dsp/piano.d.ts.map +1 -0
  70. package/dist/instruments/dsp/piano.js +230 -0
  71. package/dist/instruments/dsp/piano.js.map +1 -0
  72. package/dist/instruments/dsp/pluck.d.ts +53 -0
  73. package/dist/instruments/dsp/pluck.d.ts.map +1 -0
  74. package/dist/instruments/dsp/pluck.js +195 -0
  75. package/dist/instruments/dsp/pluck.js.map +1 -0
  76. package/dist/instruments/dsp/reverb.d.ts +16 -0
  77. package/dist/instruments/dsp/reverb.d.ts.map +1 -0
  78. package/dist/instruments/dsp/reverb.js +47 -0
  79. package/dist/instruments/dsp/reverb.js.map +1 -0
  80. package/dist/instruments/dsp/staccato.d.ts +18 -0
  81. package/dist/instruments/dsp/staccato.d.ts.map +1 -0
  82. package/dist/instruments/dsp/staccato.js +147 -0
  83. package/dist/instruments/dsp/staccato.js.map +1 -0
  84. package/dist/instruments/play.d.ts +21 -0
  85. package/dist/instruments/play.d.ts.map +1 -0
  86. package/dist/instruments/play.js +140 -0
  87. package/dist/instruments/play.js.map +1 -0
  88. package/dist/instruments/registry.d.ts +63 -0
  89. package/dist/instruments/registry.d.ts.map +1 -0
  90. package/dist/instruments/registry.js +50 -0
  91. package/dist/instruments/registry.js.map +1 -0
  92. package/dist/instruments/samples.d.ts +30 -0
  93. package/dist/instruments/samples.d.ts.map +1 -0
  94. package/dist/instruments/samples.js +109 -0
  95. package/dist/instruments/samples.js.map +1 -0
  96. package/dist/instruments/voices.d.ts +76 -0
  97. package/dist/instruments/voices.d.ts.map +1 -0
  98. package/dist/instruments/voices.js +1061 -0
  99. package/dist/instruments/voices.js.map +1 -0
  100. package/dist/midi.d.ts +15 -0
  101. package/dist/midi.d.ts.map +1 -0
  102. package/dist/midi.js +280 -0
  103. package/dist/midi.js.map +1 -0
  104. package/dist/notation/format.d.ts +12 -0
  105. package/dist/notation/format.d.ts.map +1 -0
  106. package/dist/notation/format.js +102 -0
  107. package/dist/notation/format.js.map +1 -0
  108. package/dist/notation/harmony.d.ts +83 -0
  109. package/dist/notation/harmony.d.ts.map +1 -0
  110. package/dist/notation/harmony.js +191 -0
  111. package/dist/notation/harmony.js.map +1 -0
  112. package/dist/notation/melody.d.ts +25 -0
  113. package/dist/notation/melody.d.ts.map +1 -0
  114. package/dist/notation/melody.js +138 -0
  115. package/dist/notation/melody.js.map +1 -0
  116. package/dist/notation/part.d.ts +34 -0
  117. package/dist/notation/part.d.ts.map +1 -0
  118. package/dist/notation/part.js +15 -0
  119. package/dist/notation/part.js.map +1 -0
  120. package/dist/notation/transform.d.ts +28 -0
  121. package/dist/notation/transform.d.ts.map +1 -0
  122. package/dist/notation/transform.js +67 -0
  123. package/dist/notation/transform.js.map +1 -0
  124. package/dist/song.d.ts +140 -0
  125. package/dist/song.d.ts.map +1 -0
  126. package/dist/song.js +45 -0
  127. package/dist/song.js.map +1 -0
  128. package/dist/tempo.d.ts +36 -0
  129. package/dist/tempo.d.ts.map +1 -0
  130. package/dist/tempo.js +81 -0
  131. package/dist/tempo.js.map +1 -0
  132. package/dist/theory.d.ts +70 -0
  133. package/dist/theory.d.ts.map +1 -0
  134. package/dist/theory.js +183 -0
  135. package/dist/theory.js.map +1 -0
  136. package/dist/transcribe.d.ts +81 -0
  137. package/dist/transcribe.d.ts.map +1 -0
  138. package/dist/transcribe.js +433 -0
  139. package/dist/transcribe.js.map +1 -0
  140. package/package.json +57 -4
  141. package/src/engine/automation.ts +65 -0
  142. package/src/engine/compile.ts +124 -0
  143. package/src/engine/jukebox.ts +322 -0
  144. package/src/engine/mixer.ts +166 -0
  145. package/src/engine/output.ts +59 -0
  146. package/src/engine/playback.ts +349 -0
  147. package/src/engine/prepare.ts +15 -0
  148. package/src/engine/render.ts +71 -0
  149. package/src/index.ts +28 -0
  150. package/src/instruments/bowed.ts +660 -0
  151. package/src/instruments/builtin.ts +180 -0
  152. package/src/instruments/definition.ts +52 -0
  153. package/src/instruments/dsp/ambience.ts +142 -0
  154. package/src/instruments/dsp/core.ts +97 -0
  155. package/src/instruments/dsp/drums.ts +565 -0
  156. package/src/instruments/dsp/modal.ts +76 -0
  157. package/src/instruments/dsp/piano.ts +226 -0
  158. package/src/instruments/dsp/pluck.ts +240 -0
  159. package/src/instruments/dsp/reverb.ts +56 -0
  160. package/src/instruments/dsp/staccato.ts +141 -0
  161. package/src/instruments/play.ts +151 -0
  162. package/src/instruments/registry.ts +100 -0
  163. package/src/instruments/samples.ts +119 -0
  164. package/src/instruments/voices.ts +1168 -0
  165. package/src/midi.ts +285 -0
  166. package/src/notation/format.ts +96 -0
  167. package/src/notation/harmony.ts +233 -0
  168. package/src/notation/melody.ts +133 -0
  169. package/src/notation/part.ts +42 -0
  170. package/src/notation/transform.ts +78 -0
  171. package/src/song.ts +170 -0
  172. package/src/tempo.ts +108 -0
  173. package/src/theory.ts +208 -0
  174. package/src/transcribe.ts +487 -0
@@ -0,0 +1,226 @@
1
+ // Acoustic piano (modal, calibrated against real pianos) and electric piano (FM).
2
+
3
+ import { Biquad, normalize, rng, tailFade } from './core.ts';
4
+
5
+ //
6
+ // Modal synthesis: each partial of each string is a decaying oscillator. The parameters are calibrated against measurements of real pianos and piano recordings from Apple Loops:
7
+ // - 1 to 3 strings per note (1 for the bass, 2 for the low-mid, 3 for the rest); each string is out of tune by a fraction of a cent and has a slightly different inharmonicity coefficient,
8
+ // and the beating between partials makes the sound alive rather than an electronic organ.
9
+ // - Two-stage decay: the vertical vibration of the string hands its energy quickly to the soundboard (dropping about 12 dB right after the attack), while the horizontal vibration leaves a long tail;
10
+ // high partials get an extra loss proportional to the square of frequency (the Chaigne and Askenfelt string damping model).
11
+ // - Excitation spectrum: partial amplitude is about inversely proportional to the square root of frequency, then falls away past a knee; the hammer strikes at about 1/8 to 1/13 of the string length, so partials near that point are weak.
12
+ // The sample is struck brightest (forte) and at playback is rolled off with a low-pass according to velocity (tone in instruments.ts): a light touch is dark, a hard strike bright.
13
+ // - Soundboard: a fixed set of resonance peaks shapes the body's timbre; on the attack, the low-frequency thud of the soundboard being struck, and the hammer's brief scrape.
14
+ // - Stretch tuning: bass slightly flat and treble slightly sharp (the Railsback curve), so octaves do not clash because of inharmonicity.
15
+
16
+ /** Stretch tuning (cents) */
17
+ export function pianoStretch(m: number): number {
18
+ const d = m - 64;
19
+ return Math.sign(d) * 0.004 * Math.pow(Math.abs(d), 2.2);
20
+ }
21
+
22
+ /** Inharmonicity coefficient: smallest in the low-mid register, growing toward the lowest notes and the treble */
23
+ function pianoB(m: number): number {
24
+ return m <= 40 ? 6e-5 * Math.pow(2, (40 - m) / 14) : 6e-5 * Math.pow(2, (m - 40) / 9.3);
25
+ }
26
+
27
+ /** Seconds for the tail to decay 60 dB (fundamental) */
28
+ export const pianoSustain = (m: number): number => Math.min(30, Math.max(1.2, 30 * Math.pow(2, -(m - 21) / 24)));
29
+
30
+ /** Frequency response of the soundboard and body (dB): shared by every note of the same piano */
31
+ let boardDb: ((f: number) => number) | null = null;
32
+ function board(f: number): number {
33
+ if (!boardDb) {
34
+ const r = rng(90210);
35
+ const modes: [number, number, number][] = [];
36
+ for (let i = 0; i < 34; i++) {
37
+ const fc = 70 * Math.pow(3200 / 70, (i + 0.5 + r() * 0.45) / 34);
38
+ modes.push([fc, 4.5 * r(), 3 + 4 * Math.abs(r())]);
39
+ }
40
+ boardDb = (x) => {
41
+ let g = 0;
42
+ for (const [fc, gain, q] of modes) {
43
+ const u = ((x - fc) * 2 * q) / fc;
44
+ g += gain / (1 + u * u);
45
+ }
46
+ return g;
47
+ };
48
+ }
49
+ // The soundboard radiates poorly at very low frequencies: it drops 12 dB per octave below 45 Hz
50
+ const hp = 1 / Math.sqrt(1 + Math.pow(45 / f, 4));
51
+ return Math.pow(10, boardDb(f) / 20) * hp;
52
+ }
53
+
54
+ /** Piano sample: soft is the soft voicing (felt, warm, used for narrative and the ending) */
55
+ export function piano(sr: number, m: number, soft = false): Float32Array {
56
+ const r = rng(m * 31 + (soft ? 7 : 0));
57
+ const f0 = 440 * Math.pow(2, (m - 69 + pianoStretch(m) / 100) / 12);
58
+ const Ts = pianoSustain(m);
59
+ const len = Math.min(9, Math.max(1.6, Ts * 0.85));
60
+ const n = Math.floor(len * sr);
61
+ const acc = new Float64Array(n);
62
+ const nyq = sr * 0.45;
63
+ const B = pianoB(m);
64
+ // Strings: how many, each one's detuning (cents), and the differences in inharmonicity coefficient
65
+ const nStr = m < 30 ? 1 : m < 42 ? 2 : 3;
66
+ const strings: [number, number][] = [];
67
+ for (let k = 0; k < nStr; k++) {
68
+ const cents = nStr === 1 ? 0 : (k - (nStr - 1) / 2) * (0.9 + 0.6 * r()) + 0.35 * r();
69
+ strings.push([f0 * Math.pow(2, cents / 1200), B * (1 + 0.02 * r())]);
70
+ }
71
+ // The knee of the excitation spectrum moves up with pitch; the soft voicing is darker
72
+ const oct = Math.pow(2, (m - 40) / 24);
73
+ const fk = (soft ? 650 : 1200) * oct;
74
+ const fe = (soft ? 1700 : 3000) * oct;
75
+ // Hammer strike point (as a fraction of string length): about 1/8 for the bass, about 1/13 for the treble
76
+ const beta = 1 / (8 + Math.max(0, m - 21) * 0.058);
77
+ // From the middle of the keyboard up, the felt stays on the string for about a period of the note, which filters the upper
78
+ // partials away (12 dB per octave above the cutoff: ~530 Hz at C4, 420 Hz from C5 up) and leaves the treble round; it fades in between C3 and C4
79
+ const felt = Math.min(1, Math.max(0, (m - 48) / 12));
80
+ const feltCut = (soft ? 0.8 : 1) * 420 * Math.pow(2, Math.max(0, 72 - m) / 36);
81
+ // Decay: σ = base loss + b3·ω²; the early stage (handed over to the soundboard) is fast, the later stage (the tail) is slow
82
+ const Tf = Ts / 2;
83
+ const prompt = soft ? 0.55 : 0.6;
84
+ // Frequency-dependent loss at the level measured on real strings (Chaigne and Askenfelt): the highs die well before the fundamental
85
+ const b3 = soft ? 9e-9 : 6e-9;
86
+ /** [frequency, amplitude] of the first partials, for the sympathetic strings */
87
+ const struck: [number, number][] = [];
88
+ for (let p = 1; p < 400; p++) {
89
+ const fp = p * f0 * Math.sqrt(1 + B * p * p);
90
+ if (fp > nyq) break;
91
+ const lp = 1 / (1 + (fp / fk) * (fp / fk));
92
+ const ex = Math.exp(-fp / fe);
93
+ let amp = Math.sqrt((f0 / fp) * lp * ex);
94
+ amp *= Math.pow(1 + Math.pow(fp / feltCut, 4), -felt / 2);
95
+ amp *= Math.abs(Math.sin(Math.PI * p * beta)) * 0.92 + 0.08;
96
+ amp *= board(fp);
97
+ if (amp < 3e-4) {
98
+ if (fp > fk * 4) break;
99
+ continue;
100
+ }
101
+ if (p <= 8) struck.push([fp, amp]);
102
+ const w2 = Math.pow(2 * Math.PI * fp, 2);
103
+ const s1 = 6.9 / Tf + b3 * w2;
104
+ const s2 = 6.9 / Ts + b3 * w2;
105
+ const k1 = Math.exp(-s1 / sr);
106
+ const k2 = Math.exp(-s2 / sr);
107
+ for (const [fs, Bs] of strings) {
108
+ // Each partial is also shifted a tiny bit (string non-uniformity, bridge coupling), so the beats between strings do not all cancel neatly together
109
+ const jit = nStr > 1 ? Math.pow(2, (0.45 * r()) / 1200) : 1;
110
+ const w = (2 * Math.PI * p * fs * jit * Math.sqrt(1 + Bs * p * p)) / sr;
111
+ const cw = Math.cos(w);
112
+ const sw = Math.sin(w);
113
+ // A struck string starts from rest: every partial starts from sin 0
114
+ let re = 1;
115
+ let im = 0;
116
+ let e1 = (amp * prompt) / nStr;
117
+ let e2 = (amp * (1 - prompt)) / nStr;
118
+ const stop = amp * 1e-4 / nStr;
119
+ for (let i = 0; i < n; i++) {
120
+ acc[i] += im * (e1 + e2);
121
+ const nr = re * cw - im * sw;
122
+ im = re * sw + im * cw;
123
+ re = nr;
124
+ e1 *= k1;
125
+ e2 *= k2;
126
+ if (e1 + e2 < stop) break;
127
+ }
128
+ }
129
+ }
130
+ sympathetic(acc, sr, m, struck, b3);
131
+ const y = new Float32Array(n);
132
+ for (let i = 0; i < n; i++) y[i] = acc[i];
133
+ // Set the volume by the energy in the first 0.25 s after the attack (rather than the peak): every note sounds equally loud, and loudness is left to playback velocity
134
+ let e = 0;
135
+ const w = Math.min(n, Math.floor(sr * 0.25));
136
+ for (let i = 0; i < w; i++) e += y[i] * y[i];
137
+ const rms = Math.sqrt(e / w) || 1;
138
+ // The soundboard thud when struck: a few low-frequency soundboard modes, brief; relatively noticeable in the treble (short strings, quiet string sound)
139
+ const knockAmp = rms * (soft ? 0.35 : 0.55) * Math.min(1.6, Math.max(0.5, (m - 30) / 40));
140
+ const knock: [number, number, number][] = [[96, 0.13, 1], [168, 0.09, 0.8], [281, 0.06, 0.6], [470, 0.035, 0.4]];
141
+ for (const [fq, dec, a] of knock) {
142
+ const bp = new Biquad(sr, 'bp', fq, 2.2);
143
+ const kd = Math.pow(10, -3 / (dec * sr));
144
+ let env = knockAmp * a * 6;
145
+ for (let i = 0; i < n && env > 1e-6; i++) {
146
+ y[i] += bp.run(r()) * env;
147
+ env *= kd;
148
+ }
149
+ }
150
+ // The hammer's scrape: mid-high frequencies, a few milliseconds
151
+ const hb = new Biquad(sr, 'bp', Math.min(5000, 1400 + f0 * 2), 0.9);
152
+ let he = rms * (soft ? 0.12 : 0.3);
153
+ const hk = Math.pow(10, -3 / (0.006 * sr));
154
+ for (let i = 0; i < n && he > 1e-6; i++) {
155
+ y[i] += hb.run(r()) * he;
156
+ he *= hk;
157
+ }
158
+ // Attack: it takes a moment for the hammer to push the string away (longer with the soft voicing's felt)
159
+ const att = Math.floor(sr * (soft ? 0.004 : 0.0015));
160
+ for (let i = 0; i < att && i < n; i++) y[i] *= i / att;
161
+ const g = 0.25 / rms;
162
+ for (let i = 0; i < n; i++) y[i] *= g;
163
+ return tailFade(y, sr);
164
+ }
165
+
166
+ /**
167
+ * Sympathetic resonance: strings left free by the dampers (the whole keyboard with the pedal down; the top of the treble always)
168
+ * pick up this note's partials through the bridge. A string answers in its n-th mode when that mode lands within a Hz or two of a partial:
169
+ * the strings an octave and a twelfth around a note answer, an equal-tempered third (14 cents off) barely does.
170
+ * Each answers at its own frequency (stretched tuning and inharmonicity differ), blooming in over a tenth of a second and ringing
171
+ * with its own decay, so the sound grows a soft halo that beats gently against the struck strings.
172
+ * The bloom is slow, so short unpedalled notes are damped before it is heard; held and pedalled notes carry it.
173
+ */
174
+ function sympathetic(acc: Float64Array, sr: number, m: number, struck: [number, number][], b3: number): void {
175
+ const n = acc.length;
176
+ for (const [fp, amp] of struck) {
177
+ if (fp > 4000) break;
178
+ for (const mode of [1, 2, 3]) {
179
+ const key = Math.round(69 + 12 * Math.log2(fp / mode / 440));
180
+ if (key === m || key < 21 || key > 108) continue;
181
+ const fs = 440 * Math.pow(2, (key - 69 + pianoStretch(key) / 100) / 12);
182
+ const f = mode * fs * Math.sqrt(1 + pianoB(key) * mode * mode);
183
+ const match = 1 / (1 + Math.pow((f - fp) / 1.5, 2));
184
+ const level = (amp * 0.12 * match) / mode;
185
+ if (level < 1e-4) continue;
186
+ const decay = Math.exp(-(6.9 / pianoSustain(key) + b3 * Math.pow(2 * Math.PI * f, 2)) / sr);
187
+ const bloom = Math.exp(-1 / (0.08 * sr));
188
+ const w = (2 * Math.PI * f) / sr;
189
+ let env = level;
190
+ let rise = 1;
191
+ for (let i = 0; i < n && env > level * 1e-3; i++) {
192
+ acc[i] += Math.sin(w * i) * env * (1 - rise);
193
+ rise *= bloom;
194
+ env *= decay;
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ /** Electric piano (Rhodes feel): 1:1 FM, modulation index decaying over time, plus a tuning-fork tine ring in the highs */
201
+ export function epiano(sr: number, m: number): Float32Array {
202
+ const f = 440 * Math.pow(2, (m - 69) / 12);
203
+ const t60 = Math.max(1.2, 6 * Math.pow(0.5, (m - 48) / 24));
204
+ const len = Math.min(5, t60 * 0.8);
205
+ const n = Math.floor(len * sr);
206
+ const y = new Float32Array(n);
207
+ const w = (2 * Math.PI * f) / sr;
208
+ const k = Math.pow(10, -3 / (t60 * sr));
209
+ const kt = Math.pow(10, -3 / (0.35 * sr));
210
+ let env = 1;
211
+ let tine = 0.12;
212
+ let pc = 0;
213
+ let pm = 0;
214
+ for (let i = 0; i < n; i++) {
215
+ const t = i / sr;
216
+ const I = 1.6 * Math.exp(-t / 0.3) + 0.35;
217
+ pc += w;
218
+ pm += w;
219
+ y[i] = Math.sin(pc + I * Math.sin(pm)) * env + Math.sin(pc * 7.1) * tine;
220
+ env *= k;
221
+ tine *= kt;
222
+ }
223
+ const att = Math.floor(sr * 0.003);
224
+ for (let i = 0; i < att; i++) y[i] *= i / att;
225
+ return tailFade(normalize(y), sr);
226
+ }
@@ -0,0 +1,240 @@
1
+ // Plucked strings: Karplus-Strong for guitars, harps and dulcimers; an additive model for bass plucks.
2
+
3
+ import { Biquad, normalize, rng, tailFade } from './core.ts';
4
+
5
+ // ───────────────────────── Plucked string: Karplus-Strong ─────────────────────────
6
+ //
7
+ // A digital waveguide string (Karplus-Strong, with an allpass for exact tuning), shaped by what a real pluck does:
8
+ // - The pluck: the finger or plectrum draws the string aside into a triangle and lets go. The force on the bridge is then a rectangular wave
9
+ // whose n-th harmonic is ∝ sin(nπ·pos)/n: the overtones fall 6 dB per octave, with gaps at multiples of 1/pos. The string slips off a finger
10
+ // over a fraction of a millisecond (off a nail or plectrum faster), a low-pass on top. Plain Karplus-Strong plucks with noise instead, which
11
+ // leaves every overtone at a random level, some 10-30 dB above the fundamental: a twangy, synthetic tone that changes from note to note.
12
+ // - The loss: a string loses energy to the air and to internal friction at a rate that grows with the square of frequency
13
+ // (σ = b1 + b3·ω², Chaigne and Askenfelt), whatever string the partial is on. The plain Karplus-Strong loop filter loses a fixed fraction
14
+ // per round trip instead, so a bass string (a long round trip) keeps its highs ringing like an organ while a treble string collapses to a sine
15
+ // within a few tenths of a second (a 2 kHz partial lasted 4.5 s on the harp's C2 and 0.9 s on its C5). Here the loop filter is a one-pole
16
+ // low-pass designed for each note so that every partial decays at the rate σ prescribes; the fundamental keeps the T60 asked for.
17
+ // - The body: the bridge drives the guitar top (the violin body, the harp soundboard). Its response is broadly flat, with strong peaks at the lowest
18
+ // modes (the air in the cavity, the top plate) that lift the partials near them and ring for a moment on their own, knocked by the release of the string.
19
+
20
+ /** A body mode: [frequency Hz, boost at the peak in dB, Q]; it rings for about 2.2·Q/f seconds */
21
+ export type BodyMode = [number, number, number];
22
+
23
+ /** Classical guitar: the air in the body (~95 Hz) coupled to the top plate (~200 Hz), and the top's next modes around 400 Hz */
24
+ export const GUITAR_BODY: BodyMode[] = [[96, 7, 14], [205, 6, 16], [400, 3.5, 12], [560, 2, 10]];
25
+ /** Steel-string guitar: a larger, stiffer top braced for the strings' higher tension */
26
+ export const STEEL_BODY: BodyMode[] = [[102, 6, 14], [190, 5, 16], [380, 3, 12], [650, 2, 10]];
27
+ /** Lute and oud: a light bowl-backed body with a thin top, modes higher than a guitar's */
28
+ export const LUTE_BODY: BodyMode[] = [[125, 5, 12], [260, 5, 14], [520, 3, 10]];
29
+ /** Violin family (pizzicato): the air mode A0 and the corpus modes B1-, B1+ */
30
+ export const VIOLIN_BODY: BodyMode[] = [[275, 6, 12], [460, 5, 14], [540, 4, 14]];
31
+ /** Concert harp: the long soundboard and the box behind it */
32
+ export const HARP_BODY: BodyMode[] = [[110, 3, 8], [190, 3, 10], [330, 2, 10]];
33
+ /** Hammered dulcimer: a shallow trapezoid box */
34
+ export const DULCIMER_BODY: BodyMode[] = [[180, 4, 12], [320, 4, 12], [610, 2.5, 10]];
35
+
36
+ export interface KsOpts {
37
+ /** Length in seconds */
38
+ len: number;
39
+ /** Seconds for the fundamental to decay to -60 dB (at this pitch) */
40
+ t60: number;
41
+ /** How fast the string is let go, 0-1: the pluck's low-pass, from 2 kHz (soft flesh) to 16 kHz (plectrum, hammer) */
42
+ bright: number;
43
+ /** Pluck position as a fraction of the string length, 0-0.5 (the nearer the bridge, the more overtones) */
44
+ pos: number;
45
+ /** Frequency-dependent string loss b3 (s⁻¹ per (rad/s)²): a partial at ω decays b3·(ω² − ω0²) per second faster than the fundamental. 2e-8 lets a 2 kHz partial ring about 1.5 s (steel), 6e-8 about 0.6 s (nylon) */
46
+ loss: number;
47
+ /** Detune of the second string (cents); 0 means a single string */
48
+ detune?: number;
49
+ /** Body modes */
50
+ body?: BodyMode[];
51
+ /** Loudness by register, dB per octave from middle C (the shorter, lighter treble strings move less air) */
52
+ tilt?: number;
53
+ }
54
+
55
+ /**
56
+ * The whole body: the listed low modes, then the many plate and cavity modes above them, too dense to list (24 of them log-spaced up to 4 kHz,
57
+ * -2 to +3 dB, Q 12-30). Each colours the partials near it and rings for some tens of milliseconds, so the bridge's sharp pulse each period
58
+ * comes out as a short wooden ring rather than a bare click train.
59
+ */
60
+ function bodyModes(body: BodyMode[]): BodyMode[] {
61
+ const r = rng(Math.round(body[0][0] * 97));
62
+ const lo = body[body.length - 1][0] * 1.25;
63
+ const dense: BodyMode[] = [];
64
+ for (let i = 0; i < 24; i++) dense.push([lo * Math.pow(4000 / lo, (i + 0.5 + 0.4 * r()) / 24), 0.5 + 2.5 * r(), 21 + 9 * r()]);
65
+ return [...body, ...dense];
66
+ }
67
+
68
+ /** Pass x through the body (in place): each mode adds a resonant band-pass, so a mode of gain G dB lifts its frequency by G dB */
69
+ function throughBody(sr: number, modes: BodyMode[], x: Float32Array): void {
70
+ const bank = modes.map(([hz, gain, q]) => [new Biquad(sr, 'bp', hz, q), Math.pow(10, gain / 20) - 1] as const);
71
+ for (let i = 0; i < x.length; i++) {
72
+ const v = x[i];
73
+ let s = v;
74
+ for (const [bp, k] of bank) s += k * bp.run(v);
75
+ x[i] = s;
76
+ }
77
+ }
78
+
79
+ /** Per round trip, the one-pole loop filter g·(1−p)/(1−p·z⁻¹) passes this much at ω relative to its gain g */
80
+ const onePole = (p: number, w: number): number => (1 - p) / Math.sqrt(1 - 2 * p * Math.cos(w) + p * p);
81
+
82
+ /**
83
+ * One string, heard through the body; returns the output and the bridge force it starts from (its level the instant before the string is let go).
84
+ * Body and string are both linear, so the body can come first (commuted synthesis): it filters the pluck, a fraction of a second long, rather than the whole note.
85
+ */
86
+ function ksString(sr: number, f: number, o: KsOpts, body: BodyMode[], ring: number): [Float32Array, number] {
87
+ const n = Math.floor(o.len * sr);
88
+ const y = new Float32Array(n);
89
+ const P = sr / f;
90
+ const w0 = (2 * Math.PI * f) / sr;
91
+ // Decay per second of a partial at x Hz, σ = b1 + b3·ω²: b1 is set so the fundamental keeps the T60 asked for, unless the string's own
92
+ // loss at that frequency is already faster (the top of the harp)
93
+ const b1 = Math.max(0.1, 6.91 / o.t60 - o.loss * Math.pow(2 * Math.PI * f, 2));
94
+ const sigma = (x: number) => b1 + o.loss * Math.pow(2 * Math.PI * x, 2);
95
+ // The loop filter g·(1−p)/(1−p·z⁻¹) loses σ(x)/f per round trip: matched at the fundamental and at a frequency high in the spectrum
96
+ const fr = Math.min(0.4 * sr, Math.max(4000, 3 * f));
97
+ const wr = (2 * Math.PI * fr) / sr;
98
+ const G0 = Math.exp(-sigma(f) / f);
99
+ const want = Math.exp(-(sigma(fr) - sigma(f)) / f);
100
+ let lo = 0;
101
+ let hi = 0.995;
102
+ for (let i = 0; i < 40; i++) {
103
+ const p = (lo + hi) / 2;
104
+ if (onePole(p, wr) / onePole(p, w0) > want) lo = p;
105
+ else hi = p;
106
+ }
107
+ // It must not gain at 0 Hz, where it loses b1 (on the highest strings this caps how fast the highs can die)
108
+ const gMax = Math.exp(-b1 / f);
109
+ if (G0 / onePole(hi, w0) > gMax) {
110
+ lo = 0;
111
+ for (let i = 0; i < 40; i++) {
112
+ const p = (lo + hi) / 2;
113
+ if (G0 / onePole(p, w0) > gMax) hi = p;
114
+ else lo = p;
115
+ }
116
+ }
117
+ const p = lo;
118
+ const g = G0 / onePole(p, w0);
119
+ // Tuning: the loop filter's phase delay at the fundamental comes off the delay line, and an allpass supplies the remaining fraction of a sample
120
+ const tau = Math.atan2(p * Math.sin(w0), 1 - p * Math.cos(w0)) / w0;
121
+ const L = Math.max(2, Math.floor(P - tau - 0.2));
122
+ const frac = P - tau - L;
123
+ const C = (1 - frac) / (1 + frac);
124
+ // The pluck: one period of the bridge force, summed from its harmonics (a rectangular wave of duty pos, centred on the start, rounded by the release)
125
+ const fc = 2000 * Math.pow(8, o.bright);
126
+ const exc = new Float32Array(L + 1 + ring);
127
+ for (let h = 1; h * f < sr * 0.45; h++) {
128
+ const a = (2 * Math.sin(Math.PI * h * o.pos)) / (Math.PI * h) / Math.sqrt(1 + Math.pow((h * f) / fc, 2));
129
+ const dw = (2 * Math.PI * h) / P;
130
+ for (let i = 0; i <= L; i++) exc[i] += a * Math.cos(dw * i);
131
+ }
132
+ const held = exc[0];
133
+ throughBody(sr, body, exc);
134
+ let lpv = 0;
135
+ let apIn = 0;
136
+ let apOut = 0;
137
+ for (let i = 0; i < n; i++) {
138
+ const d = i >= L ? y[i - L] : 0;
139
+ lpv = (1 - p) * d + p * lpv;
140
+ const ap = C * lpv + apIn - C * apOut;
141
+ apIn = lpv;
142
+ apOut = ap;
143
+ y[i] = (i < exc.length ? exc[i] : 0) + g * ap;
144
+ }
145
+ return [y, held];
146
+ }
147
+
148
+ /** A plucked string (or a course of two) through its instrument's body */
149
+ export function ks(sr: number, f: number, o: KsOpts): Float32Array {
150
+ const body = o.body ? bodyModes(o.body) : [];
151
+ const ring = Math.ceil(sr * Math.max(0, ...body.map(([hz, , q]) => (2.6 * q) / hz)));
152
+ const [y, held1] = ksString(sr, f, o, body, ring);
153
+ let held = held1;
154
+ if (o.detune) {
155
+ const [y2, held2] = ksString(sr, f * Math.pow(2, o.detune / 1200), o, body, ring);
156
+ for (let i = 0; i < y.length; i++) y[i] = 0.55 * (y[i] + y2[i]);
157
+ held = 0.55 * (held1 + held2);
158
+ }
159
+ // Before the release the finger holds the string with a steady force on the bridge, which makes no sound. Measured from there the sound
160
+ // starts from silence (no click), and letting go is a sudden push on the body; the body radiates poorly below its air mode, so the push
161
+ // fades within a couple of milliseconds, knocking the body modes on its way
162
+ const knock = new Float32Array(Math.min(y.length, Math.floor(sr * 0.02) + ring));
163
+ const kd = Math.exp((-2 * Math.PI * 90) / sr);
164
+ for (let i = 0; i < knock.length; i++, held *= kd) knock[i] = -held;
165
+ throughBody(sr, body, knock);
166
+ for (let i = 0; i < knock.length; i++) y[i] += knock[i];
167
+ // Remove DC and soften the highest frequencies
168
+ const hp = new Biquad(sr, 'hp', 40);
169
+ const lpf = new Biquad(sr, 'lp', 12000);
170
+ for (let i = 0; i < y.length; i++) y[i] = lpf.run(hp.run(y[i]));
171
+ // Loudness is set by the energy of the first half second rather than the peak (which the sharp edge of the pluck decides), falling by tilt dB per octave
172
+ const w = Math.min(y.length, Math.floor(sr * 0.5));
173
+ let e = 0;
174
+ for (let i = 0; i < w; i++) e += y[i] * y[i];
175
+ const k = (0.2 * Math.pow(10, ((o.tilt ?? 0) * Math.log2(f / 261.63)) / 20)) / (Math.sqrt(e / w) || 1);
176
+ for (let i = 0; i < y.length; i++) y[i] *= k;
177
+ return tailFade(y, sr);
178
+ }
179
+
180
+ // ───────────────────────── Plucked string (additive): bass plucks ─────────────────────────
181
+
182
+ export interface PluckOpts {
183
+ len: number;
184
+ /** Seconds for the fundamental to decay to -60 dB; higher harmonics progressively faster */
185
+ t60: number;
186
+ /** Pluck position 0-0.5 (decides which harmonics are weakened) */
187
+ pos: number;
188
+ /** Harmonic slope (the larger, the darker) */
189
+ tilt: number;
190
+ /** Frequencies of the body resonances (Hz): they sound together at the instant of the pluck and vanish quickly */
191
+ body: number[];
192
+ /** Amount of the finger's dull thump */
193
+ thump: number;
194
+ }
195
+
196
+ /**
197
+ * Additive-synthesis plucked string: each harmonic decays on its own (the higher, the faster), slightly inharmonic, plus body resonance and the finger's dull thump.
198
+ * In the bass range Karplus-Strong decays down to a pure sine (like a synthesizer); this model keeps the woody mid-range.
199
+ */
200
+ export function pluckAdd(sr: number, f: number, o: PluckOpts): Float32Array {
201
+ const n = Math.floor(o.len * sr);
202
+ const y = new Float32Array(n);
203
+ const nyq = Math.min(sr * 0.42, 7000);
204
+ const B = 0.00012;
205
+ const r = rng(Math.round(f * 11));
206
+ for (let h = 1; h <= 40; h++) {
207
+ const fh = h * f * Math.sqrt(1 + B * h * h);
208
+ if (fh > nyq) break;
209
+ const amp = (Math.abs(Math.sin(Math.PI * h * o.pos)) + 0.05) / Math.pow(h, o.tilt);
210
+ const t60 = o.t60 / (1 + 0.45 * (h - 1));
211
+ const k = Math.pow(10, -3 / (t60 * sr));
212
+ const w = (2 * Math.PI * fh) / sr;
213
+ let env = amp;
214
+ let ph = r() * 0.2;
215
+ for (let i = 0; i < n && env > 1e-6; i++) {
216
+ ph += w;
217
+ y[i] += Math.sin(ph) * env;
218
+ env *= k;
219
+ }
220
+ }
221
+ for (const hz of o.body) {
222
+ const w = (2 * Math.PI * hz) / sr;
223
+ const k = Math.pow(10, -3 / (0.22 * sr));
224
+ let env = 0.35;
225
+ for (let i = 0; i < n && env > 1e-6; i++) {
226
+ y[i] += Math.sin(w * i) * env;
227
+ env *= k;
228
+ }
229
+ }
230
+ const lp = new Biquad(sr, 'lp', 350, 0.7);
231
+ const kt = Math.pow(10, -3 / (0.05 * sr));
232
+ let env = o.thump;
233
+ for (let i = 0; i < n && env > 1e-6; i++) {
234
+ y[i] += lp.run(r()) * env * 3;
235
+ env *= kt;
236
+ }
237
+ const att = Math.floor(sr * 0.003);
238
+ for (let i = 0; i < att && i < n; i++) y[i] *= i / att;
239
+ return tailFade(normalize(y), sr);
240
+ }
@@ -0,0 +1,56 @@
1
+ // Procedural reverb: a stereo impulse response of decaying noise whose highs fade faster than its lows.
2
+
3
+ import { rng } from './core.ts';
4
+
5
+ /** The acoustic space a song plays in */
6
+ export interface Room {
7
+ /** Reverb time (seconds, decay by 60 dB) */
8
+ size: number;
9
+ /** 0 bright to 1 dark (how fast the highs decay; default 0.45) */
10
+ damp?: number;
11
+ /** Pre-delay (seconds; default 0.012) */
12
+ preDelay?: number;
13
+ /** Early reflection amount 0-1 (default 0.3) */
14
+ early?: number;
15
+ /** Attack time (seconds); above 0 it is a reverb that "grows in" (a reversed, ghostly feel) */
16
+ bloom?: number;
17
+ }
18
+
19
+ /** Generate a stereo impulse response: decaying noise, with the highs eaten away over time */
20
+ export function impulse(sr: number, o: Room): [Float32Array, Float32Array] {
21
+ const len = Math.floor(sr * Math.min(8, o.size * 1.15 + (o.preDelay ?? 0) + 0.05));
22
+ const pre = Math.floor((o.preDelay ?? 0.012) * sr);
23
+ const damp = o.damp ?? 0.45;
24
+ const out: [Float32Array, Float32Array] = [new Float32Array(len), new Float32Array(len)];
25
+ for (let ch = 0; ch < 2; ch++) {
26
+ const r = rng(1234 + ch * 777);
27
+ const d = out[ch];
28
+ const k = Math.pow(10, -3 / (o.size * sr));
29
+ let env = 1;
30
+ let lp = 0;
31
+ for (let i = pre; i < len; i++) {
32
+ const t = (i - pre) / sr;
33
+ // The low-pass coefficient shrinks over time: darker toward the end
34
+ const a = Math.max(0.02, (1 - damp * 0.85) * Math.exp((-t * damp * 3) / Math.max(0.5, o.size)));
35
+ lp += a * (r() - lp);
36
+ let v = lp * env;
37
+ if (o.bloom) v *= 1 - Math.exp(-t / o.bloom);
38
+ d[i] = v;
39
+ env *= k;
40
+ }
41
+ // Early reflections
42
+ const er = o.early ?? 0.3;
43
+ if (er > 0) {
44
+ for (let j = 0; j < 8; j++) {
45
+ const at = pre + Math.floor((0.004 + Math.abs(r()) * 0.06) * sr);
46
+ if (at < len) d[at] += er * (0.5 + 0.5 * Math.abs(r())) * (r() > 0 ? 1 : -1) * 0.6;
47
+ }
48
+ }
49
+ }
50
+ // Energy normalization: rooms of different sizes come out equally loud
51
+ let e = 0;
52
+ for (const d of out) for (let i = 0; i < d.length; i++) e += d[i] * d[i];
53
+ const g = 1 / Math.sqrt(e / 2 + 1e-9);
54
+ for (const d of out) for (let i = 0; i < d.length; i++) d[i] *= g * 0.5;
55
+ return out;
56
+ }
@@ -0,0 +1,141 @@
1
+ // Short sampled notes for fast figures (much cheaper than live synthesis): spiccato strings and brass stabs.
2
+
3
+ import { Biquad, rng, tailFade } from './core.ts';
4
+
5
+ /**
6
+ * Spiccato strings: the bow drops onto the string, draws a few periods of sawtooth (Helmholtz) motion, bounces off, and the string rings on freely.
7
+ * - Bowed for the first 40-65 ms: harmonics falling as 1/k through the body, the bite of the bow lightening as it bounces, and the rustle
8
+ * of the hair catching the string.
9
+ * - Then free: each partial decays at its own rate. The string's internal losses and the bridge take more energy per second from the
10
+ * upper partials (the decay rate grows with frequency), so they are gone within ~0.1 s while the fundamental rings on and the note
11
+ * darkens as it dies, like a pizzicato. (One envelope on every partial kept the attack's bright tone to the end, like a gated synth.)
12
+ * - Five players a few cents and a few milliseconds apart: three copies at ±7 cents beat like a chorus.
13
+ */
14
+ export function spicc(sr: number, m: number): Float32Array {
15
+ const f = 440 * Math.pow(2, (m - 69) / 12);
16
+ const y = new Float32Array(Math.floor(sr * 0.42));
17
+ const nyq = sr * 0.42;
18
+ const body = (hz: number) => 0.4 + 1.2 / (1 + ((hz - 450) / 250) ** 2) + 0.7 / (1 + ((hz - 1300) / 500) ** 2) + 0.5 / (1 + ((hz - 3000) / 900) ** 2);
19
+ const r = rng(m * 5 + 1);
20
+ // The bow bites and lightens as it bounces (shared by every partial)
21
+ const acc = new Float32Array(y.length);
22
+ for (let i = 0; i < y.length; i++) acc[i] = Math.min(1, i / sr / 0.008) * (0.25 + 0.75 * Math.exp(-i / sr / 0.06));
23
+ for (let p = 0; p < 5; p++) {
24
+ const ff = f * Math.pow(2, (5 * r()) / 1200);
25
+ const i0 = Math.floor(((r() + 1) / 2) * 0.012 * sr);
26
+ // The bow leaves the string
27
+ const i1 = i0 + Math.floor((0.052 + 0.012 * r()) * sr);
28
+ for (let k = 1; k < 40; k++) {
29
+ const hz = ff * k;
30
+ if (hz > nyq) break;
31
+ // Decay rate of the free string (nepers per second): T60 ≈ 0.5 s for the fundamental of A3, 0.09 s at 3 kHz
32
+ const dk = Math.exp(-(6 + 0.025 * hz) / sr);
33
+ let e = (0.77 * body(hz)) / k / (1 + (hz / 4500) ** 2);
34
+ // A rotating phasor from a random phase (cheaper than Math.sin per sample)
35
+ const w = (2 * Math.PI * hz) / sr;
36
+ const c = Math.cos(w);
37
+ const s = Math.sin(w);
38
+ const ph = r() * Math.PI;
39
+ let x = Math.cos(ph);
40
+ let z = Math.sin(ph);
41
+ for (let i = i0; i < y.length; i++) {
42
+ y[i] += z * e * acc[i - i0];
43
+ const nx = x * c - z * s;
44
+ z = x * s + z * c;
45
+ x = nx;
46
+ if (i >= i1) e *= dk;
47
+ }
48
+ }
49
+ }
50
+ const noise = new Biquad(sr, 'bp', Math.min(7000, f * 5), 1.2);
51
+ for (let i = 0; i < y.length; i++) y[i] += noise.run(r()) * 0.6 * Math.exp(-i / sr / 0.03);
52
+ // Levelled on the first 150 ms (the old peak normalization left notes ±3 dB apart at random; this is their average)
53
+ let e = 0;
54
+ const w = Math.floor(sr * 0.15);
55
+ for (let i = 0; i < w; i++) e += y[i] * y[i];
56
+ const g = 0.24 / (Math.sqrt(e / w) || 1);
57
+ for (let i = 0; i < y.length; i++) y[i] *= g;
58
+ return tailFade(y, sr);
59
+ }
60
+
61
+ /**
62
+ * Brass stab: a short forte blat from two players, a few cents and 12 ms apart. As in the live brass (voices.ts), the lips are a valve whose
63
+ * harmonics fall as e^(-f/F), with F following the loudness: the harmonics come in one after another during the attack, the note is brassiest
64
+ * at its peak and darkens as it dies away. The bell radiates the lows poorly and the cup rolls off the top (first-order corners at 1.1 and 6 kHz).
65
+ * Levelled on its first 250 ms, so every pitch hits equally hard.
66
+ */
67
+ export function brassStab(sr: number, m: number): Float32Array {
68
+ const f = 440 * Math.pow(2, (m - 69) / 12);
69
+ const y = new Float32Array(Math.floor(sr * 0.55));
70
+ const nyq = sr * 0.42;
71
+ const r = rng(m * 3 + 9);
72
+ const lo = 1100;
73
+ const hi = 6000;
74
+ const env = (t: number) => Math.min(1, t / 0.02) * Math.exp(-t / 0.28);
75
+ const BLOCK = 32;
76
+ for (const [det, delay] of [[-6, 0], [6, 0.012]]) {
77
+ const ff = f * Math.pow(2, det / 1200);
78
+ const n = Math.min(80, Math.floor(nyq / ff));
79
+ // Each harmonic is a rotating phasor (cheap sines) from a random phase, with the bell's response and the rotation over one block
80
+ const bell = new Float64Array(n + 1);
81
+ const re = new Float64Array(n + 1);
82
+ const im = new Float64Array(n + 1);
83
+ const cw = new Float64Array(n + 1);
84
+ const sw = new Float64Array(n + 1);
85
+ const cb = new Float64Array(n + 1);
86
+ const sb = new Float64Array(n + 1);
87
+ for (let k = 1; k <= n; k++) {
88
+ const hz = ff * k;
89
+ bell[k] = ((lo + hi) * hz) / Math.sqrt((hz * hz + lo * lo) * (hz * hz + hi * hi));
90
+ const ph = r() * Math.PI;
91
+ const w = (2 * Math.PI * hz) / sr;
92
+ re[k] = Math.cos(ph);
93
+ im[k] = Math.sin(ph);
94
+ cw[k] = Math.cos(w);
95
+ sw[k] = Math.sin(w);
96
+ cb[k] = Math.cos(w * BLOCK);
97
+ sb[k] = Math.sin(w * BLOCK);
98
+ }
99
+ const amp = new Float64Array(n + 1);
100
+ const i0 = Math.floor(delay * sr);
101
+ // (The last part-block falls inside the tail fade and is left silent)
102
+ for (let b = i0; b + BLOCK <= y.length; b += BLOCK) {
103
+ // Per block: the fall-off F from the loudness, then the harmonics' amplitudes, scaled so the loudness follows the envelope
104
+ const e = env((b - i0) / sr);
105
+ const rr = Math.exp(-ff / (2400 * (0.25 + 0.75 * Math.pow(e, 0.7))));
106
+ let rn = 1;
107
+ let s = 0;
108
+ for (let k = 1; k <= n; k++) {
109
+ rn *= rr;
110
+ amp[k] = rn * bell[k];
111
+ s += amp[k] * amp[k];
112
+ }
113
+ const g = e / (Math.sqrt(s) || 1);
114
+ for (let k = 1; k <= n; k++) {
115
+ const a = amp[k] * g;
116
+ let x = re[k];
117
+ let z = im[k];
118
+ if (a < 1e-6) {
119
+ // Too weak to hear: just turn the phasor on by a block
120
+ re[k] = x * cb[k] - z * sb[k];
121
+ im[k] = x * sb[k] + z * cb[k];
122
+ continue;
123
+ }
124
+ for (let i = b; i < b + BLOCK; i++) {
125
+ y[i] += z * a;
126
+ const nx = x * cw[k] - z * sw[k];
127
+ z = x * sw[k] + z * cw[k];
128
+ x = nx;
129
+ }
130
+ re[k] = x;
131
+ im[k] = z;
132
+ }
133
+ }
134
+ }
135
+ let e = 0;
136
+ const w = Math.min(y.length, Math.floor(sr * 0.25));
137
+ for (let i = 0; i < w; i++) e += y[i] * y[i];
138
+ const k = 0.29 / (Math.sqrt(e / w) || 1);
139
+ for (let i = 0; i < y.length; i++) y[i] *= k;
140
+ return tailFade(y, sr);
141
+ }