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,660 @@
1
+ // Offline synthesis of bowed string instruments: the whole of "a group of players bowing one note" is computed in advance, so playback only plays a sample and uses almost no CPU at runtime.
2
+ //
3
+ // Comparing against the spectra of real string recordings, there are three reasons the synthesized strings sound like brass:
4
+ // 1. Too clean: the overtones of real strings are smeared bands, with the scraping noise of the bow hair filling the space between partials; the synthesized ones are thin lines with pure black in between.
5
+ // 2. The ensemble is too fake: every player has different intonation, vibrato speed and bowing position, so the high partials spread out; two oscillators vibrating in sync are just a chorus.
6
+ // 3. The body is not messy enough: a wooden body has dozens of narrow, irregular resonances, and as the vibrato sweeps the partials across them their strength keeps flickering.
7
+ // So here every player is computed separately: a fixed intonation offset, slowly drifting intonation, their own vibrato, their own bowing point (the comb notches fall at different positions),
8
+ // and bow noise that appears in sync with each slip of the bow on the string; all of that is summed and then passed through a body of a hundred-odd modes.
9
+ // Measured against the University of Iowa violin, cello and double bass recordings (sustained notes on every string), three things were still off:
10
+ // - the body's actual response did not follow its envelope: below the air resonance and above the bridge hill the modes' skirts piled up, so the
11
+ // open G of a violin had a fundamental as strong as its second partial (real: ~20 dB weaker) and the double bass was 10-20 dB too bright; and
12
+ // above 1 kHz the modes were too few and too broad for the vibrato to make the partials flicker (2-3 dB, real violins 5-15 dB) (bodyModes);
13
+ // - a corner fixed in hertz left the low strings as bright as the high ones (bowTable);
14
+ // - between the partials the noise floor was 6-14 dB cleaner than the recordings (bow noise raised, and rounded off with the tone).
15
+ // Measured again on the cello's A string (A3 up), where the solo cello sounded like a synthesizer: one bowing for every string left it dull
16
+ // and hissy there; the top string now has its own bowing point, corner and noise (TopString, CELLO_A).
17
+ //
18
+ // The sample has two parts: the attack (the bow grips the string, the vibrato comes in and grows) and a sustain section that can loop seamlessly.
19
+ // All the modulation in the sustain section (vibrato, intonation drift, bow-pressure swell, bow noise) has the loop length as its period,
20
+ // and each player's frequency is also fine-tuned so that it covers a whole number of periods within the loop length, so there is no seam at the loop point.
21
+ //
22
+ // Pitch-tracked on held notes, the vibrato was there but easy to miss: it began 0.3 s into a note and reached full depth only at 0.5 s, so eighth
23
+ // notes had none and quarter notes were half straight; it was a little narrow (violin ±20 cents, cello ±15, each double bass ±8) and every cycle
24
+ // was the same; and rounding its speed to whole cycles in a 1.5 s loop put a violin at 5.3 or 6.0 Hz depending on the note, and a section's seven
25
+ // players on just two speeds. Now it starts 0.1 s after the bow (audible from about 0.2 s), is most of the way in two cycles later and keeps
26
+ // deepening a little until 0.8 s, so a held note blooms; a solo's loop is cut to fit its vibrato and a section's is long enough for its players
27
+ // to spread over three speeds; and speed and depth wander a little from cycle to cycle, as a hand's do.
28
+
29
+ import { rng } from './dsp/core.ts';
30
+
31
+ /** Body: a spectral envelope ([Hz, dB] polyline, the average radiated response) plus narrow modes scattered according to it */
32
+ export interface BodyDef {
33
+ seed: number;
34
+ env: [number, number][];
35
+ /** The lowest mode (the air resonance A0 sits just above it: nothing radiates below) and the highest */
36
+ lo: number;
37
+ hi: number;
38
+ /** Modes per octave at lo; the count grows with frequency */
39
+ density: number;
40
+ /** Q of the modes: low end, high end */
41
+ q: [number, number];
42
+ /** How far each mode jumps above and below the envelope (dB): the larger it is, the more uneven the body */
43
+ rand: number;
44
+ }
45
+
46
+ export interface BowDef {
47
+ /** Number of players: 1 is a solo */
48
+ players: number;
49
+ /** Fixed intonation offset between players (cents, standard deviation) */
50
+ spread: number;
51
+ /** Intonation that each player slowly drifts (cents) */
52
+ drift: number;
53
+ /** Vibrato speed (Hz): each note, and each player in a section, varies around it */
54
+ vibRate: number;
55
+ /** Vibrato depth once it has grown in (cents, up and down from the note) */
56
+ vibDepth: number;
57
+ /** How long after the bow starts the vibrato begins (seconds) */
58
+ vibDelay: number;
59
+ /** Range of the bowing point (fraction of the string length): each player sits somewhere in it */
60
+ beta: [number, number];
61
+ /** Bow force: the partial number where the rounded Helmholtz corner has cut the partials by 6 dB, [soft, hard] */
62
+ corner: [number, number];
63
+ /** Sustained bow noise (relative to the string tone) */
64
+ noise: number;
65
+ /** Attack scrape sound */
66
+ scrape: number;
67
+ /** Attack time (seconds), [soft, hard] */
68
+ attack: [number, number];
69
+ /** Time offset at which players come in (seconds) */
70
+ onset: number;
71
+ /** How many cents below the note the attack slides up from */
72
+ scoop: number;
73
+ body: BodyDef;
74
+ /** Loop section length (seconds); a solo's is adjusted to hold a whole number of its vibrato cycles */
75
+ loop: number;
76
+ /** The top string, where it is unlike the others (see topString); notes from its open pitch up are played on it */
77
+ top?: TopString;
78
+ }
79
+
80
+ /**
81
+ * The top string's own bowing: its open pitch (Hz); the bowing point as a fraction of the open string (the bow keeps its distance from the
82
+ * bridge as a finger shortens the string, so a stopped note's bowing point is that times f / open, and the comb's notches stay put in hertz);
83
+ * and its corner and bow noise as factors of the other strings'
84
+ */
85
+ export interface TopString {
86
+ open: number;
87
+ beta: number;
88
+ corner: number;
89
+ noise: number;
90
+ }
91
+
92
+ export interface Bowed {
93
+ /** The sample; it runs on a little past the loop's end, with the loop's start repeated (see GUARD) */
94
+ data: Float32Array;
95
+ /** Loop section: [start, end] (samples) */
96
+ loop: [number, number];
97
+ }
98
+
99
+ /** Attack section length (seconds): the attack and the vibrato's growth must both finish before it ends, and the body's resonances must have settled */
100
+ const PRE = 0.8;
101
+ /** The vibrato grows in over this long after it begins (about two cycles), to 85% of its depth; the rest it gains slowly up to the end of the attack section, so a held note blooms */
102
+ const VIB_GROW = 0.3;
103
+ const TABLE = 2048;
104
+ /** Samples after the loop's end that repeat its start */
105
+ const GUARD = 64;
106
+ /** Modulation (frequency, volume) is computed once every few samples, with linear interpolation in between */
107
+ const CR = 16;
108
+
109
+ /** Polyline envelope (linear interpolation on a log frequency axis), returns dB */
110
+ function envAt(env: [number, number][], f: number): number {
111
+ if (f <= env[0][0]) return env[0][1];
112
+ for (let i = 1; i < env.length; i++) {
113
+ if (f <= env[i][0]) {
114
+ const [f0, d0] = env[i - 1];
115
+ const [f1, d1] = env[i];
116
+ return d0 + ((d1 - d0) * Math.log(f / f0)) / Math.log(f1 / f0);
117
+ }
118
+ }
119
+ return env[env.length - 1][1];
120
+ }
121
+
122
+ /**
123
+ * One period of the bowed string: a sawtooth with the partials at integer multiples of the bowing point cut out, then rounded off; partials are only added up to maxF.
124
+ * The rounding of the Helmholtz corner (Cremer): every period the string's internal losses (a roughly constant loss factor in the gut or
125
+ * synthetic core and the windings, so partial k loses in proportion to k per period) smooth the corner, and every pass under the bow sharpens it again,
126
+ * the more so the harder the bow presses. In balance partial k is cut by 1 / (1 + (k / kc)²): the cut sits at a partial number, not a frequency,
127
+ * so low notes are darker at the same frequency: in the recordings a note on the lowest string has 7 dB (violin) and 16 dB (cello) less of its
128
+ * energy at 4-8 kHz than one on the highest; a corner fixed in hertz gave 2 and 5 dB.
129
+ */
130
+ function bowTable(f: number, beta: number, kc: number, maxF: number): Float32Array {
131
+ const t = new Float32Array(TABLE);
132
+ const K = Math.max(1, Math.min(400, Math.floor(maxF / f)));
133
+ for (let k = 1; k <= K; k++) {
134
+ const a = (0.1 + Math.abs(Math.sin(Math.PI * k * beta))) / k / (1 + (k / kc) ** 2);
135
+ // Compute sin(2πkn/N) with a rotation recurrence, much faster than calling Math.sin at every point
136
+ const th = (2 * Math.PI * k) / TABLE;
137
+ const c = Math.cos(th);
138
+ const s = Math.sin(th);
139
+ let x = 0;
140
+ let y = 1;
141
+ for (let n = 0; n < TABLE; n++) {
142
+ t[n] += a * x;
143
+ const nx = x * c + y * s;
144
+ y = y * c - x * s;
145
+ x = nx;
146
+ }
147
+ }
148
+ let mx = 0;
149
+ for (let n = 0; n < TABLE; n++) mx = Math.max(mx, Math.abs(t[n]));
150
+ for (let n = 0; n < TABLE; n++) t[n] /= mx;
151
+ return t;
152
+ }
153
+
154
+ /** A band-pass mode y = b·(x[n] - x[n-2]) - a1·y[n-1] - a2·y[n-2] (its gain folded into b) */
155
+ interface Mode {
156
+ b: number;
157
+ a1: number;
158
+ a2: number;
159
+ }
160
+
161
+ /** Modes per octave stop growing here: neighbouring modes already overlap */
162
+ const MAX_DENSITY = 30;
163
+
164
+ /**
165
+ * Scatter modes according to the envelope (band-pass); each seed is a different instrument.
166
+ * - A plate has roughly a constant number of modes per hertz, so per octave the count grows with frequency: a few isolated modes in the
167
+ * low register (A0, B1-, B1+), a thicket above 1-2 kHz. There a ±20-cent vibrato sweeps each partial across peaks and dips and its level
168
+ * flickers by 5-10 dB, as measured on real violins; with a sparse, broad body the partials sound static.
169
+ * - Each mode reaches the listener with its own sign (radiation is a transfer function, not the bridge's driving-point admittance), so far
170
+ * from their peaks the skirts cancel instead of piling up: the response falls away below the lowest mode, where the air in the f-holes and
171
+ * the plates' volume change are in antiphase (an open G on a violin, 196 Hz below A0, radiates its fundamental ~20 dB under the second
172
+ * partial), and above the bridge hill.
173
+ * - A mode's peak is scaled so its power, spread over the gap to the next mode, matches the envelope: the envelope is the average response.
174
+ */
175
+ function bodyModes(sr: number, b: BodyDef): Mode[] {
176
+ const r = rng(b.seed);
177
+ const out: Mode[] = [];
178
+ const l0 = Math.log2(b.lo);
179
+ const oct = Math.log2(b.hi) - l0;
180
+ const top = Math.log2(Math.min(b.hi, sr * 0.45));
181
+ for (let x = l0; ; ) {
182
+ const n = Math.min(MAX_DENSITY, b.density * Math.pow(2, (x - l0) * 0.6));
183
+ x += (1 + 0.8 * r()) / n;
184
+ if (x >= top) break;
185
+ const f = Math.pow(2, x);
186
+ const q = b.q[0] + ((b.q[1] - b.q[0]) * (x - l0)) / oct;
187
+ const db = envAt(b.env, f) + r() * b.rand;
188
+ const g = (r() < 0 ? -1 : 1) * Math.pow(10, db / 20) * Math.sqrt((2 * q * Math.LN2) / (Math.PI * n));
189
+ const w = (2 * Math.PI * f) / sr;
190
+ const al = Math.sin(w) / (2 * q);
191
+ const a0 = 1 + al;
192
+ out.push({ b: (g * al) / a0, a1: (-2 * Math.cos(w)) / a0, a2: (1 - al) / a0 });
193
+ }
194
+ return out;
195
+ }
196
+
197
+ /** Smooth 0 -> 1 */
198
+ const smooth = (x: number) => (x <= 0 ? 0 : x >= 1 ? 1 : x * x * (3 - 2 * x));
199
+
200
+ /**
201
+ * Desks: tracks of one section that share a bowed instrument are different players. Two tracks on one sample are one player twice as loud:
202
+ * the same tuning, a vibrato moving in lockstep, heard as one violin slightly flanged. Each desk after the first has its own players: a fresh
203
+ * seed (vibrato wobble, drift, bow noise, scoop), and these differences from the first desk, kept inside what players in one section measure:
204
+ * - intonation: unison violins in an orchestra measured 0-13 cents apart (σ per note, two players; 0.6-8.6 with three or four), each with a
205
+ * steady tendency, one "generally a bit sharp", another "consistently flat" (Ioannou & Kob, ISMA 2019); listeners prefer a unison scatter
206
+ * of 0-5 cents and tolerate 5-14 (Ternström & Sundberg, JASA 1988). A desk leans 3 cents sharp or flat, ±2 from note to note: 1-5 cents
207
+ * from the first desk, 2-10 from each other, before the slow drift every player has;
208
+ * - vibrato: violinists' rates run from about 5.5 Hz (students, Geringer & Allen, JRME 2004) to 6.3 (recorded soloists, Cheslock 1931,
209
+ * Small 1937) and the extent is around 30 cents, 10-60 (Fletcher & Sanders, JASA 1967). A desk is 6% slower or faster (a violin's 6.2 Hz
210
+ * becomes 5.8 or 6.6) and 10% narrower or wider, and starts a note 0.3 of a cycle or more away in phase from the other desks, so two desks
211
+ * drift through and out of step instead of moving together;
212
+ * - the bow: a desk grips the string a little slower or quicker, and lands a little behind the first desk (`lag`, see play.ts). Ensembles
213
+ * measure 24-28 ms (string quartets, Wing et al. 2014) to 30-50 ms (trios, Rasch 1979) of onset scatter between parts; players of one part
214
+ * following the same leader are tighter, so a desk lags 8-12 ms on average, never more than 18 ms on top of the usual humanize.
215
+ */
216
+ export interface Desk {
217
+ /** Intonation (cents) from the first desk's mean pitch: a tendency, plus or minus 2 from note to note */
218
+ cents: number;
219
+ /** Vibrato speed and depth, as a factor of the first desk's */
220
+ rate: number;
221
+ depth: number;
222
+ /** Vibrato phase at the start of a note, from the first desk's (fraction of a cycle) */
223
+ phase: number;
224
+ /** How long the bow takes to grip the string, as a factor of the first desk's */
225
+ attack: number;
226
+ /** How far behind the first desk the bow lands on average (seconds); each note lands at half to one and a half times this */
227
+ lag: number;
228
+ }
229
+
230
+ /** The first desk plays the samples as they always were; a fourth track on an instrument sits at the first desk again, and so on round */
231
+ export const DESKS: readonly Desk[] = [
232
+ { cents: 0, rate: 1, depth: 1, phase: 0, attack: 1, lag: 0 },
233
+ { cents: 3, rate: 0.94, depth: 1.1, phase: 0.35, attack: 1.2, lag: 0.008 },
234
+ { cents: -3, rate: 1.06, depth: 0.9, phase: 0.65, attack: 0.85, lag: 0.012 },
235
+ ];
236
+
237
+ /** One player's part of a sample: bowing point, vibrato, drift, swell, tuning, timing and noise */
238
+ interface Player {
239
+ beta: number;
240
+ /** Vibrato speed (Hz, whole cycles in the loop), depth (cents), phase, and when it begins (seconds) */
241
+ vr: number;
242
+ vd: number;
243
+ vph: number;
244
+ von: number;
245
+ wob: [number, number, number, number, number][];
246
+ dr: [number, number, number][];
247
+ bw: [number, number];
248
+ /** Fixed intonation offset (cents) */
249
+ det: number;
250
+ on: number;
251
+ scoop: number;
252
+ nOff: number;
253
+ nAmt: number;
254
+ /** Phase of the string at the start */
255
+ ph: number;
256
+ }
257
+
258
+ /**
259
+ * Draw one player in the order the samples have always drawn them, so the first desk renders exactly as before.
260
+ * `rate` is the vibrato speed the player varies around (a solo's is already this note's).
261
+ */
262
+ function drawPlayer(r: () => number, d: BowDef, solo: boolean, rate: number, loud: boolean, LS: number, sr: number): Player {
263
+ const L = LS / sr;
264
+ const beta = d.beta[0] + (d.beta[1] - d.beta[0]) * (solo ? 0.5 : (r() + 1) / 2);
265
+ // Vibrato: every player has a different speed, depth, phase and onset; the speed is a whole number of cycles within the loop length
266
+ const vr = Math.max(1, Math.round((solo ? rate : rate * (1 + r() * 0.09)) * L)) / L;
267
+ const vd = d.vibDepth * (loud ? 1 : 0.9) * (solo ? 1 + r() * 0.08 : 0.9 + r() * 0.3);
268
+ const vph = (r() + 1) * Math.PI;
269
+ const von = d.vibDelay * (solo ? 1 : 0.7 + 0.6 * Math.abs(r()));
270
+ // No two cycles alike: the speed wanders by a few percent and the depth by about a tenth, along four slow sinusoids whose period is the loop length.
271
+ // Each is [Hz, phase swing, depth swing, the two phases]; a phase swing of a·sin(2π·hz·t) moves the speed by up to a·hz / vr of itself (here 5%)
272
+ const wob: [number, number, number, number, number][] = [1, 2, 3, 4].map((k) => [k / L, (0.05 * r() * vr) / (k / L), 0.12 * r(), (r() + 1) * Math.PI, (r() + 1) * Math.PI]);
273
+ // Intonation drift: three slow sinusoids whose period is the loop length
274
+ const dr: [number, number, number][] = [1, 2, 3].map((k) => [k / L, d.drift * r() * 0.6, (r() + 1) * Math.PI]);
275
+ // Bow-pressure swell: a very slow volume change (±5%)
276
+ const bw: [number, number] = [(1 + Math.round((r() + 1) * 1.5)) / L, (r() + 1) * Math.PI];
277
+ const det = solo ? 0 : d.spread * r() * 1.2;
278
+ const on = solo ? 0 : ((r() + 1) / 2) * d.onset;
279
+ const scoop = d.scoop * (0.6 + 0.4 * Math.abs(r()));
280
+ const nOff = Math.floor(((r() + 1) / 2) * LS);
281
+ const nAmt = d.noise * (loud ? 1.35 : 1) * (solo ? 1 : 0.8 + 0.4 * Math.abs(r()));
282
+ const ph = (r() + 1) / 2;
283
+ return { beta, vr, vd, vph, von, wob, dr, bw, det, on, scoop, nOff, nAmt, ph };
284
+ }
285
+
286
+ /** A player's pitch (cents from the note) at time t, with the vibrato grown in to v (0-1) */
287
+ function centsOf(p: Player): (t: number, v: number) => number {
288
+ const { vr, vd, vph, wob, dr } = p;
289
+ return (t, v) => {
290
+ let ph = 2 * Math.PI * vr * t + vph;
291
+ let g = 1;
292
+ for (const [hz, a, b, p1, p2] of wob) {
293
+ ph += a * Math.sin(2 * Math.PI * hz * t + p1);
294
+ g += b * Math.sin(2 * Math.PI * hz * t + p2);
295
+ }
296
+ let c = vd * v * g * (Math.sin(ph) + 0.12 * Math.sin(2 * ph));
297
+ for (const [hz, a, p2] of dr) c += a * Math.sin(2 * Math.PI * hz * t + p2);
298
+ return c;
299
+ };
300
+ }
301
+
302
+ /** The mean frequency ratio over the sustain section (the vibrato and drift, at full depth) */
303
+ function meanRatio(cents: (t: number, v: number) => number, L: number): number {
304
+ let m = 0;
305
+ const S = 2048;
306
+ for (let i = 0; i < S; i++) m += Math.pow(2, cents(PRE + (i / S) * L, 1) / 1200);
307
+ return m / S;
308
+ }
309
+
310
+ /** Samples in a loop of this many seconds: the attack section and the loop section are both integer multiples of CR, so the modulation's sample grid lines up at the loop's head and tail */
311
+ const loopSamples = (sr: number, seconds: number) => Math.round((seconds * sr) / CR) * CR;
312
+
313
+ /** A solo's vibrato speed for this note (a lighter bow goes with a slightly slower and narrower vibrato, and it varies a little from note to note); the first draw of its seed */
314
+ const soloRate = (r: () => number, d: BowDef, loud: boolean) => d.vibRate * (loud ? 1 : 0.96) * (1 + 0.04 * r());
315
+
316
+ /** A solo's loop: a whole number of its vibrato cycles */
317
+ const soloLoop = (sr: number, d: BowDef, rate: number, cycles = Math.round(rate * d.loop)) => loopSamples(sr, Math.max(1, cycles) / rate);
318
+
319
+ /** The first desk's solo for a note, replayed without rendering: the mean frequency of its sustain (Hz) and its vibrato phase */
320
+ function firstDesk(sr: number, f: number, d: BowDef, loud: boolean, seed: number): { mean: number; vph: number; rate: number } {
321
+ const r = rng(seed * 7919 + Math.round(f * 13));
322
+ const rate = soloRate(r, d, loud);
323
+ const LS = soloLoop(sr, d, rate);
324
+ // The bow noise takes the next LS draws
325
+ for (let i = 0; i < LS; i++) r();
326
+ const p = drawPlayer(r, d, true, rate, loud, LS, sr);
327
+ const L = LS / sr;
328
+ // Its frequency is fitted to a whole number of periods in the loop (see below), so its mean is that number over the loop length
329
+ return { mean: Math.round(f * meanRatio(centsOf(p), L) * L) / L, vph: p.vph, rate };
330
+ }
331
+
332
+ /**
333
+ * A note from the top string's open pitch up is played on it (a quarter tone of slack, so its open note's sample counts): its bowing. A
334
+ * section's players keep their spread of bowing points around the top string's
335
+ */
336
+ function topString(d: BowDef, f: number): BowDef {
337
+ const s = d.top;
338
+ if (!s || f < s.open * 0.97) return d;
339
+ const mid = (d.beta[0] + d.beta[1]) / 2;
340
+ const beta = (b: number) => s.beta * (b / mid) * (f / s.open);
341
+ return { ...d, beta: [beta(d.beta[0]), beta(d.beta[1])], corner: [d.corner[0] * s.corner, d.corner[1] * s.corner], noise: d.noise * s.noise };
342
+ }
343
+
344
+ /**
345
+ * Compute one bowed-string sample. f is the pitch (Hz), loud decides bow pressure (brightness, bow noise, attack), and seed makes the players different for every note.
346
+ * desk picks the players (see DESKS): 0 is the first desk.
347
+ * The returned sample can loop seamlessly across the loop interval.
348
+ */
349
+ export function bowSample(sr: number, f: number, d: BowDef, loud: boolean, seed: number, desk = 0): Bowed {
350
+ d = topString(d, f);
351
+ const dk = DESKS[desk % DESKS.length];
352
+ let r = rng(seed * 7919 + Math.round(f * 13));
353
+ const P = d.players;
354
+ const solo = P === 1;
355
+ // Vibrato speed: a solo varies a little from note to note and its loop is cut to a whole number of its cycles; in a section each player has
356
+ // their own speed, rounded to whole cycles in the loop
357
+ const rate = soloRate(r, d, loud);
358
+ /** The players' draws: the first desk takes them from the same stream as everything else, another desk from its own */
359
+ let rp = r;
360
+ let LS = solo ? soloLoop(sr, d, rate) : loopSamples(sr, d.loop);
361
+ /** A section desk after the first: how far its players lean from the first desk's (cents) */
362
+ let deskCents = 0;
363
+ /** A solo desk after the first: its vibrato phase and the mean frequency of its sustain (Hz) */
364
+ let deskSolo: { vph: number; mean: number } | undefined;
365
+ if (dk !== DESKS[0]) {
366
+ const ds = seed * 7919 + Math.round(f * 13) + desk * 104729;
367
+ r = rng(ds);
368
+ rp = rng(ds + 1);
369
+ const cents = dk.cents + 2 * r();
370
+ if (!solo) deskCents = cents;
371
+ else {
372
+ // Its own vibrato, starting away from the first desk's. The loop holds whole vibrato cycles and whole periods of the string, and a
373
+ // period is long for a low note (an A3 in a 2 s loop moves 4 cents a period, a cello's C2 13): of loops a few cycles shorter or
374
+ // longer, take the one whose whole number of periods lands closest to the intended pitch
375
+ const first = firstDesk(sr, f, d, loud, seed);
376
+ const own = first.rate * dk.rate;
377
+ const goal = first.mean * Math.pow(2, cents / 1200);
378
+ let best = Infinity;
379
+ const base = Math.round(own * d.loop);
380
+ for (let c = Math.max(1, base - 3); c <= base + 3; c++) {
381
+ const ls = soloLoop(sr, d, own, c);
382
+ const L = ls / sr;
383
+ const miss = Math.abs(Math.round(goal * L) / L / goal - 1) + Math.abs(c - base) * 1e-5;
384
+ if (miss < best) {
385
+ best = miss;
386
+ LS = ls;
387
+ }
388
+ }
389
+ deskSolo = { vph: first.vph + 2 * Math.PI * dk.phase, mean: Math.round(goal * (LS / sr)) / (LS / sr) };
390
+ }
391
+ }
392
+ const L = LS / sr;
393
+ const A = Math.round((PRE * sr) / CR) * CR;
394
+ const N = A + LS;
395
+ const sum = new Float32Array(N);
396
+ const corner = loud ? d.corner[1] : d.corner[0];
397
+ const atk = (loud ? d.attack[1] : d.attack[0]) * dk.attack;
398
+ const maxF = sr * 0.4;
399
+
400
+ // All players share one stretch of bow noise whose period is the loop length (each reads from a different position): high-pass then low-pass, running two passes so the filter state joins up head to tail.
401
+ // The noise rides on the slip, so the string rounds it off like the tone: two low-pass poles at the corner (kc partials, 2-6 kHz).
402
+ // Left flat to 6 kHz, the hiss was all a low cello note had above 4 kHz (12 dB more than a real C string); some hiss comes from the hair itself, so not below 2 kHz.
403
+ const nz = new Float32Array(LS);
404
+ {
405
+ const raw = new Float32Array(LS);
406
+ for (let i = 0; i < LS; i++) raw[i] = r();
407
+ let lp = 0;
408
+ let lp2 = 0;
409
+ let hp = 0;
410
+ const al = 1 - Math.exp((-2 * Math.PI * Math.min(6000, Math.max(2000, corner * f))) / sr);
411
+ const ah = 1 - Math.exp((-2 * Math.PI * 700) / sr);
412
+ for (let pass = 0; pass < 2; pass++) {
413
+ for (let i = 0; i < LS; i++) {
414
+ lp += al * (raw[i] - lp);
415
+ lp2 += al * (lp - lp2);
416
+ hp += ah * (lp2 - hp);
417
+ if (pass) nz[i] = lp2 - hp;
418
+ }
419
+ }
420
+ }
421
+
422
+ const tables = new Map<number, Float32Array>();
423
+ const T = (beta: number) => {
424
+ const k = Math.round(beta * 200);
425
+ let t = tables.get(k);
426
+ if (!t) tables.set(k, (t = bowTable(f, k / 200, corner, maxF)));
427
+ return t;
428
+ };
429
+
430
+ /** The speed each player's vibrato varies around: a solo's is this note's */
431
+ const vibRate = (solo ? rate : d.vibRate * (loud ? 1 : 0.96)) * dk.rate;
432
+ for (let p = 0; p < P; p++) {
433
+ const pl = drawPlayer(rp, d, solo, vibRate, loud, LS, sr);
434
+ if (deskSolo) pl.vph = deskSolo.vph;
435
+ pl.vd *= dk.depth;
436
+ const { beta, von, bw, on, scoop, nOff, nAmt } = pl;
437
+ const tab = T(beta);
438
+ const cents = centsOf(pl);
439
+ // Frequency fine-tuning: the sustain section's mean frequency times the loop length must come out to exactly a whole number of periods
440
+ // (a solo desk after the first has picked its loop for that: its mean frequency is already a whole number of periods)
441
+ const m = meanRatio(cents, L);
442
+ const target = f * Math.pow(2, (pl.det + deskCents) / 1200);
443
+ const F = deskSolo ? deskSolo.mean / m : Math.max(1, Math.round(target * m * L)) / (m * L);
444
+
445
+ const slip = beta;
446
+ const amp = solo ? 1 : 1 / Math.sqrt(P);
447
+ // Control rate: once every CR samples compute the frequency (the phase increment per sample) and the volume
448
+ const nc = N / CR + 2;
449
+ const inc = new Float64Array(nc);
450
+ const env = new Float64Array(nc);
451
+ for (let c = 0; c < nc; c++) {
452
+ const t = (c * CR) / sr;
453
+ const ta = t - on;
454
+ if (ta < 0) continue;
455
+ const v = 0.75 * smooth((t - von) / VIB_GROW) + 0.25 * smooth((t - von) / (PRE - von));
456
+ const sc = -scoop * Math.exp(-ta / 0.045);
457
+ inc[c] = (F * Math.pow(2, (cents(t, v) + sc) / 1200)) / sr;
458
+ // Attack: the bow grips the string (a hard bow overshoots a little and then comes back); after that comes a very slow bow-pressure swell
459
+ let e = smooth(ta / atk);
460
+ if (loud) e *= 1 + 0.22 * e * Math.exp(-Math.max(0, ta - atk) / 0.08);
461
+ env[c] = amp * e * (1 + 0.05 * Math.sin(2 * Math.PI * bw[0] * t + bw[1]));
462
+ }
463
+ let ph = pl.ph;
464
+ const i0 = Math.floor((on * sr) / CR) * CR;
465
+ for (let i = i0; i < N; i++) {
466
+ const c = (i / CR) | 0;
467
+ const u = (i - c * CR) / CR;
468
+ ph += inc[c] + (inc[c + 1] - inc[c]) * u;
469
+ if (ph >= 1) ph -= 1;
470
+ const x = ph * TABLE;
471
+ const j = x | 0;
472
+ const fr = x - j;
473
+ const s0 = tab[j];
474
+ const tone = s0 + (tab[(j + 1) & (TABLE - 1)] - s0) * fr;
475
+ // Bow noise appears along with the slip of the bow on the string (the short stretch at the start of each period)
476
+ const nzv = nz[(i + nOff) % LS] * (ph < slip ? 1 : 0.3) * nAmt;
477
+ sum[i] += (env[c] + (env[c + 1] - env[c]) * u) * (tone + nzv);
478
+ }
479
+ }
480
+
481
+ // The highs come in a little later on the attack: a one-pole low-pass from dark to fully open (the sustain section passes straight through)
482
+ {
483
+ let y = 0;
484
+ const end = Math.floor((atk * 1.6 + 0.05) * sr);
485
+ for (let i = 0; i < N; i++) {
486
+ if (i >= end) {
487
+ y = sum[i];
488
+ continue;
489
+ }
490
+ const u = smooth(i / end);
491
+ const fc = f * 3 + (sr * 0.45 - f * 3) * u * u;
492
+ const a = 1 - Math.exp((-2 * Math.PI * fc) / sr);
493
+ y += a * (sum[i] - y);
494
+ sum[i] = y;
495
+ }
496
+ }
497
+
498
+ // The attack's scrape (non-periodic, finishing within the attack section)
499
+ if (d.scrape > 0) {
500
+ const sc = d.scrape * (loud ? 1.4 : 0.7);
501
+ let lp = 0;
502
+ let hp = 0;
503
+ const al = 1 - Math.exp((-2 * Math.PI * 5000) / sr);
504
+ const ah = 1 - Math.exp((-2 * Math.PI * 900) / sr);
505
+ const len = Math.min(A - 1, Math.floor((atk + 0.12) * sr));
506
+ for (let i = 0; i < len; i++) {
507
+ const t = i / sr;
508
+ lp += al * (r() - lp);
509
+ hp += ah * (lp - hp);
510
+ sum[i] += (lp - hp) * sc * smooth(t / 0.006) * Math.exp(-t / (atk * 0.6 + 0.03));
511
+ }
512
+ }
513
+
514
+ // Body: a bank of narrow modes in parallel. They all take the same input difference x[n] - x[n-2], and run two at a time
515
+ const modes = bodyModes(sr, d.body);
516
+ const dx = new Float32Array(N);
517
+ for (let i = 0; i < N; i++) dx[i] = sum[i] - (i >= 2 ? sum[i - 2] : 0);
518
+ const out = new Float32Array(N);
519
+ const none: Mode = { b: 0, a1: 0, a2: 0 };
520
+ for (let m = 0; m < modes.length; m += 2) {
521
+ const { b: pb, a1: pa1, a2: pa2 } = modes[m];
522
+ const { b: qb, a1: qa1, a2: qa2 } = modes[m + 1] ?? none;
523
+ let p1 = 0;
524
+ let p2 = 0;
525
+ let q1 = 0;
526
+ let q2 = 0;
527
+ for (let i = 0; i < N; i++) {
528
+ const x = dx[i];
529
+ const p = pb * x - pa1 * p1 - pa2 * p2;
530
+ const q = qb * x - qa1 * q1 - qa2 * q2;
531
+ p2 = p1;
532
+ p1 = p;
533
+ q2 = q1;
534
+ q1 = q;
535
+ out[i] += p + q;
536
+ }
537
+ }
538
+
539
+ // Loop seam: the lowest body modes ring for a few hundred milliseconds, so at the loop start they still carry a trace of the attack (up to -31 dB
540
+ // for a low bass note). Crossfade the last 60 ms of the loop into the 60 ms that lead into it, so the jump back to the loop start is seamless.
541
+ {
542
+ const X = Math.min(Math.round(sr * 0.06), A);
543
+ for (let j = 0; j < X; j++) {
544
+ const w = (j + 0.5) / X;
545
+ out[N - X + j] += w * (out[A - X + j] - out[N - X + j]);
546
+ }
547
+ }
548
+
549
+ // Set the volume by the sustain section's RMS (every pitch equally loud, the soft and hard layers equally loud too; loudness is left to velocity)
550
+ let e2 = 0;
551
+ for (let i = A; i < N; i++) e2 += out[i] * out[i];
552
+ const k = 0.2 / Math.sqrt(e2 / LS + 1e-12);
553
+ for (let i = 0; i < N; i++) out[i] *= k;
554
+ // The buffer runs on past the loop's end with a copy of the loop's start. Chrome's looping source does not interpolate across the jump back
555
+ // when the loop ends at the very end of the buffer (a test sine clicked on every pass unless its loop began at a zero crossing), and low notes
556
+ // ticked once a loop; now the samples either side of the loop's end are the ones the loop plays.
557
+ const data = new Float32Array(N + GUARD);
558
+ data.set(out);
559
+ data.set(out.subarray(A, A + GUARD), N);
560
+ return { data, loop: [A, N] };
561
+ }
562
+
563
+ // ───────────────────────── Instruments ─────────────────────────
564
+
565
+ /** How a bowed instrument is sampled and played */
566
+ export interface BowInst {
567
+ def: BowDef;
568
+ /** One sample every how many semitones; the notes in between are made up with a rate change */
569
+ zone: number;
570
+ /** What fraction of the device sample rate to compute at (strings have almost nothing above 12 kHz, and bass instruments even less; saves memory and time) */
571
+ rate: number;
572
+ /** Attack and release at playback (seconds); a song's params.attack / release can override them */
573
+ attack: number;
574
+ release: number;
575
+ /** Velocity (times params.bright) above this value uses the hard-bow layer */
576
+ loudAt: number;
577
+ /** Glide time constant (seconds) */
578
+ glide: number;
579
+ level: number;
580
+ /** Share another instrument's samples (the same group of players, only played differently) */
581
+ share?: string;
582
+ }
583
+
584
+ // Body envelope: [Hz, dB], the average radiated response. Fitted, with the bow corner, to the sustained mf notes of the University of Iowa
585
+ // violin, cello and double bass recordings (1/3-octave partial levels across every string). The violin: air resonance A0 at 280 Hz, the corpus
586
+ // modes B1- and B1+ at 450-550 Hz, a dip around 700 Hz, the bridge hill at 2-4 kHz and a steep fall above 4-5 kHz. The cello sits an octave
587
+ // and a half lower; the double bass radiates mostly 50-200 Hz and is dark above. Bodies without recordings keep the shape the old model actually produced.
588
+ const VIOLIN_BODY: BodyDef = { seed: 11, lo: 200, hi: 9000, density: 9, q: [36, 70], rand: 7, env: [[200, -12], [280, -1], [480, 2], [560, 0], [720, -5], [1000, -1], [1400, -2], [1900, 3], [2400, 0], [3000, 5], [3800, 8], [5000, -1], [7000, -9], [9000, -16]] };
589
+ /** String section: the averaged body of violins and violas, flatter */
590
+ const SECTION_BODY: BodyDef = { seed: 41, lo: 160, hi: 8000, density: 6, q: [28, 36], rand: 5, env: [[150, -14], [240, 0], [350, 0], [500, 3], [700, -4], [1000, -2], [1400, 1], [2000, 3], [2600, 4], [3500, 0], [5000, -4], [8000, -16]] };
591
+ const CELLO_BODY: BodyDef = { seed: 23, lo: 80, hi: 9000, density: 9, q: [34, 70], rand: 7, env: [[70, -19], [100, -4], [150, 1], [220, -3], [350, 2], [500, 0], [700, -4], [1000, -1], [1800, 7], [2500, 7], [4000, -7], [6000, -7], [9000, -16]] };
592
+ const BASS_BODY: BodyDef = { seed: 31, lo: 36, hi: 6000, density: 7, q: [26, 48], rand: 5, env: [[36, -12], [45, -11], [60, -2], [100, 5], [150, 6], [200, -3], [300, -4], [500, -10], [1000, -11], [1600, -6], [2500, -9], [4000, -9], [6000, -11]] };
593
+ const FIDDLE_BODY: BodyDef = { seed: 53, lo: 220, hi: 10000, density: 9, q: [36, 70], rand: 7, env: [[220, -12], [300, 3], [520, 4], [800, 0], [1100, -1], [1500, -2], [2200, 1], [3000, 4], [4000, 5], [6000, -3], [10000, -16]] };
594
+ /** Morin khuur (horsehead fiddle): a skin-covered square body, a 900 Hz nasal tone, few highs */
595
+ const MORIN_BODY: BodyDef = { seed: 67, lo: 150, hi: 7000, density: 7, q: [26, 40], rand: 6, env: [[150, -12], [240, 1], [400, -2], [900, 6], [1300, 0], [1700, -3], [2400, 0], [3500, -5], [5000, -9], [7000, -16]] };
596
+
597
+ /**
598
+ * The cello's A string, fitted to the Iowa A-string notes A3-F#4 (arco mf and ff). Bowed like the lower strings (a comb notch every 8.3
599
+ * partials, the corner at 16, the same bow noise), the solo cello from A3 up had 5 dB too much of its energy in the fundamental and 8 dB too
600
+ * little at 2-4 kHz (spectral centroid 910 Hz, the recordings 1390), and 10-20 dB more hiss between its partials (at 1-2 kHz 29 dB under them,
601
+ * the recordings 43): a dull tone with a layer of hiss over it, like a synthesizer's strings; the low register measures close to the C and
602
+ * G strings. Bowed 3.5 cm from the bridge (5% of the open string) the comb's first notch sits near 4.4 kHz on every note of the string, so
603
+ * the partials run strong up to the bridge hill and fall past it, as in the recordings; the steel string loses less per period than the
604
+ * wound ones (a corner twice as high) and bows cleaner (a third of the noise). Now: centroid 1420 Hz, 2-4 kHz at -8 dB of the total
605
+ * (recordings -7), and the noise between partials 43, 35 and 26 dB under them at 1-2, 2-4 and 4-8 kHz (recordings 43, 35, 27)
606
+ */
607
+ const CELLO_A: TopString = { open: 220, beta: 0.05, corner: 2, noise: 0.3 };
608
+
609
+ const section = (body: BodyDef, players: number): BowDef => ({
610
+ players,
611
+ spread: 9,
612
+ drift: 6,
613
+ vibRate: 5.8,
614
+ vibDepth: 22,
615
+ vibDelay: 0.12,
616
+ beta: [0.07, 0.16],
617
+ corner: [7, 14],
618
+ noise: 0.1,
619
+ scrape: 0.6,
620
+ attack: [0.12, 0.06],
621
+ onset: 0.04,
622
+ scoop: 12,
623
+ body,
624
+ loop: 2,
625
+ });
626
+
627
+ const solo = (body: BodyDef, vibRate: number, vibDepth: number, beta: number, corner: [number, number], noise: number): BowDef => ({
628
+ players: 1,
629
+ spread: 0,
630
+ drift: 3,
631
+ vibRate,
632
+ vibDepth,
633
+ vibDelay: 0.1,
634
+ beta: [beta, beta],
635
+ corner,
636
+ noise,
637
+ scrape: 0.5,
638
+ attack: [0.08, 0.045],
639
+ onset: 0,
640
+ scoop: 14,
641
+ body,
642
+ loop: 2,
643
+ });
644
+
645
+ export const BOWS = {
646
+ /** String section: seven players */
647
+ strings: { def: section(SECTION_BODY, 7), zone: 3, rate: 0.75, attack: 0.02, release: 0.4, loudAt: 0.72, glide: 0.09, level: 0.89 },
648
+ /** The slow version of the string section (soft, long): the same samples */
649
+ strPad: { def: section(SECTION_BODY, 7), zone: 3, rate: 0.75, attack: 0.6, release: 0.9, loudAt: 2, glide: 0.09, level: 0.81, share: 'strings' },
650
+ violin: { def: solo(VIOLIN_BODY, 6.2, 27, 0.113, [8, 16], 0.1), zone: 3, rate: 0.75, attack: 0.02, release: 0.25, loudAt: 0.72, glide: 0.09, level: 0.57 },
651
+ cello: { def: { ...solo(CELLO_BODY, 5.6, 24, 0.12, [8, 16], 0.2), top: CELLO_A }, zone: 3, rate: 0.5, attack: 0.02, release: 0.3, loudAt: 0.72, glide: 0.09, level: 0.66 },
652
+ /** Folk fiddle: brighter, with a faster, narrower vibrato that comes in later */
653
+ fiddle: { def: { ...solo(FIDDLE_BODY, 6.2, 12, 0.09, [11, 22], 0.12), vibDelay: 0.18 }, zone: 3, rate: 0.75, attack: 0.015, release: 0.2, loudAt: 0.72, glide: 0.07, level: 0.6 },
654
+ /** Morin khuur: horsehair bow, lots of breath noise, wide slow vibrato that comes in later */
655
+ morin: { def: { ...solo(MORIN_BODY, 4.4, 26, 0.15, [6, 11], 0.14), vibDelay: 0.18 }, zone: 3, rate: 0.75, attack: 0.03, release: 0.3, loudAt: 0.72, glide: 0.14, level: 0.81 },
656
+ /** Cello section: five players, for sustained notes and harmony in the low-mid register (use cello for solos) */
657
+ celli: { def: { ...section(CELLO_BODY, 5), vibRate: 5.5, vibDepth: 20, corner: [7, 14], noise: 0.15, top: CELLO_A }, zone: 3, rate: 0.5, attack: 0.03, release: 0.4, loudAt: 0.72, glide: 0.1, level: 0.66 },
658
+ /** Double bass section: three players, with a slower vibrato */
659
+ contrabass: { def: { ...section(BASS_BODY, 3), vibRate: 5, vibDepth: 14, corner: [8, 16], spread: 4 }, zone: 3, rate: 0.5, attack: 0.03, release: 0.35, loudAt: 0.72, glide: 0.1, level: 0.61 },
660
+ } satisfies Record<string, BowInst>;