@libraz/libcantus 0.9.3 → 0.9.5

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 (201) hide show
  1. package/README.md +29 -20
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +39 -1947
  4. package/dist/analyze/index.cjs.map +1 -1
  5. package/dist/analyze/index.d.cts +161 -91
  6. package/dist/analyze/index.d.ts +161 -91
  7. package/dist/analyze/index.js +19 -8
  8. package/dist/chunk-2A2QMA74.cjs +261 -0
  9. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  10. package/dist/chunk-2PTFGVD7.js +261 -0
  11. package/dist/chunk-2PTFGVD7.js.map +1 -0
  12. package/dist/chunk-357ZDPVU.cjs +162 -0
  13. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  14. package/dist/chunk-3KX76RLG.js +1548 -0
  15. package/dist/chunk-3KX76RLG.js.map +1 -0
  16. package/dist/chunk-4VZ5MZAM.cjs +889 -0
  17. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  18. package/dist/{chunk-C5XYKUVG.js → chunk-7EONYVI5.js} +471 -171
  19. package/dist/chunk-7EONYVI5.js.map +1 -0
  20. package/dist/{chunk-IT2O4XFE.js → chunk-7JEP3MFY.js} +42 -34
  21. package/dist/chunk-7JEP3MFY.js.map +1 -0
  22. package/dist/{chunk-4MHA432G.js → chunk-AID75RN3.js} +572 -250
  23. package/dist/chunk-AID75RN3.js.map +1 -0
  24. package/dist/chunk-BSLTLGM3.cjs +497 -0
  25. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  26. package/dist/chunk-CU6VG2YM.cjs +109 -0
  27. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  28. package/dist/{chunk-EH6EM37U.js → chunk-DZMHIGI4.js} +81 -59
  29. package/dist/chunk-DZMHIGI4.js.map +1 -0
  30. package/dist/{chunk-AKLYPWIN.js → chunk-EDOTEABK.js} +45 -19
  31. package/dist/chunk-EDOTEABK.js.map +1 -0
  32. package/dist/{chunk-2ZWMSJH6.js → chunk-HN2OEUN2.js} +7 -4
  33. package/dist/chunk-HN2OEUN2.js.map +1 -0
  34. package/dist/{chunk-MIWO425M.js → chunk-KBTTEX7Q.js} +128 -43
  35. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  36. package/dist/chunk-KQN2ZYND.cjs +724 -0
  37. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  38. package/dist/chunk-MPTVJIVG.cjs +3176 -0
  39. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  40. package/dist/chunk-MTMZTMWM.cjs +53 -0
  41. package/dist/chunk-MTMZTMWM.cjs.map +1 -0
  42. package/dist/{chunk-CHMXELDS.js → chunk-NNB67XA2.js} +372 -197
  43. package/dist/chunk-NNB67XA2.js.map +1 -0
  44. package/dist/chunk-OL3IHHSJ.cjs +1204 -0
  45. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  46. package/dist/chunk-PI3UXRWK.cjs +528 -0
  47. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  48. package/dist/chunk-R2VVFAIN.js +528 -0
  49. package/dist/chunk-R2VVFAIN.js.map +1 -0
  50. package/dist/chunk-T64C7ZD7.cjs +1 -0
  51. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  52. package/dist/chunk-WFDXOLUB.cjs +337 -0
  53. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  54. package/dist/chunk-WTAN4ACG.cjs +1548 -0
  55. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  56. package/dist/chunk-YFJXS3AU.cjs +33 -0
  57. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  58. package/dist/{chunk-GOAW4VYL.js → chunk-YUE6TJ4M.js} +315 -145
  59. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  60. package/dist/{chunk-62QZKCJQ.js → chunk-ZGLJM4A4.js} +10 -6
  61. package/dist/chunk-ZGLJM4A4.js.map +1 -0
  62. package/dist/core/index.cjs +140 -647
  63. package/dist/core/index.cjs.map +1 -1
  64. package/dist/core/index.d.cts +313 -41
  65. package/dist/core/index.d.ts +313 -41
  66. package/dist/core/index.js +64 -16
  67. package/dist/{function-DVH4Eusr.d.ts → function-CxixTB02.d.cts} +122 -10
  68. package/dist/{function-DYt4iKsn.d.cts → function-DTCt675z.d.ts} +122 -10
  69. package/dist/generate/index.cjs +28 -4085
  70. package/dist/generate/index.cjs.map +1 -1
  71. package/dist/generate/index.d.cts +374 -66
  72. package/dist/generate/index.d.ts +374 -66
  73. package/dist/generate/index.js +23 -9
  74. package/dist/{index-Jc0Ryi79.d.ts → index-B2Ulxxpo.d.cts} +21 -5
  75. package/dist/{index-CpOsP1y9.d.cts → index-BkYJ0xMq.d.cts} +18 -17
  76. package/dist/{index-CxqBQtH1.d.cts → index-BuaB-ypt.d.ts} +21 -5
  77. package/dist/index-CAN7v-kw.d.ts +199 -0
  78. package/dist/{index-Bt4tOUD9.d.ts → index-COw1Ky9g.d.ts} +93 -8
  79. package/dist/{index-Dikst44v.d.cts → index-Cfx8xgH0.d.ts} +14 -11
  80. package/dist/{index-DxvARIP2.d.ts → index-DRtIhlJj.d.ts} +18 -17
  81. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  82. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  83. package/dist/index-alrRninU.d.cts +199 -0
  84. package/dist/{index-BNlGP86A.d.cts → index-oTQbYYIs.d.cts} +93 -8
  85. package/dist/index-tR9jTn26.d.cts +275 -0
  86. package/dist/index-tR9jTn26.d.ts +275 -0
  87. package/dist/{index-Bg5dYMmZ.d.ts → index-x2vcsduE.d.cts} +14 -11
  88. package/dist/index.cjs +429 -7383
  89. package/dist/index.cjs.map +1 -1
  90. package/dist/index.d.cts +15 -15
  91. package/dist/index.d.ts +15 -15
  92. package/dist/index.js +121 -27
  93. package/dist/model/index.cjs +8 -2562
  94. package/dist/model/index.cjs.map +1 -1
  95. package/dist/model/index.d.cts +471 -81
  96. package/dist/model/index.d.ts +471 -81
  97. package/dist/model/index.js +6 -7
  98. package/dist/styled-Bl0ZK28h.d.ts +543 -0
  99. package/dist/styled-Crv6SA4S.d.cts +543 -0
  100. package/dist/theory/index.cjs +172 -1822
  101. package/dist/theory/index.cjs.map +1 -1
  102. package/dist/theory/index.d.cts +151 -233
  103. package/dist/theory/index.d.ts +151 -233
  104. package/dist/theory/index.js +30 -7
  105. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  106. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  107. package/package.json +34 -13
  108. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  109. package/dist/chunk-4MHA432G.js.map +0 -1
  110. package/dist/chunk-62QZKCJQ.js.map +0 -1
  111. package/dist/chunk-AKLYPWIN.js.map +0 -1
  112. package/dist/chunk-C5XYKUVG.js.map +0 -1
  113. package/dist/chunk-CHMXELDS.js.map +0 -1
  114. package/dist/chunk-DYWFNC4A.js +0 -152
  115. package/dist/chunk-DYWFNC4A.js.map +0 -1
  116. package/dist/chunk-EH6EM37U.js.map +0 -1
  117. package/dist/chunk-FBQK3ZQN.js +0 -131
  118. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  119. package/dist/chunk-GOAW4VYL.js.map +0 -1
  120. package/dist/chunk-I5R7ZBJS.js +0 -202
  121. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  122. package/dist/chunk-IT2O4XFE.js.map +0 -1
  123. package/dist/chunk-MIWO425M.js.map +0 -1
  124. package/dist/chunk-SGU7SPLK.js +0 -924
  125. package/dist/chunk-SGU7SPLK.js.map +0 -1
  126. package/dist/index-CGMZNCx6.d.cts +0 -140
  127. package/dist/index-CGMZNCx6.d.ts +0 -140
  128. package/dist/index-CnZCo29u.d.cts +0 -108
  129. package/dist/index-JnF4v9bx.d.ts +0 -108
  130. package/dist/styled-DZ7GjShb.d.ts +0 -282
  131. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  132. package/src/analyze/arrange/index.ts +0 -21
  133. package/src/analyze/arrange/internal.ts +0 -127
  134. package/src/analyze/arrange/tension.ts +0 -199
  135. package/src/analyze/arrange/tracks.ts +0 -312
  136. package/src/analyze/detect/index.ts +0 -272
  137. package/src/analyze/functional/borrowed.ts +0 -84
  138. package/src/analyze/functional/cadence.ts +0 -66
  139. package/src/analyze/functional/function.ts +0 -184
  140. package/src/analyze/functional/index.ts +0 -23
  141. package/src/analyze/functional/internal.ts +0 -29
  142. package/src/analyze/functional/roman.ts +0 -402
  143. package/src/analyze/index.ts +0 -49
  144. package/src/analyze/timeline/index.ts +0 -451
  145. package/src/analyze/voice/index.ts +0 -339
  146. package/src/core/event-index/index.ts +0 -104
  147. package/src/core/index.ts +0 -61
  148. package/src/core/interval/index.ts +0 -81
  149. package/src/core/meter/index.ts +0 -302
  150. package/src/core/pitch/index.ts +0 -311
  151. package/src/core/random/index.ts +0 -76
  152. package/src/core/tuning/index.ts +0 -186
  153. package/src/core/types.ts +0 -29
  154. package/src/core/validation/index.ts +0 -119
  155. package/src/generate/bass/index.ts +0 -373
  156. package/src/generate/countermelody/index.ts +0 -470
  157. package/src/generate/drums/beat.ts +0 -266
  158. package/src/generate/drums/euclid.ts +0 -65
  159. package/src/generate/drums/fills.ts +0 -356
  160. package/src/generate/drums/ghost.ts +0 -119
  161. package/src/generate/drums/hihat.ts +0 -285
  162. package/src/generate/drums/hit.ts +0 -31
  163. package/src/generate/drums/index.ts +0 -361
  164. package/src/generate/drums/internal.ts +0 -257
  165. package/src/generate/drums/kick.ts +0 -160
  166. package/src/generate/drums/percussion.ts +0 -121
  167. package/src/generate/drums/rng.ts +0 -7
  168. package/src/generate/drums/swing.ts +0 -71
  169. package/src/generate/groove/index.ts +0 -351
  170. package/src/generate/harmonize/index.ts +0 -451
  171. package/src/generate/index.ts +0 -58
  172. package/src/generate/motif/index.ts +0 -422
  173. package/src/generate/progression/index.ts +0 -362
  174. package/src/generate/reharmony/index.ts +0 -323
  175. package/src/generate/rhythm/index.ts +0 -175
  176. package/src/index.ts +0 -13
  177. package/src/model/chord.ts +0 -442
  178. package/src/model/index.ts +0 -15
  179. package/src/model/interval.ts +0 -82
  180. package/src/model/key.ts +0 -266
  181. package/src/model/note.ts +0 -176
  182. package/src/model/progression.ts +0 -161
  183. package/src/model/shared.ts +0 -12
  184. package/src/theory/chord/index.ts +0 -375
  185. package/src/theory/chordscale/index.ts +0 -374
  186. package/src/theory/counterpoint/index.ts +0 -230
  187. package/src/theory/harmony/index.ts +0 -77
  188. package/src/theory/index.ts +0 -112
  189. package/src/theory/safety/index.ts +0 -488
  190. package/src/theory/scale/degrees.ts +0 -123
  191. package/src/theory/scale/index.ts +0 -28
  192. package/src/theory/scale/key.ts +0 -56
  193. package/src/theory/scale/masks.ts +0 -153
  194. package/src/theory/spelling/index.ts +0 -249
  195. package/src/theory/symbol/index.ts +0 -268
  196. package/src/theory/voicing/index.ts +0 -5
  197. package/src/theory/voicing/internal.ts +0 -168
  198. package/src/theory/voicing/leading.ts +0 -112
  199. package/src/theory/voicing/satb.ts +0 -216
  200. package/src/theory/voicing/styled.ts +0 -199
  201. package/tsconfig.json +0 -23
