@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.
- package/README.md +2 -2
- package/dist/analyze/index.cjs +39 -1947
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +197 -37
- package/dist/analyze/index.d.ts +197 -37
- package/dist/analyze/index.js +19 -8
- package/dist/chunk-36N73KZ3.cjs +246 -0
- package/dist/chunk-36N73KZ3.cjs.map +1 -0
- package/dist/chunk-4HLLZPXT.cjs +3077 -0
- package/dist/chunk-4HLLZPXT.cjs.map +1 -0
- package/dist/chunk-7MGZ7D62.cjs +498 -0
- package/dist/chunk-7MGZ7D62.cjs.map +1 -0
- package/dist/{chunk-4MHA432G.js → chunk-7NKDPF74.js} +403 -180
- package/dist/chunk-7NKDPF74.js.map +1 -0
- package/dist/chunk-CAN76NCU.cjs +679 -0
- package/dist/chunk-CAN76NCU.cjs.map +1 -0
- package/dist/{chunk-MIWO425M.js → chunk-D2FAJBYL.js} +101 -34
- package/dist/chunk-D2FAJBYL.js.map +1 -0
- package/dist/{chunk-SGU7SPLK.js → chunk-EHVEBWM5.js} +1056 -531
- package/dist/chunk-EHVEBWM5.js.map +1 -0
- package/dist/chunk-FCANXBAT.cjs +30 -0
- package/dist/chunk-FCANXBAT.cjs.map +1 -0
- package/dist/chunk-FNDWXRRI.cjs +479 -0
- package/dist/chunk-FNDWXRRI.cjs.map +1 -0
- package/dist/{chunk-IT2O4XFE.js → chunk-GQSGS2N4.js} +43 -24
- package/dist/chunk-GQSGS2N4.js.map +1 -0
- package/dist/{chunk-62QZKCJQ.js → chunk-HKGYCQ34.js} +10 -6
- package/dist/chunk-HKGYCQ34.js.map +1 -0
- package/dist/{chunk-AKLYPWIN.js → chunk-I24MV65U.js} +29 -6
- package/dist/chunk-I24MV65U.js.map +1 -0
- package/dist/{chunk-CHMXELDS.js → chunk-KMHOICIY.js} +311 -181
- package/dist/chunk-KMHOICIY.js.map +1 -0
- package/dist/chunk-LW53AHQU.cjs +1117 -0
- package/dist/chunk-LW53AHQU.cjs.map +1 -0
- package/dist/{chunk-2ZWMSJH6.js → chunk-NKIBQ6PT.js} +2 -2
- package/dist/chunk-NKIBQ6PT.js.map +1 -0
- package/dist/chunk-NP4NA2FB.cjs +106 -0
- package/dist/chunk-NP4NA2FB.cjs.map +1 -0
- package/dist/chunk-NVE34YGZ.cjs +1449 -0
- package/dist/chunk-NVE34YGZ.cjs.map +1 -0
- package/dist/{chunk-EH6EM37U.js → chunk-NYYWMV2C.js} +61 -49
- package/dist/chunk-NYYWMV2C.js.map +1 -0
- package/dist/{chunk-GOAW4VYL.js → chunk-Q2XUP227.js} +243 -117
- package/dist/chunk-Q2XUP227.js.map +1 -0
- package/dist/chunk-Q6OP4U43.cjs +327 -0
- package/dist/chunk-Q6OP4U43.cjs.map +1 -0
- package/dist/chunk-RC6GBMCD.cjs +845 -0
- package/dist/chunk-RC6GBMCD.cjs.map +1 -0
- package/dist/{chunk-C5XYKUVG.js → chunk-RK35RIN7.js} +370 -157
- package/dist/chunk-RK35RIN7.js.map +1 -0
- package/dist/chunk-RUMU3UA6.cjs +53 -0
- package/dist/chunk-RUMU3UA6.cjs.map +1 -0
- package/dist/chunk-T64C7ZD7.cjs +1 -0
- package/dist/chunk-T64C7ZD7.cjs.map +1 -0
- package/dist/chunk-XEBG3G53.js +498 -0
- package/dist/chunk-XEBG3G53.js.map +1 -0
- package/dist/{chunk-I5R7ZBJS.js → chunk-YLEMGEFH.js} +103 -59
- package/dist/chunk-YLEMGEFH.js.map +1 -0
- package/dist/chunk-ZEJR4TKL.cjs +173 -0
- package/dist/chunk-ZEJR4TKL.cjs.map +1 -0
- package/dist/core/index.cjs +130 -647
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +301 -39
- package/dist/core/index.d.ts +301 -39
- package/dist/core/index.js +54 -16
- package/dist/{function-DYt4iKsn.d.cts → function-BtiZb9i9.d.ts} +28 -7
- package/dist/{function-DVH4Eusr.d.ts → function-DNugAOrt.d.cts} +28 -7
- package/dist/generate/index.cjs +28 -4085
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +370 -62
- package/dist/generate/index.d.ts +370 -62
- package/dist/generate/index.js +23 -9
- package/dist/{index-CxqBQtH1.d.cts → index-6VR8-kit.d.ts} +19 -4
- package/dist/index-70p168VM.d.ts +192 -0
- package/dist/{index-CpOsP1y9.d.cts → index-Be74mcon.d.cts} +18 -17
- package/dist/index-BtXT6UQ0.d.cts +192 -0
- package/dist/{index-DxvARIP2.d.ts → index-C5UBbQ4G.d.ts} +18 -17
- package/dist/{index-Bt4tOUD9.d.ts → index-C83mZI1P.d.ts} +93 -8
- package/dist/{index-Bg5dYMmZ.d.ts → index-D6b6na69.d.cts} +14 -11
- package/dist/{index-Cl7mhOFM.d.cts → index-DXd9JikN.d.cts} +92 -10
- package/dist/{index-Cl7mhOFM.d.ts → index-DXd9JikN.d.ts} +92 -10
- package/dist/index-OG65ocOr.d.cts +272 -0
- package/dist/index-OG65ocOr.d.ts +272 -0
- package/dist/{index-Jc0Ryi79.d.ts → index-tJhkmQgE.d.cts} +19 -4
- package/dist/{index-Dikst44v.d.cts → index-tSQh-xwY.d.ts} +14 -11
- package/dist/{index-BNlGP86A.d.cts → index-y_ak9_Hi.d.cts} +93 -8
- package/dist/index.cjs +419 -7383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +111 -27
- package/dist/model/index.cjs +8 -2562
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +433 -75
- package/dist/model/index.d.ts +433 -75
- package/dist/model/index.js +6 -7
- package/dist/styled-DIRkk9Uz.d.ts +506 -0
- package/dist/styled-DJbkJQfj.d.cts +506 -0
- package/dist/theory/index.cjs +172 -1822
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +151 -233
- package/dist/theory/index.d.ts +151 -233
- package/dist/theory/index.js +30 -7
- package/dist/{types-MGP29r3t.d.cts → types-BN6_Q9Kl.d.cts} +11 -2
- package/dist/{types-MGP29r3t.d.ts → types-BN6_Q9Kl.d.ts} +11 -2
- package/package.json +24 -6
- package/dist/chunk-2ZWMSJH6.js.map +0 -1
- package/dist/chunk-4MHA432G.js.map +0 -1
- package/dist/chunk-62QZKCJQ.js.map +0 -1
- package/dist/chunk-AKLYPWIN.js.map +0 -1
- package/dist/chunk-C5XYKUVG.js.map +0 -1
- package/dist/chunk-CHMXELDS.js.map +0 -1
- package/dist/chunk-DYWFNC4A.js +0 -152
- package/dist/chunk-DYWFNC4A.js.map +0 -1
- package/dist/chunk-EH6EM37U.js.map +0 -1
- package/dist/chunk-FBQK3ZQN.js +0 -131
- package/dist/chunk-FBQK3ZQN.js.map +0 -1
- package/dist/chunk-GOAW4VYL.js.map +0 -1
- package/dist/chunk-I5R7ZBJS.js.map +0 -1
- package/dist/chunk-IT2O4XFE.js.map +0 -1
- package/dist/chunk-MIWO425M.js.map +0 -1
- package/dist/chunk-SGU7SPLK.js.map +0 -1
- package/dist/index-CGMZNCx6.d.cts +0 -140
- package/dist/index-CGMZNCx6.d.ts +0 -140
- package/dist/index-CnZCo29u.d.cts +0 -108
- package/dist/index-JnF4v9bx.d.ts +0 -108
- package/dist/styled-DZ7GjShb.d.ts +0 -282
- package/dist/styled-ZTbdaVFM.d.cts +0 -282
- package/src/analyze/arrange/index.ts +0 -21
- package/src/analyze/arrange/internal.ts +0 -127
- package/src/analyze/arrange/tension.ts +0 -199
- package/src/analyze/arrange/tracks.ts +0 -312
- package/src/analyze/detect/index.ts +0 -272
- package/src/analyze/functional/borrowed.ts +0 -84
- package/src/analyze/functional/cadence.ts +0 -66
- package/src/analyze/functional/function.ts +0 -184
- package/src/analyze/functional/index.ts +0 -23
- package/src/analyze/functional/internal.ts +0 -29
- package/src/analyze/functional/roman.ts +0 -402
- package/src/analyze/index.ts +0 -49
- package/src/analyze/timeline/index.ts +0 -451
- package/src/analyze/voice/index.ts +0 -339
- package/src/core/event-index/index.ts +0 -104
- package/src/core/index.ts +0 -61
- package/src/core/interval/index.ts +0 -81
- package/src/core/meter/index.ts +0 -302
- package/src/core/pitch/index.ts +0 -311
- package/src/core/random/index.ts +0 -76
- package/src/core/tuning/index.ts +0 -186
- package/src/core/types.ts +0 -29
- package/src/core/validation/index.ts +0 -119
- package/src/generate/bass/index.ts +0 -373
- package/src/generate/countermelody/index.ts +0 -470
- package/src/generate/drums/beat.ts +0 -266
- package/src/generate/drums/euclid.ts +0 -65
- package/src/generate/drums/fills.ts +0 -356
- package/src/generate/drums/ghost.ts +0 -119
- package/src/generate/drums/hihat.ts +0 -285
- package/src/generate/drums/hit.ts +0 -31
- package/src/generate/drums/index.ts +0 -361
- package/src/generate/drums/internal.ts +0 -257
- package/src/generate/drums/kick.ts +0 -160
- package/src/generate/drums/percussion.ts +0 -121
- package/src/generate/drums/rng.ts +0 -7
- package/src/generate/drums/swing.ts +0 -71
- package/src/generate/groove/index.ts +0 -351
- package/src/generate/harmonize/index.ts +0 -451
- package/src/generate/index.ts +0 -58
- package/src/generate/motif/index.ts +0 -422
- package/src/generate/progression/index.ts +0 -362
- package/src/generate/reharmony/index.ts +0 -323
- package/src/generate/rhythm/index.ts +0 -175
- package/src/index.ts +0 -13
- package/src/model/chord.ts +0 -442
- package/src/model/index.ts +0 -15
- package/src/model/interval.ts +0 -82
- package/src/model/key.ts +0 -266
- package/src/model/note.ts +0 -176
- package/src/model/progression.ts +0 -161
- package/src/model/shared.ts +0 -12
- package/src/theory/chord/index.ts +0 -375
- package/src/theory/chordscale/index.ts +0 -374
- package/src/theory/counterpoint/index.ts +0 -230
- package/src/theory/harmony/index.ts +0 -77
- package/src/theory/index.ts +0 -112
- package/src/theory/safety/index.ts +0 -488
- package/src/theory/scale/degrees.ts +0 -123
- package/src/theory/scale/index.ts +0 -28
- package/src/theory/scale/key.ts +0 -56
- package/src/theory/scale/masks.ts +0 -153
- package/src/theory/spelling/index.ts +0 -249
- package/src/theory/symbol/index.ts +0 -268
- package/src/theory/voicing/index.ts +0 -5
- package/src/theory/voicing/internal.ts +0 -168
- package/src/theory/voicing/leading.ts +0 -112
- package/src/theory/voicing/satb.ts +0 -216
- package/src/theory/voicing/styled.ts +0 -199
- 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
|
-
}
|