@libraz/libcantus 0.9.3 → 0.9.4

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 (198) hide show
  1. package/README.md +2 -2
  2. package/dist/analyze/index.cjs +39 -1947
  3. package/dist/analyze/index.cjs.map +1 -1
  4. package/dist/analyze/index.d.cts +197 -37
  5. package/dist/analyze/index.d.ts +197 -37
  6. package/dist/analyze/index.js +19 -8
  7. package/dist/chunk-36N73KZ3.cjs +246 -0
  8. package/dist/chunk-36N73KZ3.cjs.map +1 -0
  9. package/dist/chunk-4HLLZPXT.cjs +3077 -0
  10. package/dist/chunk-4HLLZPXT.cjs.map +1 -0
  11. package/dist/chunk-7MGZ7D62.cjs +498 -0
  12. package/dist/chunk-7MGZ7D62.cjs.map +1 -0
  13. package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
  14. package/dist/chunk-7NKDPF74.js.map +1 -0
  15. package/dist/chunk-CAN76NCU.cjs +679 -0
  16. package/dist/chunk-CAN76NCU.cjs.map +1 -0
  17. package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
  18. package/dist/chunk-D2FAJBYL.js.map +1 -0
  19. package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
  20. package/dist/chunk-EHVEBWM5.js.map +1 -0
  21. package/dist/chunk-FCANXBAT.cjs +30 -0
  22. package/dist/chunk-FCANXBAT.cjs.map +1 -0
  23. package/dist/chunk-FNDWXRRI.cjs +479 -0
  24. package/dist/chunk-FNDWXRRI.cjs.map +1 -0
  25. package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
  26. package/dist/chunk-GQSGS2N4.js.map +1 -0
  27. package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
  28. package/dist/chunk-HKGYCQ34.js.map +1 -0
  29. package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
  30. package/dist/chunk-I24MV65U.js.map +1 -0
  31. package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
  32. package/dist/chunk-KMHOICIY.js.map +1 -0
  33. package/dist/chunk-LW53AHQU.cjs +1117 -0
  34. package/dist/chunk-LW53AHQU.cjs.map +1 -0
  35. package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
  36. package/dist/chunk-NKIBQ6PT.js.map +1 -0
  37. package/dist/chunk-NP4NA2FB.cjs +106 -0
  38. package/dist/chunk-NP4NA2FB.cjs.map +1 -0
  39. package/dist/chunk-NVE34YGZ.cjs +1449 -0
  40. package/dist/chunk-NVE34YGZ.cjs.map +1 -0
  41. package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
  42. package/dist/chunk-NYYWMV2C.js.map +1 -0
  43. package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
  44. package/dist/chunk-Q2XUP227.js.map +1 -0
  45. package/dist/chunk-Q6OP4U43.cjs +327 -0
  46. package/dist/chunk-Q6OP4U43.cjs.map +1 -0
  47. package/dist/chunk-RC6GBMCD.cjs +845 -0
  48. package/dist/chunk-RC6GBMCD.cjs.map +1 -0
  49. package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
  50. package/dist/chunk-RK35RIN7.js.map +1 -0
  51. package/dist/chunk-RUMU3UA6.cjs +53 -0
  52. package/dist/chunk-RUMU3UA6.cjs.map +1 -0
  53. package/dist/chunk-T64C7ZD7.cjs +1 -0
  54. package/dist/chunk-T64C7ZD7.cjs.map +1 -0
  55. package/dist/chunk-XEBG3G53.js +498 -0
  56. package/dist/chunk-XEBG3G53.js.map +1 -0
  57. package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
  58. package/dist/chunk-YLEMGEFH.js.map +1 -0
  59. package/dist/chunk-ZEJR4TKL.cjs +173 -0
  60. package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
  61. package/dist/core/index.cjs +130 -647
  62. package/dist/core/index.cjs.map +1 -1
  63. package/dist/core/index.d.cts +301 -39
  64. package/dist/core/index.d.ts +301 -39
  65. package/dist/core/index.js +54 -16
  66. package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
  67. package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
  68. package/dist/generate/index.cjs +28 -4085
  69. package/dist/generate/index.cjs.map +1 -1
  70. package/dist/generate/index.d.cts +370 -62
  71. package/dist/generate/index.d.ts +370 -62
  72. package/dist/generate/index.js +23 -9
  73. package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
  74. package/dist/index-70p168VM.d.ts +192 -0
  75. package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
  76. package/dist/index-BtXT6UQ0.d.cts +192 -0
  77. package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
  78. package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
  79. package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
  80. package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
  81. package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
  82. package/dist/index-OG65ocOr.d.cts +272 -0
  83. package/dist/index-OG65ocOr.d.ts +272 -0
  84. package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
  85. package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
  86. package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
  87. package/dist/index.cjs +419 -7383
  88. package/dist/index.cjs.map +1 -1
  89. package/dist/index.d.cts +14 -14
  90. package/dist/index.d.ts +14 -14
  91. package/dist/index.js +111 -27
  92. package/dist/model/index.cjs +8 -2562
  93. package/dist/model/index.cjs.map +1 -1
  94. package/dist/model/index.d.cts +433 -75
  95. package/dist/model/index.d.ts +433 -75
  96. package/dist/model/index.js +6 -7
  97. package/dist/styled-DIRkk9Uz.d.ts +506 -0
  98. package/dist/styled-DJbkJQfj.d.cts +506 -0
  99. package/dist/theory/index.cjs +172 -1822
  100. package/dist/theory/index.cjs.map +1 -1
  101. package/dist/theory/index.d.cts +151 -233
  102. package/dist/theory/index.d.ts +151 -233
  103. package/dist/theory/index.js +30 -7
  104. package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
  105. package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
  106. package/package.json +24 -6
  107. package/dist/chunk-2ZWMSJH6.js.map +0 -1
  108. package/dist/chunk-4MHA432G.js.map +0 -1
  109. package/dist/chunk-62QZKCJQ.js.map +0 -1
  110. package/dist/chunk-AKLYPWIN.js.map +0 -1
  111. package/dist/chunk-C5XYKUVG.js.map +0 -1
  112. package/dist/chunk-CHMXELDS.js.map +0 -1
  113. package/dist/chunk-DYWFNC4A.js +0 -152
  114. package/dist/chunk-DYWFNC4A.js.map +0 -1
  115. package/dist/chunk-EH6EM37U.js.map +0 -1
  116. package/dist/chunk-FBQK3ZQN.js +0 -131
  117. package/dist/chunk-FBQK3ZQN.js.map +0 -1
  118. package/dist/chunk-GOAW4VYL.js.map +0 -1
  119. package/dist/chunk-I5R7ZBJS.js.map +0 -1
  120. package/dist/chunk-IT2O4XFE.js.map +0 -1
  121. package/dist/chunk-MIWO425M.js.map +0 -1
  122. package/dist/chunk-SGU7SPLK.js.map +0 -1
  123. package/dist/index-CGMZNCx6.d.cts +0 -140
  124. package/dist/index-CGMZNCx6.d.ts +0 -140
  125. package/dist/index-CnZCo29u.d.cts +0 -108
  126. package/dist/index-JnF4v9bx.d.ts +0 -108
  127. package/dist/styled-DZ7GjShb.d.ts +0 -282
  128. package/dist/styled-ZTbdaVFM.d.cts +0 -282
  129. package/src/analyze/arrange/index.ts +0 -21
  130. package/src/analyze/arrange/internal.ts +0 -127
  131. package/src/analyze/arrange/tension.ts +0 -199
  132. package/src/analyze/arrange/tracks.ts +0 -312
  133. package/src/analyze/detect/index.ts +0 -272
  134. package/src/analyze/functional/borrowed.ts +0 -84
  135. package/src/analyze/functional/cadence.ts +0 -66
  136. package/src/analyze/functional/function.ts +0 -184
  137. package/src/analyze/functional/index.ts +0 -23
  138. package/src/analyze/functional/internal.ts +0 -29
  139. package/src/analyze/functional/roman.ts +0 -402
  140. package/src/analyze/index.ts +0 -49
  141. package/src/analyze/timeline/index.ts +0 -451
  142. package/src/analyze/voice/index.ts +0 -339
  143. package/src/core/event-index/index.ts +0 -104
  144. package/src/core/index.ts +0 -61
  145. package/src/core/interval/index.ts +0 -81
  146. package/src/core/meter/index.ts +0 -302
  147. package/src/core/pitch/index.ts +0 -311
  148. package/src/core/random/index.ts +0 -76
  149. package/src/core/tuning/index.ts +0 -186
  150. package/src/core/types.ts +0 -29
  151. package/src/core/validation/index.ts +0 -119
  152. package/src/generate/bass/index.ts +0 -373
  153. package/src/generate/countermelody/index.ts +0 -470
  154. package/src/generate/drums/beat.ts +0 -266
  155. package/src/generate/drums/euclid.ts +0 -65
  156. package/src/generate/drums/fills.ts +0 -356
  157. package/src/generate/drums/ghost.ts +0 -119
  158. package/src/generate/drums/hihat.ts +0 -285
  159. package/src/generate/drums/hit.ts +0 -31
  160. package/src/generate/drums/index.ts +0 -361
  161. package/src/generate/drums/internal.ts +0 -257
  162. package/src/generate/drums/kick.ts +0 -160
  163. package/src/generate/drums/percussion.ts +0 -121
  164. package/src/generate/drums/rng.ts +0 -7
  165. package/src/generate/drums/swing.ts +0 -71
  166. package/src/generate/groove/index.ts +0 -351
  167. package/src/generate/harmonize/index.ts +0 -451
  168. package/src/generate/index.ts +0 -58
  169. package/src/generate/motif/index.ts +0 -422
  170. package/src/generate/progression/index.ts +0 -362
  171. package/src/generate/reharmony/index.ts +0 -323
  172. package/src/generate/rhythm/index.ts +0 -175
  173. package/src/index.ts +0 -13
  174. package/src/model/chord.ts +0 -442
  175. package/src/model/index.ts +0 -15
  176. package/src/model/interval.ts +0 -82
  177. package/src/model/key.ts +0 -266
  178. package/src/model/note.ts +0 -176
  179. package/src/model/progression.ts +0 -161
  180. package/src/model/shared.ts +0 -12
  181. package/src/theory/chord/index.ts +0 -375
  182. package/src/theory/chordscale/index.ts +0 -374
  183. package/src/theory/counterpoint/index.ts +0 -230
  184. package/src/theory/harmony/index.ts +0 -77
  185. package/src/theory/index.ts +0 -112
  186. package/src/theory/safety/index.ts +0 -488
  187. package/src/theory/scale/degrees.ts +0 -123
  188. package/src/theory/scale/index.ts +0 -28
  189. package/src/theory/scale/key.ts +0 -56
  190. package/src/theory/scale/masks.ts +0 -153
  191. package/src/theory/spelling/index.ts +0 -249
  192. package/src/theory/symbol/index.ts +0 -268
  193. package/src/theory/voicing/index.ts +0 -5
  194. package/src/theory/voicing/internal.ts +0 -168
  195. package/src/theory/voicing/leading.ts +0 -112
  196. package/src/theory/voicing/satb.ts +0 -216
  197. package/src/theory/voicing/styled.ts +0 -199
  198. 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
- }