@@ -1,351 +0,0 @@
1
- /**
2
- * Groove and humanization: nudging quantized note events toward a more human
3
- * feel, and capturing/replaying the timing-and-velocity "feel" of a
4
- * performance as a reusable groove template.
5
- *
6
- * Positions and durations are measured in quarter-note beats, matching the
7
- * library-wide convention. Randomness, where used, always goes through the
8
- * seeded PRNG from the `random` module so results are fully reproducible.
9
- */
10
-
11
- import {
12
- beatsPerBar,
13
- formatTimeSignature,
14
- metricWeight,
15
- type TimeSignature,
16
- } from '../../core/meter/index.js';
17
- import { createRng } from '../../core/random/index.js';
18
- import type { NoteEvent } from '../../core/types.js';
19
- import {
20
- assertGenerationBudget,
21
- assertNoteEvents,
22
- assertPositiveInt,
23
- assertRange,
24
- assertTimeSignature,
25
- } from '../../core/validation/index.js';
26
-
27
- /**
28
- * Options controlling {@link humanize}.
29
- *
30
- * @category Rhythm & Meter
31
- */
32
- export type HumanizeOptions = {
33
- /**
34
- * Time signature used to derive metric accents.
35
- *
36
- * @defaultValue 4/4
37
- */
38
- ts?: TimeSignature;
39
- /**
40
- * Maximum timing jitter in quarter-note beats, applied as ±this value.
41
- *
42
- * @defaultValue 0.02
43
- */
44
- timing?: number;
45
- /**
46
- * Maximum velocity jitter (MIDI units), applied as ±this value.
47
- *
48
- * @defaultValue 8
49
- */
50
- velocity?: number;
51
- /**
52
- * How much louder strong beats get relative to weak ones, in MIDI velocity units.
53
- *
54
- * @defaultValue 12
55
- */
56
- accent?: number;
57
- /**
58
- * Velocity assumed for events with no `velocity` of their own.
59
- *
60
- * @defaultValue 80
61
- */
62
- baseVelocity?: number;
63
- /**
64
- * Seed for the deterministic PRNG.
65
- *
66
- * @defaultValue 0
67
- */
68
- seed?: number;
69
- };
70
-
71
- /** Default time signature for {@link humanize} when none is given. */
72
- const DEFAULT_TS: TimeSignature = { numerator: 4, denominator: 4 };
73
- const DEFAULT_TIMING = 0.02;
74
- const DEFAULT_VELOCITY_JITTER = 8;
75
- const DEFAULT_ACCENT = 12;
76
- const DEFAULT_BASE_VELOCITY = 80;
77
- const MAX_METRIC_WEIGHT = 3;
78
- const MIN_VELOCITY = 1;
79
- const MAX_VELOCITY = 127;
80
-
81
- /**
82
- * Humanize a sequence of note events by nudging their timing and velocity
83
- * with small, deterministic (seeded) randomness plus a metric-accent shape:
84
- * events on strong beats (see {@link metricWeight}) come out louder on
85
- * average than events on weak ones.
86
- *
87
- * Each returned event is a copy; `startBeat` is jittered within
88
- * `[-timing, +timing]` beats and clamped to be non-negative, and `velocity`
89
- * is the event's own velocity (or `baseVelocity` if it has none) plus an
90
- * accent term scaled by the event's metric weight plus a jitter within
91
- * `[-velocity, +velocity]`, clamped to `[1, 127]` and rounded. The metric
92
- * weight is computed from each event's original (pre-jitter) `startBeat`.
93
- * `pitch` and `durationBeat` pass through unchanged, and the output keeps the
94
- * input order.
95
- *
96
- * @param events The events to humanize.
97
- * @param opts Humanization options.
98
- * @returns Humanized copies of `events`, in input order.
99
- * @example
100
- * ```ts
101
- * import { humanize } from '@libraz/libcantus';
102
- * const events = [{ pitch: 60, startBeat: 0, durationBeat: 1, velocity: 80 }];
103
- * humanize(events, { seed: 1, timing: 0.03 }); // copies with jittered timing and accented velocity
104
- * ```
105
- * @category Rhythm & Meter
106
- */
107
- export function humanize(events: NoteEvent[], opts: HumanizeOptions = {}): NoteEvent[] {
108
- const ts = opts.ts ?? DEFAULT_TS;
109
- assertTimeSignature(ts);
110
- assertNoteEvents(events, 'humanize events');
111
- const timing = opts.timing ?? DEFAULT_TIMING;
112
- const velocityJitter = opts.velocity ?? DEFAULT_VELOCITY_JITTER;
113
- const accent = opts.accent ?? DEFAULT_ACCENT;
114
- const baseVelocity = opts.baseVelocity ?? DEFAULT_BASE_VELOCITY;
115
- assertRange(timing, 0, Number.MAX_SAFE_INTEGER, 'humanize timing');
116
- assertRange(velocityJitter, 0, 127, 'humanize velocity jitter');
117
- assertRange(accent, 0, 127, 'humanize accent');
118
- assertRange(baseVelocity, 0, 127, 'humanize base velocity');
119
- const rng = createRng(opts.seed ?? 0);
120
-
121
- return events.map((event) => {
122
- const timingOffset = rng.float(-timing, timing);
123
- const startBeat = Math.max(0, event.startBeat + timingOffset);
124
-
125
- const weight = metricWeight(event.startBeat, ts);
126
- const accentBoost = (weight / MAX_METRIC_WEIGHT) * accent;
127
- const velocityOffset = rng.float(-velocityJitter, velocityJitter);
128
- const rawVelocity = (event.velocity ?? baseVelocity) + accentBoost + velocityOffset;
129
- const velocity = Math.min(MAX_VELOCITY, Math.max(MIN_VELOCITY, Math.round(rawVelocity)));
130
-
131
- return {
132
- pitch: event.pitch,
133
- startBeat,
134
- durationBeat: event.durationBeat,
135
- velocity,
136
- };
137
- });
138
- }
139
-
140
- /**
141
- * The recorded feel at a single grid slot: an average timing deviation and velocity.
142
- *
143
- * @category Rhythm & Meter
144
- */
145
- export type GrooveSlot = {
146
- /** Average `actualStartBeat - quantizedBeat` (in quarter-note beats) for events landing on this slot. */
147
- timingOffset: number;
148
- /**
149
- * Average velocity of events landing on this slot, or `null` if no event
150
- * with a velocity did. `null` (not 0) is the "unrecorded" sentinel so that a
151
- * genuine velocity of 0 is preserved and applied like any other value.
152
- */
153
- velocity: number | null;
154
- };
155
-
156
- /**
157
- * A per-bar grid of timing and velocity deviations captured from a performance.
158
- *
159
- * @category Rhythm & Meter
160
- */
161
- export type GrooveTemplate = {
162
- /** Grid resolution: equal grid steps per quarter-note beat. */
163
- subdivision: number;
164
- /** Number of grid slots per bar (`beatsPerBar(ts) * subdivision`). */
165
- slotsPerBar: number;
166
- /** One entry per grid slot, in slot-index order (slot 0 is the downbeat). */
167
- slots: GrooveSlot[];
168
- /**
169
- * The time signature the template was extracted under. {@link applyGrooveTemplate}
170
- * requires the apply-time meter to match this so the per-bar grid lines up
171
- * instead of silently drifting. Optional for backward compatibility with
172
- * templates constructed without it (in which case no check is performed).
173
- */
174
- ts?: TimeSignature;
175
- };
176
-
177
- /**
178
- * Quantize a beat position to the nearest grid slot at the given subdivision,
179
- * within the bar that contains it.
180
- *
181
- * @param beat Absolute position in quarter-note beats.
182
- * @param barBeats Bar length in quarter-note beats.
183
- * @param subdivision Grid steps per quarter-note beat.
184
- * @param slotsPerBar Number of grid slots per bar.
185
- * @returns The quantized absolute beat and its 0-based slot index within the bar.
186
- */
187
- function quantizeToGrid(
188
- beat: number,
189
- barBeats: number,
190
- subdivision: number,
191
- slotsPerBar: number,
192
- ): { quantizedBeat: number; slotIndex: number } {
193
- const barIndex = Math.floor(beat / barBeats);
194
- const modBeat = beat - barIndex * barBeats;
195
- let slotIndex = Math.round(modBeat * subdivision);
196
- let effectiveBarIndex = barIndex;
197
- // Rounding the last slot in a bar up to the grid step count means it
198
- // actually lands on the next bar's downbeat.
199
- if (slotIndex >= slotsPerBar) {
200
- slotIndex = 0;
201
- effectiveBarIndex += 1;
202
- }
203
- return {
204
- quantizedBeat: effectiveBarIndex * barBeats + slotIndex / subdivision,
205
- slotIndex,
206
- };
207
- }
208
-
209
- /**
210
- * Extract a groove template from a set of note events: for each grid slot,
211
- * the average timing deviation from the quantized grid and the average
212
- * velocity of events landing on it. Slots with no velocity-carrying event
213
- * record `velocity: null` (the "unrecorded" sentinel); empty slots default to
214
- * `{ timingOffset: 0, velocity: null }`. The time signature is stored on the
215
- * template so {@link applyGrooveTemplate} can verify the meter matches.
216
- *
217
- * @param events The (typically "groovy", human-played) events to analyze.
218
- * @param ts The time signature, used to compute the bar length.
219
- * @param subdivision Grid resolution: equal grid steps per quarter-note beat.
220
- * @returns The extracted groove template.
221
- * @category Rhythm & Meter
222
- */
223
- export function extractGrooveTemplate(
224
- events: NoteEvent[],
225
- ts: TimeSignature,
226
- subdivision: number,
227
- ): GrooveTemplate {
228
- assertTimeSignature(ts);
229
- assertNoteEvents(events, 'groove source events');
230
- assertPositiveInt(subdivision, 'groove subdivision');
231
- const barBeats = beatsPerBar(ts);
232
- const slotsPerBar = Math.round(barBeats * subdivision);
233
- assertGenerationBudget(slotsPerBar, 'groove template slots');
234
- const offsetSums = new Array<number>(slotsPerBar).fill(0);
235
- const velocitySums = new Array<number>(slotsPerBar).fill(0);
236
- const counts = new Array<number>(slotsPerBar).fill(0);
237
- const velocityCounts = new Array<number>(slotsPerBar).fill(0);
238
-
239
- for (const event of events) {
240
- const { quantizedBeat, slotIndex } = quantizeToGrid(
241
- event.startBeat,
242
- barBeats,
243
- subdivision,
244
- slotsPerBar,
245
- );
246
- offsetSums[slotIndex] = (offsetSums[slotIndex] ?? 0) + (event.startBeat - quantizedBeat);
247
- counts[slotIndex] = (counts[slotIndex] ?? 0) + 1;
248
- // Only events that carry a velocity contribute to the velocity average, so a
249
- // velocity-less event does not pull the slot's recorded velocity toward zero.
250
- if (event.velocity !== undefined) {
251
- velocitySums[slotIndex] = (velocitySums[slotIndex] ?? 0) + event.velocity;
252
- velocityCounts[slotIndex] = (velocityCounts[slotIndex] ?? 0) + 1;
253
- }
254
- }
255
-
256
- const slots: GrooveSlot[] = [];
257
- for (let i = 0; i < slotsPerBar; i += 1) {
258
- const count = counts[i] ?? 0;
259
- if (count === 0) {
260
- slots.push({ timingOffset: 0, velocity: null });
261
- continue;
262
- }
263
- const velocityCount = velocityCounts[i] ?? 0;
264
- slots.push({
265
- timingOffset: (offsetSums[i] ?? 0) / count,
266
- velocity: velocityCount > 0 ? (velocitySums[i] ?? 0) / velocityCount : null,
267
- });
268
- }
269
-
270
- return { subdivision, slotsPerBar, slots, ts };
271
- }
272
-
273
- /**
274
- * Apply a groove template to a set of (typically quantized) note events: each
275
- * event is snapped to its grid slot and then offset by that slot's recorded
276
- * timing deviation, pushing stiff timing toward the template's feel.
277
- * Velocity is set to the slot's recorded average when it recorded one (a
278
- * non-`null` velocity, including a genuine 0); otherwise the event's own
279
- * velocity is left untouched.
280
- *
281
- * If the template carries a time signature (see {@link GrooveTemplate.ts}) it
282
- * must match `ts`, since a differing bar length would misalign the per-bar
283
- * grid and drift the feel; a mismatch throws rather than corrupting timing
284
- * silently.
285
- *
286
- * @param events The events to reshape.
287
- * @param template The groove template, from {@link extractGrooveTemplate}.
288
- * @param ts The time signature, used to compute the bar length.
289
- * @returns Reshaped copies of `events`, in input order.
290
- * @throws If `template.ts` is set and does not match `ts`.
291
- * @example
292
- * ```ts
293
- * import { extractGrooveTemplate, applyGrooveTemplate, parseTimeSignature } from '@libraz/libcantus';
294
- * const ts = parseTimeSignature('4/4');
295
- * // Learn the feel from a loosely played performance...
296
- * const groovy = [
297
- * { pitch: 36, startBeat: 0.02, durationBeat: 1, velocity: 100 },
298
- * { pitch: 38, startBeat: 1.06, durationBeat: 1, velocity: 70 },
299
- * ];
300
- * const template = extractGrooveTemplate(groovy, ts, 4);
301
- * // ...then impose it on stiff, quantized events.
302
- * const quantized = [{ pitch: 36, startBeat: 0, durationBeat: 1, velocity: 80 }];
303
- * applyGrooveTemplate(quantized, template, ts); // events nudged toward the captured feel
304
- * ```
305
- * @category Rhythm & Meter
306
- */
307
- export function applyGrooveTemplate(
308
- events: NoteEvent[],
309
- template: GrooveTemplate,
310
- ts: TimeSignature,
311
- ): NoteEvent[] {
312
- assertTimeSignature(ts);
313
- assertNoteEvents(events, 'groove target events');
314
- assertPositiveInt(template.subdivision, 'template subdivision');
315
- assertPositiveInt(template.slotsPerBar, 'template slotsPerBar');
316
- assertGenerationBudget(template.slots.length, 'template slots');
317
- if (template.slots.length !== template.slotsPerBar) {
318
- throw new RangeError('template slots length must equal slotsPerBar');
319
- }
320
- if (
321
- template.ts &&
322
- (template.ts.numerator !== ts.numerator || template.ts.denominator !== ts.denominator)
323
- ) {
324
- throw new Error(
325
- `Groove template meter ${formatTimeSignature(template.ts)} does not match ` +
326
- `apply-time meter ${formatTimeSignature(ts)}`,
327
- );
328
- }
329
- const barBeats = beatsPerBar(ts);
330
- return events.map((event) => {
331
- const { quantizedBeat, slotIndex } = quantizeToGrid(
332
- event.startBeat,
333
- barBeats,
334
- template.subdivision,
335
- template.slotsPerBar,
336
- );
337
- const slot = template.slots[slotIndex];
338
- const startBeat = quantizedBeat + (slot?.timingOffset ?? 0);
339
- const velocity =
340
- slot && slot.velocity !== null && slot.velocity !== undefined
341
- ? Math.round(slot.velocity)
342
- : event.velocity;
343
-
344
- return {
345
- pitch: event.pitch,
346
- startBeat,
347
- durationBeat: event.durationBeat,
348
- velocity,
349
- };
350
- });
351
- }