@libraz/libcantus 0.9.2 → 0.9.3
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 +4 -3
- package/dist/analyze/index.cjs +315 -42
- package/dist/analyze/index.cjs.map +1 -1
- package/dist/analyze/index.d.cts +11 -99
- package/dist/analyze/index.d.ts +11 -99
- package/dist/analyze/index.js +6 -6
- package/dist/chunk-2ZWMSJH6.js +30 -0
- package/dist/chunk-2ZWMSJH6.js.map +1 -0
- package/dist/{chunk-R3S2LUBH.js → chunk-4MHA432G.js} +316 -184
- package/dist/chunk-4MHA432G.js.map +1 -0
- package/dist/chunk-62QZKCJQ.js +49 -0
- package/dist/chunk-62QZKCJQ.js.map +1 -0
- package/dist/chunk-AKLYPWIN.js +83 -0
- package/dist/chunk-AKLYPWIN.js.map +1 -0
- package/dist/{chunk-XTC3X6MS.js → chunk-C5XYKUVG.js} +100 -12
- package/dist/chunk-C5XYKUVG.js.map +1 -0
- package/dist/{chunk-LN7ZZMD2.js → chunk-CHMXELDS.js} +33 -14
- package/dist/chunk-CHMXELDS.js.map +1 -0
- package/dist/{chunk-A4532ZFR.js → chunk-EH6EM37U.js} +60 -4
- package/dist/chunk-EH6EM37U.js.map +1 -0
- package/dist/chunk-FBQK3ZQN.js +131 -0
- package/dist/chunk-FBQK3ZQN.js.map +1 -0
- package/dist/{chunk-5HTNMQW4.js → chunk-GOAW4VYL.js} +87 -21
- package/dist/chunk-GOAW4VYL.js.map +1 -0
- package/dist/{chunk-MSWFZF7J.js → chunk-I5R7ZBJS.js} +90 -6
- package/dist/chunk-I5R7ZBJS.js.map +1 -0
- package/dist/{chunk-GQKN5KDT.js → chunk-IT2O4XFE.js} +4 -9
- package/dist/chunk-IT2O4XFE.js.map +1 -0
- package/dist/{chunk-7Y7WF64D.js → chunk-MIWO425M.js} +6 -4
- package/dist/chunk-MIWO425M.js.map +1 -0
- package/dist/chunk-N6IHQUOX.js +1 -0
- package/dist/chunk-N6IHQUOX.js.map +1 -0
- package/dist/{chunk-COKTX2G5.js → chunk-SGU7SPLK.js} +29 -12
- package/dist/chunk-SGU7SPLK.js.map +1 -0
- package/dist/core/index.cjs +230 -14
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +56 -4
- package/dist/core/index.d.ts +56 -4
- package/dist/core/index.js +24 -4
- package/dist/{function-2bJW1Zp_.d.ts → function-DVH4Eusr.d.ts} +1 -1
- package/dist/{function-r2sh-jOO.d.cts → function-DYt4iKsn.d.cts} +1 -1
- package/dist/generate/index.cjs +830 -494
- package/dist/generate/index.cjs.map +1 -1
- package/dist/generate/index.d.cts +11 -5
- package/dist/generate/index.d.ts +11 -5
- package/dist/generate/index.js +9 -9
- package/dist/{index-BOBXBNwP.d.cts → index-BNlGP86A.d.cts} +1 -1
- package/dist/{index-CP3wAfEH.d.ts → index-Bg5dYMmZ.d.ts} +1 -1
- package/dist/{index-CM5W4rcm.d.ts → index-Bt4tOUD9.d.ts} +1 -1
- package/dist/index-CnZCo29u.d.cts +108 -0
- package/dist/{index-D_ZTUNKp.d.cts → index-CpOsP1y9.d.cts} +2 -2
- package/dist/{index-DZ1M21do.d.cts → index-CxqBQtH1.d.cts} +2 -1
- package/dist/{index-BdeQ1r_p.d.cts → index-Dikst44v.d.cts} +1 -1
- package/dist/{index-BMPxwjwV.d.ts → index-DxvARIP2.d.ts} +2 -2
- package/dist/{index-BreoJRw0.d.ts → index-Jc0Ryi79.d.ts} +2 -1
- package/dist/index-JnF4v9bx.d.ts +108 -0
- package/dist/index.cjs +818 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +34 -13
- package/dist/model/index.cjs +170 -31
- package/dist/model/index.cjs.map +1 -1
- package/dist/model/index.d.cts +6 -5
- package/dist/model/index.d.ts +6 -5
- package/dist/model/index.js +6 -6
- package/dist/{styled-D7BAx3S3.d.ts → styled-DZ7GjShb.d.ts} +1 -1
- package/dist/{styled-BjXtOFNk.d.cts → styled-ZTbdaVFM.d.cts} +1 -1
- package/dist/theory/index.cjs +180 -34
- package/dist/theory/index.cjs.map +1 -1
- package/dist/theory/index.d.cts +6 -6
- package/dist/theory/index.d.ts +6 -6
- package/dist/theory/index.js +6 -5
- package/package.json +50 -19
- package/src/analyze/arrange/internal.ts +3 -1
- package/src/analyze/arrange/tension.ts +15 -3
- package/src/analyze/arrange/tracks.ts +36 -3
- package/src/analyze/detect/index.ts +46 -9
- package/src/analyze/functional/roman.ts +14 -5
- package/src/analyze/index.ts +1 -1
- package/src/analyze/timeline/index.ts +43 -7
- package/src/analyze/voice/index.ts +11 -2
- package/src/core/event-index/index.ts +104 -0
- package/src/core/index.ts +14 -0
- package/src/core/meter/index.ts +24 -4
- package/src/core/random/index.ts +26 -3
- package/src/core/tuning/index.ts +36 -6
- package/src/core/validation/index.ts +119 -0
- package/src/generate/bass/index.ts +23 -0
- package/src/generate/countermelody/index.ts +167 -76
- package/src/generate/drums/euclid.ts +10 -3
- package/src/generate/drums/index.ts +56 -24
- package/src/generate/drums/percussion.ts +1 -1
- package/src/generate/groove/index.ts +25 -0
- package/src/generate/harmonize/index.ts +55 -44
- package/src/generate/index.ts +0 -1
- package/src/generate/motif/index.ts +45 -21
- package/src/generate/progression/index.ts +8 -16
- package/src/generate/reharmony/index.ts +1 -8
- package/src/generate/rhythm/index.ts +11 -1
- package/src/index.ts +13 -247
- package/src/model/chord.ts +29 -10
- package/src/theory/chord/index.ts +4 -2
- package/src/theory/harmony/index.ts +6 -21
- package/src/theory/index.ts +6 -1
- package/src/theory/safety/index.ts +71 -2
- package/src/theory/spelling/index.ts +70 -3
- package/src/theory/voicing/satb.ts +25 -4
- package/src/theory/voicing/styled.ts +26 -3
- package/dist/chunk-5HTNMQW4.js.map +0 -1
- package/dist/chunk-7Y7WF64D.js.map +0 -1
- package/dist/chunk-A4532ZFR.js.map +0 -1
- package/dist/chunk-BHUT2S5G.js +0 -22
- package/dist/chunk-BHUT2S5G.js.map +0 -1
- package/dist/chunk-CCBLVAPK.js +0 -42
- package/dist/chunk-CCBLVAPK.js.map +0 -1
- package/dist/chunk-COKTX2G5.js.map +0 -1
- package/dist/chunk-GQKN5KDT.js.map +0 -1
- package/dist/chunk-LN7ZZMD2.js.map +0 -1
- package/dist/chunk-MSWFZF7J.js.map +0 -1
- package/dist/chunk-R3S2LUBH.js.map +0 -1
- package/dist/chunk-SDM3VJJ5.js +0 -41
- package/dist/chunk-SDM3VJJ5.js.map +0 -1
- package/dist/chunk-XTC3X6MS.js.map +0 -1
- package/dist/chunk-ZXW5YIWK.js +0 -55
- package/dist/chunk-ZXW5YIWK.js.map +0 -1
package/dist/theory/index.d.cts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { C as Chord } from '../index-
|
|
2
|
-
export { a as ChordQuality, c as chordFromDegree, d as chordPitchClasses, e as chordQualities, f as chordToneRole, g as diatonicSeventh, h as diatonicTriad, m as makeChord } from '../index-
|
|
3
|
-
import { d as VoicingOptions } from '../styled-
|
|
4
|
-
export { C as ChordScaleMatch, a as ChordScaleReportEntry, S as SATB_RANGES, b as ScaleChoice, c as StyledVoicingOptions, V as VoiceRange, e as VoicingStyle, f as availableTensions, g as avoidNotes, h as chordScaleReport, i as chordScales, s as scaleMatchesChord, j as scalesForChanges, v as voiceChord, k as voiceChordStyled, l as voiceProgression } from '../styled-
|
|
1
|
+
import { C as Chord } from '../index-BNlGP86A.cjs';
|
|
2
|
+
export { a as ChordQuality, b as ChordSpan, P as PitchSpelling, c as chordFromDegree, d as chordPitchClasses, e as chordQualities, f as chordToneRole, g as diatonicSeventh, h as diatonicTriad, m as makeChord } from '../index-BNlGP86A.cjs';
|
|
3
|
+
import { d as VoicingOptions } from '../styled-ZTbdaVFM.cjs';
|
|
4
|
+
export { C as ChordScaleMatch, a as ChordScaleReportEntry, S as SATB_RANGES, b as ScaleChoice, c as StyledVoicingOptions, V as VoiceRange, e as VoicingStyle, f as availableTensions, g as avoidNotes, h as chordScaleReport, i as chordScales, s as scaleMatchesChord, j as scalesForChanges, v as voiceChord, k as voiceChordStyled, l as voiceProgression } from '../styled-ZTbdaVFM.cjs';
|
|
5
5
|
import { K as KeyScale } from '../types-MGP29r3t.cjs';
|
|
6
|
-
export { H as HarmonyRole, L as LockLevel, V as VoicedRole, r as roleOf } from '../index-
|
|
7
|
-
export { N as NoteSafety, R as ReasonFlag, S as SafetyProfile, a as SafetyQuery, b as SafetyResult, V as VoiceSnapshot, e as enumerateSafePitches, c as evaluateSafety } from '../index-
|
|
6
|
+
export { H as HarmonyRole, L as LockLevel, V as VoicedRole, r as roleOf } from '../index-Dikst44v.cjs';
|
|
7
|
+
export { N as NoteSafety, R as ReasonFlag, S as SafetyProfile, a as SafetyQuery, b as SafetyResult, V as VoiceSnapshot, e as enumerateSafePitches, c as evaluateSafety } from '../index-CxqBQtH1.cjs';
|
|
8
8
|
import { N as Note } from '../index-CGMZNCx6.cjs';
|
|
9
9
|
|
|
10
10
|
/**
|
package/dist/theory/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { C as Chord } from '../index-
|
|
2
|
-
export { a as ChordQuality, c as chordFromDegree, d as chordPitchClasses, e as chordQualities, f as chordToneRole, g as diatonicSeventh, h as diatonicTriad, m as makeChord } from '../index-
|
|
3
|
-
import { d as VoicingOptions } from '../styled-
|
|
4
|
-
export { C as ChordScaleMatch, a as ChordScaleReportEntry, S as SATB_RANGES, b as ScaleChoice, c as StyledVoicingOptions, V as VoiceRange, e as VoicingStyle, f as availableTensions, g as avoidNotes, h as chordScaleReport, i as chordScales, s as scaleMatchesChord, j as scalesForChanges, v as voiceChord, k as voiceChordStyled, l as voiceProgression } from '../styled-
|
|
1
|
+
import { C as Chord } from '../index-Bt4tOUD9.js';
|
|
2
|
+
export { a as ChordQuality, b as ChordSpan, P as PitchSpelling, c as chordFromDegree, d as chordPitchClasses, e as chordQualities, f as chordToneRole, g as diatonicSeventh, h as diatonicTriad, m as makeChord } from '../index-Bt4tOUD9.js';
|
|
3
|
+
import { d as VoicingOptions } from '../styled-DZ7GjShb.js';
|
|
4
|
+
export { C as ChordScaleMatch, a as ChordScaleReportEntry, S as SATB_RANGES, b as ScaleChoice, c as StyledVoicingOptions, V as VoiceRange, e as VoicingStyle, f as availableTensions, g as avoidNotes, h as chordScaleReport, i as chordScales, s as scaleMatchesChord, j as scalesForChanges, v as voiceChord, k as voiceChordStyled, l as voiceProgression } from '../styled-DZ7GjShb.js';
|
|
5
5
|
import { K as KeyScale } from '../types-MGP29r3t.js';
|
|
6
|
-
export { H as HarmonyRole, L as LockLevel, V as VoicedRole, r as roleOf } from '../index-
|
|
7
|
-
export { N as NoteSafety, R as ReasonFlag, S as SafetyProfile, a as SafetyQuery, b as SafetyResult, V as VoiceSnapshot, e as enumerateSafePitches, c as evaluateSafety } from '../index-
|
|
6
|
+
export { H as HarmonyRole, L as LockLevel, V as VoicedRole, r as roleOf } from '../index-Bg5dYMmZ.js';
|
|
7
|
+
export { N as NoteSafety, R as ReasonFlag, S as SafetyProfile, a as SafetyQuery, b as SafetyResult, V as VoiceSnapshot, e as enumerateSafePitches, c as evaluateSafety } from '../index-Jc0Ryi79.js';
|
|
8
8
|
import { N as Note } from '../index-CGMZNCx6.js';
|
|
9
9
|
|
|
10
10
|
/**
|
package/dist/theory/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import "../chunk-N6IHQUOX.js";
|
|
1
2
|
import {
|
|
2
3
|
roleOf
|
|
3
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-2ZWMSJH6.js";
|
|
4
5
|
import {
|
|
5
6
|
NoteSafety,
|
|
6
7
|
ReasonFlag,
|
|
7
8
|
enumerateSafePitches,
|
|
8
9
|
evaluateSafety
|
|
9
|
-
} from "../chunk-
|
|
10
|
+
} from "../chunk-EH6EM37U.js";
|
|
10
11
|
import {
|
|
11
12
|
SATB_RANGES,
|
|
12
13
|
availableTensions,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
voiceChordStyled,
|
|
29
30
|
voiceLeadingCost,
|
|
30
31
|
voiceProgression
|
|
31
|
-
} from "../chunk-
|
|
32
|
+
} from "../chunk-C5XYKUVG.js";
|
|
32
33
|
import "../chunk-DYWFNC4A.js";
|
|
33
34
|
import {
|
|
34
35
|
BLUES_MASK,
|
|
@@ -74,8 +75,8 @@ import {
|
|
|
74
75
|
pitchToScaleDegree,
|
|
75
76
|
scaleByName,
|
|
76
77
|
scaleTonesInDegreeOrder
|
|
77
|
-
} from "../chunk-
|
|
78
|
-
import "../chunk-
|
|
78
|
+
} from "../chunk-MIWO425M.js";
|
|
79
|
+
import "../chunk-FBQK3ZQN.js";
|
|
79
80
|
export {
|
|
80
81
|
BLUES_MASK,
|
|
81
82
|
CHROMATIC_MASK,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@libraz/libcantus",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Pure-TypeScript music-theory and composition primitives (intervals, scales, chords).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -35,34 +35,64 @@
|
|
|
35
35
|
"types": "./dist/index.d.ts",
|
|
36
36
|
"exports": {
|
|
37
37
|
".": {
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
"import": {
|
|
39
|
+
"types": "./dist/index.d.ts",
|
|
40
|
+
"default": "./dist/index.js"
|
|
41
|
+
},
|
|
42
|
+
"require": {
|
|
43
|
+
"types": "./dist/index.d.cts",
|
|
44
|
+
"default": "./dist/index.cjs"
|
|
45
|
+
}
|
|
41
46
|
},
|
|
42
47
|
"./core": {
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
"import": {
|
|
49
|
+
"types": "./dist/core/index.d.ts",
|
|
50
|
+
"default": "./dist/core/index.js"
|
|
51
|
+
},
|
|
52
|
+
"require": {
|
|
53
|
+
"types": "./dist/core/index.d.cts",
|
|
54
|
+
"default": "./dist/core/index.cjs"
|
|
55
|
+
}
|
|
46
56
|
},
|
|
47
57
|
"./theory": {
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
"import": {
|
|
59
|
+
"types": "./dist/theory/index.d.ts",
|
|
60
|
+
"default": "./dist/theory/index.js"
|
|
61
|
+
},
|
|
62
|
+
"require": {
|
|
63
|
+
"types": "./dist/theory/index.d.cts",
|
|
64
|
+
"default": "./dist/theory/index.cjs"
|
|
65
|
+
}
|
|
51
66
|
},
|
|
52
67
|
"./analyze": {
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
"import": {
|
|
69
|
+
"types": "./dist/analyze/index.d.ts",
|
|
70
|
+
"default": "./dist/analyze/index.js"
|
|
71
|
+
},
|
|
72
|
+
"require": {
|
|
73
|
+
"types": "./dist/analyze/index.d.cts",
|
|
74
|
+
"default": "./dist/analyze/index.cjs"
|
|
75
|
+
}
|
|
56
76
|
},
|
|
57
77
|
"./generate": {
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
"import": {
|
|
79
|
+
"types": "./dist/generate/index.d.ts",
|
|
80
|
+
"default": "./dist/generate/index.js"
|
|
81
|
+
},
|
|
82
|
+
"require": {
|
|
83
|
+
"types": "./dist/generate/index.d.cts",
|
|
84
|
+
"default": "./dist/generate/index.cjs"
|
|
85
|
+
}
|
|
61
86
|
},
|
|
62
87
|
"./model": {
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
"import": {
|
|
89
|
+
"types": "./dist/model/index.d.ts",
|
|
90
|
+
"default": "./dist/model/index.js"
|
|
91
|
+
},
|
|
92
|
+
"require": {
|
|
93
|
+
"types": "./dist/model/index.d.cts",
|
|
94
|
+
"default": "./dist/model/index.cjs"
|
|
95
|
+
}
|
|
66
96
|
}
|
|
67
97
|
},
|
|
68
98
|
"files": [
|
|
@@ -83,6 +113,7 @@
|
|
|
83
113
|
"test": "vitest run",
|
|
84
114
|
"test:watch": "vitest",
|
|
85
115
|
"test:coverage": "vitest run --coverage",
|
|
116
|
+
"bench:timeline": "yarn build && node --expose-gc benchmark/timeline-index.mjs",
|
|
86
117
|
"docs": "typedoc",
|
|
87
118
|
"prepare": "[ -d node_modules/typescript ] && tsup || true",
|
|
88
119
|
"prepublishOnly": "yarn build"
|
|
@@ -108,9 +108,11 @@ export function prepareTracks(tracks: ArrangementTrack[]): PreparedTrack[] {
|
|
|
108
108
|
voice,
|
|
109
109
|
sounding: voice.map((note, i) => {
|
|
110
110
|
const prev = i > 0 ? voice[i - 1] : undefined;
|
|
111
|
+
const prevEnd = prev === undefined ? undefined : prev.startBeat + prev.durationBeat;
|
|
112
|
+
const contiguous = prevEnd !== undefined && Math.abs(prevEnd - note.startBeat) <= EPS;
|
|
111
113
|
return {
|
|
112
114
|
pitch: note.pitch,
|
|
113
|
-
prevPitch: prev?.pitch,
|
|
115
|
+
prevPitch: contiguous ? prev?.pitch : undefined,
|
|
114
116
|
startBeat: note.startBeat,
|
|
115
117
|
endBeat: note.startBeat + note.durationBeat,
|
|
116
118
|
};
|
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import { isStrongBeat, parseTimeSignature, type TimeSignature } from '../../core/meter/index.js';
|
|
9
9
|
import type { KeyScale } from '../../core/types.js';
|
|
10
|
+
import {
|
|
11
|
+
assertGenerationBudget,
|
|
12
|
+
assertNoteEvents,
|
|
13
|
+
assertRange,
|
|
14
|
+
assertTimeSignature,
|
|
15
|
+
} from '../../core/validation/index.js';
|
|
10
16
|
import { chordPitchClasses } from '../../theory/chord/index.js';
|
|
11
17
|
import { evaluateSafety, NoteSafety, type SafetyProfile } from '../../theory/safety/index.js';
|
|
12
18
|
import { functionOf } from '../functional/index.js';
|
|
@@ -84,11 +90,16 @@ export function tensionCurve(
|
|
|
84
90
|
opts: ArrangementOptions & { step?: number } = {},
|
|
85
91
|
): TensionPoint[] {
|
|
86
92
|
const ts = opts.ts ?? parseTimeSignature('4/4');
|
|
93
|
+
assertTimeSignature(ts);
|
|
94
|
+
assertGenerationBudget(tracks.length, 'arrangement tracks');
|
|
95
|
+
for (let index = 0; index < tracks.length; index += 1) {
|
|
96
|
+
assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
|
|
97
|
+
allowNonPositiveDuration: true,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
87
100
|
const profile: SafetyProfile = opts.profile ?? 'pop';
|
|
88
101
|
const step = opts.step ?? 1;
|
|
89
|
-
|
|
90
|
-
throw new Error(`Invalid tension sampling step: ${step}`);
|
|
91
|
-
}
|
|
102
|
+
assertRange(step, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'tension sampling step');
|
|
92
103
|
|
|
93
104
|
const pooled = poolNotes(tracks);
|
|
94
105
|
const totalBeats = pooled.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
@@ -101,6 +112,7 @@ export function tensionCurve(
|
|
|
101
112
|
const prepared = prepareTracks(tracks);
|
|
102
113
|
const points: TensionPoint[] = [];
|
|
103
114
|
const sampleCount = Math.max(0, Math.ceil(totalBeats / step - EPS));
|
|
115
|
+
assertGenerationBudget(sampleCount, 'tension samples');
|
|
104
116
|
for (let i = 0; i < sampleCount; i += 1) {
|
|
105
117
|
const beat = i * step;
|
|
106
118
|
points.push({ beat, tension: sampleTension(prepared, timeline, key, ts, profile, beat) });
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import { isStrongBeat, parseTimeSignature, type TimeSignature } from '../../core/meter/index.js';
|
|
9
9
|
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
10
|
+
import {
|
|
11
|
+
assertGenerationBudget,
|
|
12
|
+
assertNoteEvents,
|
|
13
|
+
assertTimeSignature,
|
|
14
|
+
} from '../../core/validation/index.js';
|
|
10
15
|
import {
|
|
11
16
|
evaluateSafety,
|
|
12
17
|
NoteSafety,
|
|
@@ -65,6 +70,10 @@ export type Conflict = {
|
|
|
65
70
|
safety: NoteSafety;
|
|
66
71
|
/** {@link import('../safety/index.js').SafetyResult.reasons} bitmask. */
|
|
67
72
|
reasons: number;
|
|
73
|
+
/** Preferred stepwise resolution, when the safety evaluator provides one. */
|
|
74
|
+
resolveTo?: number;
|
|
75
|
+
/** Nearby fully safe replacement pitches, nearest first. */
|
|
76
|
+
suggestions?: number[];
|
|
68
77
|
rationale?: string;
|
|
69
78
|
};
|
|
70
79
|
|
|
@@ -143,8 +152,12 @@ function otherVoicesSounding(
|
|
|
143
152
|
continue;
|
|
144
153
|
}
|
|
145
154
|
const snap: VoiceSnapshot = { pitch: note.pitch };
|
|
146
|
-
|
|
147
|
-
|
|
155
|
+
// Motion reasons compare one real transition shared by both voices.
|
|
156
|
+
// A voice attacking exactly here contributes its adjacent predecessor;
|
|
157
|
+
// a sustained voice contributes the same pitch (oblique motion).
|
|
158
|
+
const previous = Math.abs(note.startBeat - beat) <= EPS ? note.prevPitch : note.pitch;
|
|
159
|
+
if (previous !== undefined) {
|
|
160
|
+
snap.prevPitch = previous;
|
|
148
161
|
}
|
|
149
162
|
out.push(snap);
|
|
150
163
|
}
|
|
@@ -211,6 +224,13 @@ export function analyzeArrangement(
|
|
|
211
224
|
opts: ArrangementOptions = {},
|
|
212
225
|
): ArrangementAnalysis {
|
|
213
226
|
const ts = opts.ts ?? parseTimeSignature('4/4');
|
|
227
|
+
assertTimeSignature(ts);
|
|
228
|
+
assertGenerationBudget(tracks.length, 'arrangement tracks');
|
|
229
|
+
for (let index = 0; index < tracks.length; index += 1) {
|
|
230
|
+
assertNoteEvents(tracks[index]?.notes ?? [], `tracks[${index}].notes`, {
|
|
231
|
+
allowNonPositiveDuration: true,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
214
234
|
const profile: SafetyProfile = opts.profile ?? 'pop';
|
|
215
235
|
const pooled = poolNotes(tracks);
|
|
216
236
|
|
|
@@ -242,11 +262,18 @@ export function analyzeArrangement(
|
|
|
242
262
|
),
|
|
243
263
|
);
|
|
244
264
|
|
|
245
|
-
for (
|
|
265
|
+
for (let noteIndex = 0; noteIndex < subVoice.voice.length; noteIndex += 1) {
|
|
266
|
+
const note = subVoice.voice[noteIndex];
|
|
267
|
+
const preparedNote = subVoice.sounding[noteIndex];
|
|
268
|
+
if (note === undefined || preparedNote === undefined) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
246
271
|
for (const beat of evaluationBeats(note, timeline)) {
|
|
272
|
+
const atOnset = Math.abs(beat - note.startBeat) <= EPS;
|
|
247
273
|
const result = evaluateSafety({
|
|
248
274
|
profile,
|
|
249
275
|
candidatePitch: note.pitch,
|
|
276
|
+
prevPitch: atOnset ? preparedNote.prevPitch : note.pitch,
|
|
250
277
|
chord: timeline.at(beat),
|
|
251
278
|
key,
|
|
252
279
|
otherVoices: otherVoicesSounding(prepared, t, v, beat),
|
|
@@ -263,6 +290,12 @@ export function analyzeArrangement(
|
|
|
263
290
|
if (result.rationale !== undefined) {
|
|
264
291
|
conflict.rationale = result.rationale;
|
|
265
292
|
}
|
|
293
|
+
if (result.resolveTo !== undefined) {
|
|
294
|
+
conflict.resolveTo = result.resolveTo;
|
|
295
|
+
}
|
|
296
|
+
if (result.suggestions !== undefined) {
|
|
297
|
+
conflict.suggestions = [...result.suggestions];
|
|
298
|
+
}
|
|
266
299
|
conflicts.push(conflict);
|
|
267
300
|
}
|
|
268
301
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { KeyScale } from '../../core/types.js';
|
|
8
|
+
import { assertFiniteNumber, assertGenerationBudget } from '../../core/validation/index.js';
|
|
8
9
|
import type { Chord, ChordQuality } from '../../theory/chord/index.js';
|
|
9
10
|
import { chordPitchClasses, chordQualities, makeChord } from '../../theory/chord/index.js';
|
|
10
11
|
import {
|
|
@@ -30,12 +31,24 @@ export type ChordMatch = {
|
|
|
30
31
|
extraPcs: number[];
|
|
31
32
|
/** True when the input pitch-class set equals the chord exactly. */
|
|
32
33
|
exact: boolean;
|
|
33
|
-
/** Inversion implied by
|
|
34
|
-
inversion: number;
|
|
34
|
+
/** Inversion implied by a known bass, or null for an unordered pitch-class set. */
|
|
35
|
+
inversion: number | null;
|
|
35
36
|
/** Bass pitch class when inverted (the lowest note is not the root). */
|
|
36
37
|
bassPc?: number;
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
/** Input interpretation for {@link detectChord} and {@link detectChordBest}. */
|
|
41
|
+
export type DetectChordOptions = {
|
|
42
|
+
/**
|
|
43
|
+
* `midi` uses the numerically lowest pitch as bass; `pitchClass` treats the
|
|
44
|
+
* input as unordered. `auto` (default) selects pitch-class mode only when all
|
|
45
|
+
* values lie in 0..11.
|
|
46
|
+
*/
|
|
47
|
+
input?: 'auto' | 'midi' | 'pitchClass';
|
|
48
|
+
/** Explicit bass pitch class, including for an unordered pitch-class set. */
|
|
49
|
+
bassPc?: number;
|
|
50
|
+
};
|
|
51
|
+
|
|
39
52
|
/**
|
|
40
53
|
* A candidate key interpretation of a pitch-class set.
|
|
41
54
|
*
|
|
@@ -58,6 +71,13 @@ function uniquePitchClasses(pitches: number[]): number[] {
|
|
|
58
71
|
return [...new Set(pitches.map(pitchClass))].sort((a, b) => a - b);
|
|
59
72
|
}
|
|
60
73
|
|
|
74
|
+
function assertPitches(pitches: number[]): void {
|
|
75
|
+
assertGenerationBudget(pitches.length, 'detection pitches');
|
|
76
|
+
for (let index = 0; index < pitches.length; index += 1) {
|
|
77
|
+
assertFiniteNumber(pitches[index] ?? Number.NaN, `pitches[${index}]`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
/**
|
|
62
82
|
* Identify chords matching a set of pitches.
|
|
63
83
|
*
|
|
@@ -78,12 +98,25 @@ function uniquePitchClasses(pitches: number[]): number[] {
|
|
|
78
98
|
* ```
|
|
79
99
|
* @category Recognition
|
|
80
100
|
*/
|
|
81
|
-
export function detectChord(pitches: number[]): ChordMatch[] {
|
|
101
|
+
export function detectChord(pitches: number[], opts: DetectChordOptions = {}): ChordMatch[] {
|
|
102
|
+
assertPitches(pitches);
|
|
103
|
+
if (opts.bassPc !== undefined) assertFiniteNumber(opts.bassPc, 'bassPc');
|
|
82
104
|
const input = uniquePitchClasses(pitches);
|
|
83
105
|
if (input.length === 0) {
|
|
84
106
|
return [];
|
|
85
107
|
}
|
|
86
|
-
const
|
|
108
|
+
const inputKind =
|
|
109
|
+
opts.input === undefined || opts.input === 'auto'
|
|
110
|
+
? pitches.every((pitch) => Number.isInteger(pitch) && pitch >= 0 && pitch <= 11)
|
|
111
|
+
? 'pitchClass'
|
|
112
|
+
: 'midi'
|
|
113
|
+
: opts.input;
|
|
114
|
+
const bassPc =
|
|
115
|
+
opts.bassPc !== undefined
|
|
116
|
+
? pitchClass(opts.bassPc)
|
|
117
|
+
: inputKind === 'midi'
|
|
118
|
+
? pitchClass(pitches.reduce((lowest, pitch) => Math.min(lowest, pitch), Infinity))
|
|
119
|
+
: undefined;
|
|
87
120
|
const inputSet = new Set(input);
|
|
88
121
|
const matches: ChordMatch[] = [];
|
|
89
122
|
const qualities = chordQualities();
|
|
@@ -100,8 +133,11 @@ export function detectChord(pitches: number[]): ChordMatch[] {
|
|
|
100
133
|
continue;
|
|
101
134
|
}
|
|
102
135
|
const extraPcs = input.filter((pc) => !toneSet.has(pc));
|
|
103
|
-
const bassIndex =
|
|
104
|
-
|
|
136
|
+
const bassIndex =
|
|
137
|
+
bassPc === undefined
|
|
138
|
+
? -1
|
|
139
|
+
: chord.intervals.findIndex((iv) => pitchClass(rootPc + iv) === bassPc);
|
|
140
|
+
const inversion = bassPc === undefined ? null : bassIndex > 0 ? bassIndex : 0;
|
|
105
141
|
const match: ChordMatch = {
|
|
106
142
|
rootPc,
|
|
107
143
|
quality,
|
|
@@ -110,7 +146,7 @@ export function detectChord(pitches: number[]): ChordMatch[] {
|
|
|
110
146
|
exact: extraPcs.length === 0 && missingPcs.length === 0,
|
|
111
147
|
inversion,
|
|
112
148
|
};
|
|
113
|
-
if (
|
|
149
|
+
if (bassPc !== undefined && bassPc !== rootPc) {
|
|
114
150
|
match.bassPc = bassPc;
|
|
115
151
|
}
|
|
116
152
|
matches.push(match);
|
|
@@ -146,8 +182,8 @@ export function detectChord(pitches: number[]): ChordMatch[] {
|
|
|
146
182
|
* ```
|
|
147
183
|
* @category Recognition
|
|
148
184
|
*/
|
|
149
|
-
export function detectChordBest(pitches: number[]): Chord | null {
|
|
150
|
-
const best = detectChord(pitches)[0];
|
|
185
|
+
export function detectChordBest(pitches: number[], opts: DetectChordOptions = {}): Chord | null {
|
|
186
|
+
const best = detectChord(pitches, opts)[0];
|
|
151
187
|
if (!best) {
|
|
152
188
|
return null;
|
|
153
189
|
}
|
|
@@ -183,6 +219,7 @@ const MINOR_MASK_VARIANTS = [NATURAL_MINOR_MASK, HARMONIC_MINOR_MASK, MELODIC_MI
|
|
|
183
219
|
* @category Recognition
|
|
184
220
|
*/
|
|
185
221
|
export function detectKey(pitches: number[]): KeyMatch[] {
|
|
222
|
+
assertPitches(pitches);
|
|
186
223
|
const input = uniquePitchClasses(pitches);
|
|
187
224
|
if (input.length === 0) {
|
|
188
225
|
return [];
|
|
@@ -161,11 +161,20 @@ function parseSimpleRoman(
|
|
|
161
161
|
return { rootPc, quality: canonical, inversion: 0 };
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
164
|
+
// Non-canonical spellings are limited to a complete quality marker followed
|
|
165
|
+
// by an optional major-seventh marker and a recognized figured bass. Never
|
|
166
|
+
// search substrings: `Vfoo` must not become diminished merely because it
|
|
167
|
+
// contains an "o", and unknown trailing text must be rejected in full.
|
|
168
|
+
const parsedSuffix = /^(o|°|dim|ø|\+|aug)?(maj7|M7)?(65|64|43|42|7|6|2)?$/.exec(suffix);
|
|
169
|
+
if (!parsedSuffix) {
|
|
170
|
+
throw new Error(`Unsupported suffix "${suffix}" in Roman numeral: ${text}`);
|
|
171
|
+
}
|
|
172
|
+
const qualityMarker = parsedSuffix[1] ?? '';
|
|
173
|
+
const isDim = qualityMarker === 'o' || qualityMarker === '°' || qualityMarker === 'dim';
|
|
174
|
+
const isAug = qualityMarker === '+' || qualityMarker === 'aug';
|
|
175
|
+
const isHalfDim = qualityMarker === 'ø';
|
|
176
|
+
const explicitMaj7 = parsedSuffix[2] !== undefined;
|
|
177
|
+
const figures = parsedSuffix[3] ?? '';
|
|
169
178
|
|
|
170
179
|
// Reject figure strings that are neither a known figured-bass inversion nor a
|
|
171
180
|
// canonical quality suffix rather than silently degrading to a root triad.
|
package/src/analyze/index.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type {
|
|
|
12
12
|
TrackRole,
|
|
13
13
|
} from './arrange/index.js';
|
|
14
14
|
export { analyzeArrangement, tensionCurve } from './arrange/index.js';
|
|
15
|
-
export type { ChordMatch, KeyMatch } from './detect/index.js';
|
|
15
|
+
export type { ChordMatch, DetectChordOptions, KeyMatch } from './detect/index.js';
|
|
16
16
|
export { detectChord, detectChordBest, detectKey } from './detect/index.js';
|
|
17
17
|
export type {
|
|
18
18
|
BorrowedSource,
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { createNoteEventIndex } from '../../core/event-index/index.js';
|
|
1
2
|
import type { TimeSignature } from '../../core/meter/index.js';
|
|
2
3
|
import { beatsPerBar, metricWeight, parseTimeSignature } from '../../core/meter/index.js';
|
|
3
4
|
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
5
|
+
import {
|
|
6
|
+
assertFiniteNumber,
|
|
7
|
+
assertGenerationBudget,
|
|
8
|
+
assertNoteEvents,
|
|
9
|
+
assertRange,
|
|
10
|
+
assertTimeSignature,
|
|
11
|
+
} from '../../core/validation/index.js';
|
|
4
12
|
import type { Chord, ChordSpan } from '../../theory/chord/index.js';
|
|
5
13
|
import { chordPitchClasses, makeChord } from '../../theory/chord/index.js';
|
|
6
14
|
import { isScaleTone, majorKey } from '../../theory/scale/index.js';
|
|
@@ -45,11 +53,24 @@ export type ChordTimeline = {
|
|
|
45
53
|
* @category Arrangement & Analysis
|
|
46
54
|
*/
|
|
47
55
|
export function chordTimelineFromChords(chords: ChordSpan[], totalBeats: number): ChordTimeline {
|
|
48
|
-
|
|
56
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'timeline totalBeats');
|
|
57
|
+
assertGenerationBudget(chords.length, 'timeline chords');
|
|
58
|
+
for (let index = 0; index < chords.length; index += 1) {
|
|
59
|
+
assertRange(
|
|
60
|
+
chords[index]?.startBeat ?? Number.NaN,
|
|
61
|
+
0,
|
|
62
|
+
Number.MAX_SAFE_INTEGER,
|
|
63
|
+
`chords[${index}].startBeat`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const sorted = [...chords]
|
|
67
|
+
.filter((chord) => chord.startBeat < totalBeats)
|
|
68
|
+
.sort((a, b) => a.startBeat - b.startBeat);
|
|
49
69
|
const segments: ChordSegment[] = sorted
|
|
50
70
|
.map((gc, i) => {
|
|
51
71
|
const next = sorted[i + 1];
|
|
52
|
-
const
|
|
72
|
+
const nextBeat = next ? next.startBeat : totalBeats;
|
|
73
|
+
const endBeat = Math.min(totalBeats, Math.max(gc.startBeat, nextBeat));
|
|
53
74
|
return {
|
|
54
75
|
startBeat: gc.startBeat,
|
|
55
76
|
endBeat,
|
|
@@ -64,6 +85,7 @@ export function chordTimelineFromChords(chords: ChordSpan[], totalBeats: number)
|
|
|
64
85
|
/** Build the `at(beat)` lookup over a segment list. */
|
|
65
86
|
function segmentLookup(segments: ChordSegment[]): (beat: number) => Chord | null {
|
|
66
87
|
return (beat) => {
|
|
88
|
+
assertFiniteNumber(beat, 'timeline query beat');
|
|
67
89
|
for (const seg of segments) {
|
|
68
90
|
if (beat >= seg.startBeat && beat < seg.endBeat) {
|
|
69
91
|
return seg.chord;
|
|
@@ -317,23 +339,37 @@ export function chordTimelineFromNotes(
|
|
|
317
339
|
opts: ChordTimelineOptions = {},
|
|
318
340
|
): ChordTimelineResult {
|
|
319
341
|
const ts = opts.ts ?? parseTimeSignature('4/4');
|
|
342
|
+
assertTimeSignature(ts);
|
|
320
343
|
const harmonicRhythm = opts.harmonicRhythm ?? beatsPerBar(ts);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
344
|
+
assertRange(harmonicRhythm, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, 'harmonic rhythm');
|
|
345
|
+
assertNoteEvents(notes, 'timeline notes', { allowNonPositiveDuration: true });
|
|
324
346
|
// Zero/negative-length notes never sound; drop them before any inference.
|
|
325
|
-
const
|
|
347
|
+
const soundingIndex = createNoteEventIndex(notes.filter((note) => note.durationBeat > 0));
|
|
348
|
+
const sounding = soundingIndex.notes.map(({ note }) => note);
|
|
326
349
|
const lastNoteEnd = sounding.reduce((end, n) => Math.max(end, n.startBeat + n.durationBeat), 0);
|
|
327
350
|
const totalBeats = opts.totalBeats ?? lastNoteEnd;
|
|
351
|
+
assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'timeline totalBeats');
|
|
328
352
|
const key = opts.key ?? detectKey(sounding.map((n) => n.pitch))[0]?.key ?? majorKey(0);
|
|
329
353
|
|
|
330
354
|
const segments: ChordSegment[] = [];
|
|
331
355
|
const segmentConfidence: number[] = [];
|
|
332
356
|
const windowCount = Math.max(0, Math.ceil(totalBeats / harmonicRhythm - EPS));
|
|
357
|
+
assertGenerationBudget(windowCount, 'timeline windows');
|
|
358
|
+
const windowNotes: NoteEvent[][] = Array.from({ length: windowCount }, () => []);
|
|
359
|
+
let memberships = 0;
|
|
360
|
+
for (const indexed of soundingIndex.notes) {
|
|
361
|
+
const first = Math.max(0, Math.floor(indexed.note.startBeat / harmonicRhythm));
|
|
362
|
+
const lastExclusive = Math.min(windowCount, Math.ceil(indexed.endBeat / harmonicRhythm));
|
|
363
|
+
memberships += Math.max(0, lastExclusive - first);
|
|
364
|
+
assertGenerationBudget(memberships, 'timeline note-to-window memberships');
|
|
365
|
+
for (let window = first; window < lastExclusive; window += 1) {
|
|
366
|
+
windowNotes[window]?.push(indexed.note);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
333
369
|
for (let i = 0; i < windowCount; i += 1) {
|
|
334
370
|
const start = i * harmonicRhythm;
|
|
335
371
|
const end = Math.min(start + harmonicRhythm, totalBeats);
|
|
336
|
-
const inferred = analyzeWindow(
|
|
372
|
+
const inferred = analyzeWindow(windowNotes[i] ?? [], start, end, ts, key);
|
|
337
373
|
if (!inferred) {
|
|
338
374
|
continue;
|
|
339
375
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { KeyScale, NoteEvent } from '../../core/types.js';
|
|
2
|
+
import { assertNoteEvents } from '../../core/validation/index.js';
|
|
2
3
|
import type { Chord } from '../../theory/chord/index.js';
|
|
3
4
|
import { chordPitchClasses, chordToneRole } from '../../theory/chord/index.js';
|
|
4
5
|
import {
|
|
@@ -165,6 +166,7 @@ export function analyzeVoice(
|
|
|
165
166
|
key: KeyScale,
|
|
166
167
|
otherVoicesAtBeat: (beat: number) => VoiceSnapshot[],
|
|
167
168
|
): AnalyzedNote[] {
|
|
169
|
+
assertNoteEvents(voice, 'voice notes', { allowNonPositiveDuration: true });
|
|
168
170
|
const result: AnalyzedNote[] = [];
|
|
169
171
|
|
|
170
172
|
for (let i = 0; i < voice.length; i += 1) {
|
|
@@ -177,10 +179,17 @@ export function analyzeVoice(
|
|
|
177
179
|
// A neighboring note sharing this note's onset is a simultaneous cluster
|
|
178
180
|
// member, not a melodic predecessor/successor; treating it as one would
|
|
179
181
|
// fabricate passing/suspension figures, so it is dropped here.
|
|
182
|
+
const prevEnd =
|
|
183
|
+
prevNote === undefined
|
|
184
|
+
? Number.NEGATIVE_INFINITY
|
|
185
|
+
: prevNote.startBeat + prevNote.durationBeat;
|
|
186
|
+
const noteEnd = note.startBeat + note.durationBeat;
|
|
180
187
|
const prev =
|
|
181
|
-
prevNote !== undefined &&
|
|
188
|
+
prevNote !== undefined && Math.abs(prevEnd - note.startBeat) <= EPS ? prevNote : undefined;
|
|
182
189
|
const next =
|
|
183
|
-
nextNote !== undefined &&
|
|
190
|
+
nextNote !== undefined && Math.abs(noteEnd - nextNote.startBeat) <= EPS
|
|
191
|
+
? nextNote
|
|
192
|
+
: undefined;
|
|
184
193
|
const chord = chordAtBeat(note.startBeat);
|
|
185
194
|
const labels: TheoryLabel[] = [];
|
|
186
195
|
const member = isChordMember(note.pitch, chord);
|