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
package/src/theory.ts ADDED
@@ -0,0 +1,208 @@
1
+ // Music theory: note names, modes, chord symbols, voicing and mode remapping. Pure functions only.
2
+
3
+ /** A pitch: a note name such as 'C4', 'F#3', 'Bb5', or a MIDI number (C4 = 60) */
4
+ export type Pitch = string | number;
5
+
6
+ /** Note name (or MIDI number) to MIDI: C4 = 60. Supports #, b, ##, bb */
7
+ export function midi(pitch: Pitch): number {
8
+ if (typeof pitch === 'number') return pitch;
9
+ const m = /^([A-Ga-g])(##|bb|#|b)?(-?\d)$/.exec(pitch.trim());
10
+ if (!m) throw new Error(`Unrecognized note name: ${pitch}`);
11
+ const base = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 }[m[1].toUpperCase() as 'C'];
12
+ const acc = m[2] === '#' ? 1 : m[2] === '##' ? 2 : m[2] === 'b' ? -1 : m[2] === 'bb' ? -2 : 0;
13
+ return 12 * (Number(m[3]) + 1) + base + acc;
14
+ }
15
+
16
+ /** Frequency in Hz (A4 = 440) */
17
+ export const frequency = (pitch: Pitch): number => 440 * Math.pow(2, (midi(pitch) - 69) / 12);
18
+
19
+ const NAMES = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B'];
20
+ /** MIDI to a note name: 61 → 'C#4' */
21
+ export const noteName = (m: number): string => `${NAMES[((m % 12) + 12) % 12]}${Math.floor(m / 12) - 1}`;
22
+
23
+ /** Modes and scales (semitones relative to the tonic) */
24
+ export const MODES = {
25
+ ionian: [0, 2, 4, 5, 7, 9, 11],
26
+ dorian: [0, 2, 3, 5, 7, 9, 10],
27
+ phrygian: [0, 1, 3, 5, 7, 8, 10],
28
+ lydian: [0, 2, 4, 6, 7, 9, 11],
29
+ mixolydian: [0, 2, 4, 5, 7, 9, 10],
30
+ aeolian: [0, 2, 3, 5, 7, 8, 10],
31
+ locrian: [0, 1, 3, 5, 6, 8, 10],
32
+ harmonicMinor: [0, 2, 3, 5, 7, 8, 11],
33
+ melodicMinor: [0, 2, 3, 5, 7, 9, 11],
34
+ /** Phrygian dominant scale (Spanish / Middle Eastern flavour) */
35
+ phrygianDom: [0, 1, 4, 5, 7, 8, 10],
36
+ /** Hungarian minor (the Gypsy flavour of the augmented fourth) */
37
+ hungarianMinor: [0, 2, 3, 6, 7, 8, 11],
38
+ lydianDom: [0, 2, 4, 6, 7, 9, 10],
39
+ /** Double harmonic (Byzantine) scale */
40
+ doubleHarmonic: [0, 1, 4, 5, 7, 8, 11],
41
+ majorPent: [0, 2, 4, 7, 9],
42
+ minorPent: [0, 3, 5, 7, 10],
43
+ /** Japanese miyako-bushi (the In scale), used only to give the Rift an alien feel */
44
+ inSen: [0, 1, 5, 7, 10],
45
+ wholeTone: [0, 2, 4, 6, 8, 10],
46
+ /** Half-whole diminished scale */
47
+ octatonic: [0, 1, 3, 4, 6, 7, 9, 10],
48
+ } as const satisfies Record<string, readonly number[]>;
49
+ export type Mode = keyof typeof MODES;
50
+
51
+ /** Chord qualities (semitones relative to the root; slash-chord bass not included) */
52
+ const QUALITIES: Record<string, number[]> = {
53
+ '': [0, 4, 7],
54
+ maj: [0, 4, 7],
55
+ m: [0, 3, 7],
56
+ dim: [0, 3, 6],
57
+ aug: [0, 4, 8],
58
+ '+': [0, 4, 8],
59
+ sus2: [0, 2, 7],
60
+ sus4: [0, 5, 7],
61
+ sus: [0, 5, 7],
62
+ '5': [0, 7],
63
+ '6': [0, 4, 7, 9],
64
+ m6: [0, 3, 7, 9],
65
+ '7': [0, 4, 7, 10],
66
+ maj7: [0, 4, 7, 11],
67
+ m7: [0, 3, 7, 10],
68
+ mmaj7: [0, 3, 7, 11],
69
+ m7b5: [0, 3, 6, 10],
70
+ dim7: [0, 3, 6, 9],
71
+ '7sus4': [0, 5, 7, 10],
72
+ add9: [0, 4, 7, 14],
73
+ madd9: [0, 3, 7, 14],
74
+ '9': [0, 4, 7, 10, 14],
75
+ maj9: [0, 4, 7, 11, 14],
76
+ m9: [0, 3, 7, 10, 14],
77
+ '7b9': [0, 4, 7, 10, 13],
78
+ '7#9': [0, 4, 7, 10, 15],
79
+ m11: [0, 3, 7, 10, 14, 17],
80
+ '11': [0, 7, 10, 14, 17],
81
+ maj7s11: [0, 4, 7, 11, 18],
82
+ '6/9': [0, 4, 7, 9, 14],
83
+ /** Stacked fourths (ethereal, floating) */
84
+ q: [0, 5, 10],
85
+ q4: [0, 5, 10, 15],
86
+ };
87
+
88
+ /** A parsed chord symbol */
89
+ export interface ChordInfo {
90
+ symbol: string;
91
+ /** Root pitch class 0-11 */
92
+ root: number;
93
+ /** Semitones relative to the root (sorted, may be > 12) */
94
+ intervals: number[];
95
+ /** Bass pitch class (differs from the root for slash chords) */
96
+ bass: number;
97
+ /** Third: minor third 3, major third 4, no third 0 (used when a bass line writes "3") */
98
+ third: number;
99
+ }
100
+
101
+ const PC: Record<string, number> = { C: 0, D: 2, E: 4, F: 5, G: 7, A: 9, B: 11 };
102
+ const pcOf = (s: string): number => {
103
+ const m = /^([A-G])(#|b)?$/.exec(s);
104
+ if (!m) throw new Error(`Unrecognized root note: ${s}`);
105
+ return (PC[m[1]] + (m[2] === '#' ? 1 : m[2] === 'b' ? -1 : 0) + 12) % 12;
106
+ };
107
+
108
+ /** Parse a chord symbol: Am7, F#m7b5, Bb/D, Gsus4, C5, Ebmaj7s11, Dq4... */
109
+ export function parseChord(symbol: string): ChordInfo {
110
+ const m = /^([A-G](?:#|b)?)([^/]*)(?:\/([A-G](?:#|b)?))?$/.exec(symbol.trim());
111
+ if (!m) throw new Error(`Unrecognized chord: ${symbol}`);
112
+ const root = pcOf(m[1]);
113
+ const q = m[2];
114
+ const iv = QUALITIES[q];
115
+ if (!iv) throw new Error(`Unrecognized chord quality "${q}" (${symbol})`);
116
+ const third = iv.includes(4) ? 4 : iv.includes(3) ? 3 : 0;
117
+ return { symbol, root, intervals: [...iv], bass: m[3] ? pcOf(m[3]) : root, third };
118
+ }
119
+
120
+ export interface VoicingOptions {
121
+ /** The register to center the chord around (default C4) */
122
+ center?: Pitch;
123
+ /** The previous voicing: pick the inversion that moves the least from it */
124
+ previous?: number[];
125
+ /** Number of notes (default: every chord tone, at least 3) */
126
+ size?: number;
127
+ }
128
+
129
+ /** Voice a chord in close position around a center; with a previous voicing, pick the smoothest inversion */
130
+ export function voiceChord(chord: ChordInfo, { center = 60, previous, size }: VoicingOptions = {}): number[] {
131
+ const mid0 = midi(center);
132
+ const pcs = chord.intervals.map((i) => (chord.root + i) % 12);
133
+ const uniq = [...new Set(pcs)];
134
+ const n = size ?? Math.max(3, uniq.length);
135
+ // List every inversion: starting from the first note above center-9 or center-3, stack upward
136
+ const cands: number[][] = [];
137
+ for (let inv = 0; inv < uniq.length; inv++) {
138
+ for (const lo of [mid0 - 9, mid0 - 3]) {
139
+ const out: number[] = [];
140
+ let p = lo;
141
+ for (let k = 0; k < n; k++) {
142
+ const pc = uniq[(inv + k) % uniq.length];
143
+ while (((p % 12) + 12) % 12 !== pc) p++;
144
+ out.push(p);
145
+ p++;
146
+ }
147
+ cands.push(out);
148
+ }
149
+ }
150
+ const score = (v: number[]): number => {
151
+ const mid = (v[0] + v[v.length - 1]) / 2;
152
+ let s = Math.abs(mid - mid0) * 0.6;
153
+ if (previous && previous.length) {
154
+ for (let i = 0; i < v.length; i++) s += Math.abs(v[i] - previous[Math.min(i, previous.length - 1)]);
155
+ }
156
+ return s;
157
+ };
158
+ cands.sort((a, b) => score(a) - score(b));
159
+ return cands[0];
160
+ }
161
+
162
+ /** The note at a degree of a mode (0-based, may be negative or exceed 7) above a tonic */
163
+ export function scaleDegree(tonic: Pitch, mode: Mode, degree: number): number {
164
+ const sc = MODES[mode];
165
+ const n = sc.length;
166
+ const oct = Math.floor(degree / n);
167
+ const i = ((degree % n) + n) % n;
168
+ return midi(tonic) + oct * 12 + sc[i];
169
+ }
170
+
171
+ /**
172
+ * Mode remapping: map a note written on C's natural notes (white keys) to the same scale degree of tonic + mode.
173
+ * Accidentals are kept as a "semitone offset", so a C major melody can be written once and dropped into Phrygian or harmonic minor.
174
+ * base is the written tonic (by default the C at C4 = 60).
175
+ */
176
+ export function remapToMode(m: number, tonic: Pitch, mode: Mode, base: Pitch = 60): number {
177
+ const WHITE = [0, 2, 4, 5, 7, 9, 11];
178
+ const rel = m - midi(base);
179
+ const oct = Math.floor(rel / 12);
180
+ const pc = ((rel % 12) + 12) % 12;
181
+ let idx = WHITE.indexOf(pc);
182
+ let off = 0;
183
+ if (idx < 0) {
184
+ idx = WHITE.indexOf(pc - 1);
185
+ off = 1;
186
+ }
187
+ const t = midi(tonic);
188
+ const sc: readonly number[] = MODES[mode];
189
+ if (sc.length === 7) return t + oct * 12 + sc[idx] + off;
190
+ // Pentatonic and other non-heptatonic scales: take the nearest scale note
191
+ const target = MODES.ionian[idx];
192
+ let best = sc[0];
193
+ for (const s of sc) if (Math.abs(s - target) < Math.abs(best - target)) best = s;
194
+ return t + oct * 12 + best + off;
195
+ }
196
+
197
+ /** Roman-numeral chord in a key, as a chord symbol: romanChord('bVII', 'D4', 'aeolian') → 'C' ('i', 'IV', 'bVII', 'v7', 'ii°') */
198
+ export function romanChord(numeral: string, tonic: Pitch, mode: Mode): string {
199
+ const m = /^(b|#)?(iv|vi{0,2}|i{1,3}|IV|VI{0,2}|I{1,3})(°|\+)?(.*)$/.exec(numeral);
200
+ if (!m) throw new Error(`Unrecognized scale degree: ${numeral}`);
201
+ const ROM = ['i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii'];
202
+ const deg = ROM.indexOf(m[2].toLowerCase());
203
+ const sc: readonly number[] = MODES[mode];
204
+ const pc = (midi(tonic) + sc[deg % sc.length] + (m[1] === 'b' ? -1 : m[1] === '#' ? 1 : 0) + 120) % 12;
205
+ const minor = m[2] === m[2].toLowerCase();
206
+ const q = m[3] === '°' ? 'dim' : m[3] === '+' ? 'aug' : minor ? 'm' : '';
207
+ return NAMES[pc] + q + (m[4] ?? '');
208
+ }