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,151 @@
1
+ // Play one note on any instrument: a cached sample, a bowed sample with a looping sustain, or a live voice.
2
+
3
+ import { DESKS } from './bowed.ts';
4
+ import { midiToFrequency, type SampledInstrument } from './definition.ts';
5
+ import { getInstrument } from './registry.ts';
6
+ import { getSample, hasSample, loopOf, sampleMidi } from './samples.ts';
7
+
8
+ export interface NoteCall {
9
+ ctx: BaseAudioContext;
10
+ out: AudioNode;
11
+ /** Start time (context seconds) */
12
+ t: number;
13
+ /** Held length (seconds) */
14
+ dur: number;
15
+ /** MIDI pitch (0 for unpitched drums) */
16
+ m: number;
17
+ /** 0-1 */
18
+ vel: number;
19
+ /** Glides: [seconds after the start, target MIDI pitch] */
20
+ bends?: [number, number][];
21
+ /** The track's instrument parameters */
22
+ params?: Readonly<Record<string, number>>;
23
+ /** Bowed strings: the desk (players) of the track, 0 for the first (see deskList in compile.ts) */
24
+ desk?: number;
25
+ }
26
+
27
+ /** Play one note */
28
+ export function playNote(name: string, c: NoteCall): void {
29
+ const def = getInstrument(name);
30
+ const amp = Math.pow(c.vel, 1.4) * def.level;
31
+ if (def.kind === 'live') {
32
+ def.voice({ ctx: c.ctx, out: c.out, t: c.t, dur: c.dur, f: midiToFrequency(c.m), vel: amp, touch: c.vel, bends: c.bends?.map(([at, m]) => [at, midiToFrequency(m)]), o: c.params ?? {} });
33
+ return;
34
+ }
35
+ if (def.bow) {
36
+ playBow(def, name, c, amp);
37
+ return;
38
+ }
39
+ const variant = Math.floor(Math.random() * (def.variants ?? 1));
40
+ const b = getSample(c.ctx, def, name, c.m, variant);
41
+ const src = c.ctx.createBufferSource();
42
+ src.buffer = b;
43
+ // A fixed-pitch drum given a different note is tuned by changing the playback rate (taiko, tom and others are already rendered by pitch and never reach here)
44
+ if (def.fixed !== undefined && c.m && c.m !== def.fixed && (c.params?.tune ?? 0) > 0) src.playbackRate.value = Math.pow(2, (c.m - def.fixed) / 12);
45
+ const g = c.ctx.createGain();
46
+ if (def.loop) {
47
+ src.loop = true;
48
+ const a = c.params?.attack ?? 1.5;
49
+ const r = c.params?.release ?? 1.5;
50
+ g.gain.setValueAtTime(0, c.t);
51
+ g.gain.linearRampToValueAtTime(amp, c.t + a);
52
+ g.gain.setValueAtTime(amp, c.t + Math.max(a, c.dur));
53
+ g.gain.linearRampToValueAtTime(0, c.t + Math.max(a, c.dur) + r);
54
+ src.connect(g).connect(c.out);
55
+ src.start(c.t, (c.t * 7.3) % b.duration);
56
+ src.stop(c.t + Math.max(a, c.dur) + r + 0.05);
57
+ return;
58
+ }
59
+ g.gain.value = amp;
60
+ if (def.choke) choke(c, def.choke, g.gain);
61
+ let out: AudioNode = c.out;
62
+ if (def.spread) {
63
+ const pan = c.ctx.createStereoPanner();
64
+ pan.pan.value = Math.max(-1, Math.min(1, (c.m - 64) / 36)) * def.spread;
65
+ pan.connect(c.out);
66
+ out = pan;
67
+ }
68
+ if (def.tone) {
69
+ const lp = c.ctx.createBiquadFilter();
70
+ lp.type = 'lowpass';
71
+ lp.frequency.value = Math.min(c.ctx.sampleRate * 0.45, def.tone(c.m, c.vel));
72
+ // The Web Audio low-pass Q is the resonance in decibels: -3 dB is roughly Butterworth, with no nasal tone
73
+ lp.Q.value = -3;
74
+ src.connect(lp).connect(g).connect(out);
75
+ } else src.connect(g).connect(out);
76
+ src.start(c.t);
77
+ const r = c.params?.release ?? def.damper?.(c.m) ?? def.release ?? 0.2;
78
+ if (def.hold === 'damp' && !(c.params?.ring ?? 0) && r !== Infinity) {
79
+ const off = c.t + Math.max(def.minHold ?? 0.03, c.dur);
80
+ g.gain.setValueAtTime(amp, off);
81
+ if (def.afterRing) {
82
+ // The damper stops the string quickly, but a faint ring stays a moment longer
83
+ const [level, fade] = def.afterRing;
84
+ g.gain.setTargetAtTime(amp * level, off, r / 4);
85
+ g.gain.setTargetAtTime(1e-4, off + r, fade / 6.9);
86
+ src.stop(Math.min(off + r + fade, c.t + b.duration));
87
+ } else {
88
+ g.gain.setTargetAtTime(1e-4, off, r / 4);
89
+ src.stop(Math.min(off + r * 1.5, c.t + b.duration));
90
+ }
91
+ } else src.stop(c.t + b.duration);
92
+ }
93
+
94
+ /** The gain of the note last started in each choke group, per audio context */
95
+ const chokeGroups = new WeakMap<BaseAudioContext, Map<string, AudioParam>>();
96
+
97
+ /** Cut off the note still ringing in a choke group as this one starts (notes are scheduled in time order), and make this one the group's */
98
+ function choke(c: NoteCall, group: string, gain: AudioParam): void {
99
+ let groups = chokeGroups.get(c.ctx);
100
+ if (!groups) chokeGroups.set(c.ctx, (groups = new Map()));
101
+ const ringing = groups.get(group);
102
+ if (ringing) {
103
+ // The target of setTargetAtTime cannot be 0 (some implementations diverge): fade toward silence, then close
104
+ ringing.cancelScheduledValues(c.t);
105
+ ringing.setTargetAtTime(1e-4, c.t, 0.004);
106
+ ringing.setValueAtTime(0, c.t + 0.04);
107
+ }
108
+ groups.set(group, gain);
109
+ }
110
+
111
+ /** Bowed strings: pick the soft or hard layer, make up pitch with a rate change, loop the sustain section, and lift the bow when the note ends */
112
+ function playBow(def: SampledInstrument, name: string, c: NoteCall, amp: number): void {
113
+ const bw = def.bow!;
114
+ const o = c.params ?? {};
115
+ let loud = c.vel * (o.bright ?? 1) > bw.loudAt ? 1 : 0;
116
+ const root = sampleMidi(def, c.m);
117
+ // If this layer is not prepared but the other one is: use the other for now, rather than rendering during playback (that would stall);
118
+ // a desk with neither prepared borrows the first desk's players
119
+ const has = (layer: number, desk: number) => hasSample(c.ctx.sampleRate, def, name, c.m, layer, desk);
120
+ const seat = DESKS[(c.desk ?? 0) % DESKS.length];
121
+ let desk = DESKS.indexOf(seat);
122
+ if (desk && !has(0, desk) && !has(1, desk) && (has(0, 0) || has(1, 0))) desk = 0;
123
+ if (!has(loud, desk) && has(1 - loud, desk)) loud = 1 - loud;
124
+ const b = getSample(c.ctx, def, name, c.m, loud, desk);
125
+ const src = c.ctx.createBufferSource();
126
+ src.buffer = b;
127
+ const lp = loopOf(b);
128
+ if (lp) {
129
+ src.loop = true;
130
+ src.loopStart = lp[0];
131
+ src.loopEnd = lp[1];
132
+ }
133
+ const rate = src.playbackRate;
134
+ rate.setValueAtTime(Math.pow(2, (c.m - root) / 12), c.t);
135
+ const glide = o.glide ?? bw.glide;
136
+ if (c.bends) for (const [at, m] of c.bends) rate.setTargetAtTime(Math.pow(2, (m - root) / 12), c.t + at, glide / 3);
137
+ const a = Math.max(0.005, o.attack ?? bw.attack);
138
+ const r = o.release ?? bw.release;
139
+ const off = c.t + Math.max(c.dur, 0.06);
140
+ // A desk after the first lands its bow a little behind the first desk's, by a different amount every note; it lifts it with the others
141
+ const t = c.t + (seat.lag && seat.lag * (0.5 + Math.random()));
142
+ const g = c.ctx.createGain();
143
+ const p = g.gain;
144
+ p.setValueAtTime(0, t);
145
+ p.linearRampToValueAtTime(amp, t + Math.min(a, Math.max(0.005, off - t)));
146
+ p.setValueAtTime(amp, off);
147
+ p.setTargetAtTime(1e-4, off, r / 4);
148
+ src.connect(g).connect(c.out);
149
+ src.start(t);
150
+ src.stop(off + r * 1.6 + 0.02);
151
+ }
@@ -0,0 +1,100 @@
1
+ // The instrument registry: every name a track can use, built-in or registered by the application.
2
+
3
+ import { BUILT_IN, type BuiltInInstrument } from './builtin.ts';
4
+ import type { InstrumentDef } from './definition.ts';
5
+ import { forgetSamples } from './samples.ts';
6
+
7
+ /** An instrument name. Built-in names autocomplete; any registered name is accepted too. */
8
+ export type InstrumentName = BuiltInInstrument | (string & {});
9
+
10
+ /** One note handed to a custom synthesized instrument */
11
+ export interface SynthNote {
12
+ context: BaseAudioContext;
13
+ /** Connect the note's output here */
14
+ destination: AudioNode;
15
+ /** Start time (context seconds) */
16
+ time: number;
17
+ /** How long the note is held (seconds); the release comes after this */
18
+ duration: number;
19
+ /** Frequency (Hz) */
20
+ frequency: number;
21
+ /** Loudness 0-1, already shaped by the instrument's level */
22
+ velocity: number;
23
+ /** Glides: [seconds after the start, target frequency in Hz] */
24
+ glides: [number, number][];
25
+ /** The track's `params`, for instrument-specific settings */
26
+ params: Readonly<Record<string, number>>;
27
+ }
28
+
29
+ /** A custom instrument rendered offline, once per pitch, then played back from a buffer */
30
+ export interface CustomSampledInstrument {
31
+ /** Render one note: mono, or [left, right] */
32
+ render(sampleRate: number, midi: number): Float32Array | [Float32Array, Float32Array];
33
+ /** 'ring' (default) plays the whole sample; 'damp' fades it out `release` seconds after the note ends */
34
+ sustain?: 'ring' | 'damp';
35
+ /** Release time in seconds for 'damp' (default 0.2) */
36
+ release?: number;
37
+ /** Output level (default 1) */
38
+ level?: number;
39
+ }
40
+
41
+ /** A custom instrument synthesized live, note by note */
42
+ export interface CustomSynthInstrument {
43
+ /** Build the Web Audio graph for one note */
44
+ play(note: SynthNote): void;
45
+ /** Output level (default 1) */
46
+ level?: number;
47
+ /** Supports glides between notes (the `>` marker in a melody) */
48
+ glide?: boolean;
49
+ }
50
+
51
+ export type CustomInstrument = CustomSampledInstrument | CustomSynthInstrument;
52
+
53
+ const registry = new Map<string, InstrumentDef>(Object.entries(BUILT_IN));
54
+
55
+ /**
56
+ * Add an instrument (or replace one) under a name tracks can use.
57
+ *
58
+ * @example
59
+ * registerInstrument('beep', {
60
+ * play({ context, destination, time, duration, frequency, velocity }) {
61
+ * const osc = new OscillatorNode(context, { type: 'square', frequency });
62
+ * const amp = new GainNode(context, { gain: velocity * 0.2 });
63
+ * osc.connect(amp).connect(destination);
64
+ * osc.start(time);
65
+ * osc.stop(time + duration);
66
+ * },
67
+ * });
68
+ */
69
+ export function registerInstrument(name: string, instrument: CustomInstrument): void {
70
+ forgetSamples(name);
71
+ if ('play' in instrument) {
72
+ registry.set(name, {
73
+ kind: 'live',
74
+ level: instrument.level ?? 1,
75
+ glide: instrument.glide ?? false,
76
+ voice: (c) =>
77
+ instrument.play({ context: c.ctx, destination: c.out, time: c.t, duration: c.dur, frequency: c.f, velocity: c.vel, glides: c.bends ?? [], params: c.o }),
78
+ });
79
+ } else {
80
+ registry.set(name, {
81
+ kind: 'sample',
82
+ render: (sampleRate, midi) => instrument.render(sampleRate, midi),
83
+ hold: instrument.sustain ?? 'ring',
84
+ release: instrument.release ?? 0.2,
85
+ level: instrument.level ?? 1,
86
+ });
87
+ }
88
+ }
89
+
90
+ /** Every instrument name currently available */
91
+ export function instrumentNames(): string[] {
92
+ return [...registry.keys()];
93
+ }
94
+
95
+ /** Look up an instrument; throws a helpful error for unknown names */
96
+ export function getInstrument(name: string): InstrumentDef {
97
+ const def = registry.get(name);
98
+ if (!def) throw new Error(`Unknown instrument "${name}". Available: ${instrumentNames().join(', ')}`);
99
+ return def;
100
+ }
@@ -0,0 +1,119 @@
1
+ // Sample cache: sampled instruments are rendered once per (sample rate, instrument, pitch, variant) and reused.
2
+ // Rendering happens before a song starts (prepareSamples), in small slices so the main thread never stalls.
3
+
4
+ import type { SampledInstrument } from './definition.ts';
5
+
6
+ /** A sample a song needs rendered before it plays */
7
+ export interface SampleRequest {
8
+ def: SampledInstrument;
9
+ name: string;
10
+ midi: number;
11
+ /** Only this layer (bowed strings pick soft or hard by velocity) */
12
+ layer?: number;
13
+ /** Bowed strings: the desk (players) of the track that plays it, 0 for the first */
14
+ desk?: number;
15
+ }
16
+
17
+ const cache = new Map<string, AudioBuffer>();
18
+ /** Loop section of a bowed-string sample (seconds) */
19
+ const loops = new WeakMap<AudioBuffer, [number, number]>();
20
+
21
+ /** Bowed samples share a cache entry when one instrument borrows another's players (strPad uses strings) */
22
+ const sampleName = (def: SampledInstrument, name: string) => def.bow?.name ?? name;
23
+ const keyOf = (sampleRate: number, name: string, midi: number, variant: number, desk: number) => `${sampleRate}|${name}|${midi}|${variant}${desk ? `|${desk}` : ''}`;
24
+
25
+ /** The pitch actually rendered: bowed strings sample every few semitones, fixed-pitch drums always use one note */
26
+ export function sampleMidi(def: SampledInstrument, midi: number): number {
27
+ if (def.bow) return Math.round(midi / def.bow.zone) * def.bow.zone;
28
+ return def.fixed ?? Math.round(midi);
29
+ }
30
+
31
+ /** How much memory bowed-string samples may take at most (bytes): beyond that the least recently used are dropped (notes already playing still hold a reference, so they are unaffected) */
32
+ const BOW_BUDGET = 48e6;
33
+ const bowUse = new Map<string, number>();
34
+ let bowBytes = 0;
35
+ function keepBow(key: string, buffer: AudioBuffer): void {
36
+ if (!bowUse.has(key)) bowBytes += buffer.length * 4;
37
+ bowUse.delete(key);
38
+ bowUse.set(key, buffer.length * 4);
39
+ for (const [old, n] of bowUse) {
40
+ if (bowBytes <= BOW_BUDGET) break;
41
+ bowUse.delete(old);
42
+ cache.delete(old);
43
+ bowBytes -= n;
44
+ }
45
+ }
46
+
47
+ /** Which layer a bowed string uses at this velocity (times params.bright); near the threshold both layers are prepared (velocity has a little randomness at playback) */
48
+ export function bowLayers(def: SampledInstrument, velocity: number, bright = 1): number[] {
49
+ const x = velocity * bright;
50
+ const at = def.bow!.loudAt;
51
+ if (Math.abs(x - at) < 0.06) return [0, 1];
52
+ return [x > at ? 1 : 0];
53
+ }
54
+
55
+ /** How many variants of a sampled instrument to precompute (random variants for drums; the soft and hard layers for bowed strings) */
56
+ export const variantsOf = (def: SampledInstrument): number => (def.bow ? 2 : (def.variants ?? 1));
57
+
58
+ /** Only bowed strings have desks (different players for tracks that share an instrument) */
59
+ const deskOf = (def: SampledInstrument, desk: number) => (def.bow ? desk : 0);
60
+
61
+ /** Whether a sample is already rendered */
62
+ export function hasSample(sampleRate: number, def: SampledInstrument, name: string, midi: number, variant: number, desk = 0): boolean {
63
+ return cache.has(keyOf(sampleRate, sampleName(def, name), sampleMidi(def, midi), variant, deskOf(def, desk)));
64
+ }
65
+
66
+ /** The loop section (seconds) of a bowed sample, if it has one */
67
+ export const loopOf = (buffer: AudioBuffer): [number, number] | undefined => loops.get(buffer);
68
+
69
+ /** Render (or fetch from the cache) a sample */
70
+ export function getSample(ctx: BaseAudioContext, def: SampledInstrument, name: string, midi: number, variant = 0, desk = 0): AudioBuffer {
71
+ const mm = sampleMidi(def, midi);
72
+ const v = variant % variantsOf(def);
73
+ const dk = deskOf(def, desk);
74
+ const k = keyOf(ctx.sampleRate, sampleName(def, name), mm, v, dk);
75
+ let b = cache.get(k);
76
+ if (!b) {
77
+ const sr = def.bow ? Math.round(ctx.sampleRate * def.bow.rate) : ctx.sampleRate;
78
+ const data = def.render(sr, mm, v, dk);
79
+ const chans = data instanceof Float32Array ? [data] : Array.isArray(data) ? data : [data.data];
80
+ // One NaN would circulate through the reverb and delay feedback and silence the whole mix for good
81
+ for (const d of chans) for (let i = 0; i < d.length; i++) if (!Number.isFinite(d[i])) d[i] = 0;
82
+ b = ctx.createBuffer(chans.length, chans[0].length, sr);
83
+ chans.forEach((d, i) => b!.getChannelData(i).set(d));
84
+ if (!(data instanceof Float32Array) && !Array.isArray(data)) loops.set(b, [data.loop[0] / sr, data.loop[1] / sr]);
85
+ cache.set(k, b);
86
+ }
87
+ if (def.bow) keepBow(k, b);
88
+ return b;
89
+ }
90
+
91
+ /** Drop every cached sample of an instrument (after it is redefined) */
92
+ export function forgetSamples(name: string): void {
93
+ for (const k of [...cache.keys()]) if (k.split('|')[1] === name) cache.delete(k);
94
+ }
95
+
96
+ /** Every [variant] of a need that should be rendered */
97
+ function* variantsToRender(def: SampledInstrument, layer: number | undefined): Generator<number> {
98
+ for (let v = 0; v < variantsOf(def); v++) if (layer === undefined || v === layer) yield v;
99
+ }
100
+
101
+ /** Render samples ahead of time; after more than 12 ms of continuous work, yield the main thread for a moment (so the screen does not stall) */
102
+ export async function prepareSamples(ctx: BaseAudioContext, requests: Iterable<SampleRequest>): Promise<void> {
103
+ let last = performance.now();
104
+ for (const { def, name, midi, layer, desk } of requests) {
105
+ for (const v of variantsToRender(def, layer)) {
106
+ if (hasSample(ctx.sampleRate, def, name, midi, v, desk)) continue;
107
+ getSample(ctx, def, name, midi, v, desk);
108
+ if (performance.now() - last > 12) {
109
+ await new Promise((r) => setTimeout(r, 0));
110
+ last = performance.now();
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ /** Synchronous version (offline rendering, short play-once jingles) */
117
+ export function prepareSamplesSync(ctx: BaseAudioContext, requests: Iterable<SampleRequest>): void {
118
+ for (const { def, name, midi, layer, desk } of requests) for (const v of variantsToRender(def, layer)) getSample(ctx, def, name, midi, v, desk);
119
+ }