@libraz/libcantus 0.9.4 → 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 (120) hide show
  1. package/README.md +27 -18
  2. package/README_ja.md +268 -0
  3. package/dist/analyze/index.cjs +7 -7
  4. package/dist/analyze/index.d.cts +9 -99
  5. package/dist/analyze/index.d.ts +9 -99
  6. package/dist/analyze/index.js +6 -6
  7. package/dist/{chunk-36N73KZ3.cjs → chunk-2A2QMA74.cjs} +80 -65
  8. package/dist/chunk-2A2QMA74.cjs.map +1 -0
  9. package/dist/{chunk-YLEMGEFH.js → chunk-2PTFGVD7.js} +50 -35
  10. package/dist/chunk-2PTFGVD7.js.map +1 -0
  11. package/dist/{chunk-ZEJR4TKL.cjs → chunk-357ZDPVU.cjs} +36 -47
  12. package/dist/chunk-357ZDPVU.cjs.map +1 -0
  13. package/dist/{chunk-EHVEBWM5.js → chunk-3KX76RLG.js} +136 -37
  14. package/dist/chunk-3KX76RLG.js.map +1 -0
  15. package/dist/{chunk-RC6GBMCD.cjs → chunk-4VZ5MZAM.cjs} +155 -111
  16. package/dist/chunk-4VZ5MZAM.cjs.map +1 -0
  17. package/dist/{chunk-RK35RIN7.js → chunk-7EONYVI5.js} +115 -28
  18. package/dist/chunk-7EONYVI5.js.map +1 -0
  19. package/dist/{chunk-GQSGS2N4.js → chunk-7JEP3MFY.js} +5 -16
  20. package/dist/chunk-7JEP3MFY.js.map +1 -0
  21. package/dist/{chunk-7NKDPF74.js → chunk-AID75RN3.js} +203 -104
  22. package/dist/chunk-AID75RN3.js.map +1 -0
  23. package/dist/{chunk-FNDWXRRI.cjs → chunk-BSLTLGM3.cjs} +55 -37
  24. package/dist/chunk-BSLTLGM3.cjs.map +1 -0
  25. package/dist/{chunk-NP4NA2FB.cjs → chunk-CU6VG2YM.cjs} +39 -36
  26. package/dist/chunk-CU6VG2YM.cjs.map +1 -0
  27. package/dist/{chunk-NYYWMV2C.js → chunk-DZMHIGI4.js} +27 -17
  28. package/dist/chunk-DZMHIGI4.js.map +1 -0
  29. package/dist/{chunk-I24MV65U.js → chunk-EDOTEABK.js} +18 -15
  30. package/dist/{chunk-I24MV65U.js.map → chunk-EDOTEABK.js.map} +1 -1
  31. package/dist/{chunk-NKIBQ6PT.js → chunk-HN2OEUN2.js} +7 -4
  32. package/dist/chunk-HN2OEUN2.js.map +1 -0
  33. package/dist/{chunk-D2FAJBYL.js → chunk-KBTTEX7Q.js} +30 -12
  34. package/dist/chunk-KBTTEX7Q.js.map +1 -0
  35. package/dist/{chunk-CAN76NCU.cjs → chunk-KQN2ZYND.cjs} +147 -102
  36. package/dist/chunk-KQN2ZYND.cjs.map +1 -0
  37. package/dist/{chunk-4HLLZPXT.cjs → chunk-MPTVJIVG.cjs} +350 -251
  38. package/dist/chunk-MPTVJIVG.cjs.map +1 -0
  39. package/dist/{chunk-RUMU3UA6.cjs → chunk-MTMZTMWM.cjs} +12 -12
  40. package/dist/{chunk-RUMU3UA6.cjs.map → chunk-MTMZTMWM.cjs.map} +1 -1
  41. package/dist/{chunk-KMHOICIY.js → chunk-NNB67XA2.js} +90 -45
  42. package/dist/chunk-NNB67XA2.js.map +1 -0
  43. package/dist/{chunk-LW53AHQU.cjs → chunk-OL3IHHSJ.cjs} +228 -141
  44. package/dist/chunk-OL3IHHSJ.cjs.map +1 -0
  45. package/dist/{chunk-7MGZ7D62.cjs → chunk-PI3UXRWK.cjs} +43 -13
  46. package/dist/chunk-PI3UXRWK.cjs.map +1 -0
  47. package/dist/{chunk-XEBG3G53.js → chunk-R2VVFAIN.js} +42 -12
  48. package/dist/chunk-R2VVFAIN.js.map +1 -0
  49. package/dist/{chunk-Q6OP4U43.cjs → chunk-WFDXOLUB.cjs} +50 -40
  50. package/dist/chunk-WFDXOLUB.cjs.map +1 -0
  51. package/dist/{chunk-NVE34YGZ.cjs → chunk-WTAN4ACG.cjs} +212 -113
  52. package/dist/chunk-WTAN4ACG.cjs.map +1 -0
  53. package/dist/{chunk-FCANXBAT.cjs → chunk-YFJXS3AU.cjs} +8 -5
  54. package/dist/chunk-YFJXS3AU.cjs.map +1 -0
  55. package/dist/{chunk-Q2XUP227.js → chunk-YUE6TJ4M.js} +89 -45
  56. package/dist/chunk-YUE6TJ4M.js.map +1 -0
  57. package/dist/{chunk-HKGYCQ34.js → chunk-ZGLJM4A4.js} +2 -2
  58. package/dist/core/index.cjs +15 -5
  59. package/dist/core/index.cjs.map +1 -1
  60. package/dist/core/index.d.cts +14 -4
  61. package/dist/core/index.d.ts +14 -4
  62. package/dist/core/index.js +14 -4
  63. package/dist/{function-DNugAOrt.d.cts → function-CxixTB02.d.cts} +98 -7
  64. package/dist/{function-BtiZb9i9.d.ts → function-DTCt675z.d.ts} +98 -7
  65. package/dist/generate/index.cjs +10 -10
  66. package/dist/generate/index.d.cts +21 -21
  67. package/dist/generate/index.d.ts +21 -21
  68. package/dist/generate/index.js +9 -9
  69. package/dist/{index-tJhkmQgE.d.cts → index-B2Ulxxpo.d.cts} +3 -2
  70. package/dist/{index-Be74mcon.d.cts → index-BkYJ0xMq.d.cts} +2 -2
  71. package/dist/{index-6VR8-kit.d.ts → index-BuaB-ypt.d.ts} +3 -2
  72. package/dist/{index-70p168VM.d.ts → index-CAN7v-kw.d.ts} +9 -2
  73. package/dist/{index-C83mZI1P.d.ts → index-COw1Ky9g.d.ts} +1 -1
  74. package/dist/{index-tSQh-xwY.d.ts → index-Cfx8xgH0.d.ts} +1 -1
  75. package/dist/{index-C5UBbQ4G.d.ts → index-DRtIhlJj.d.ts} +2 -2
  76. package/dist/{index-BtXT6UQ0.d.cts → index-alrRninU.d.cts} +9 -2
  77. package/dist/{index-y_ak9_Hi.d.cts → index-oTQbYYIs.d.cts} +1 -1
  78. package/dist/{index-OG65ocOr.d.ts → index-tR9jTn26.d.cts} +4 -1
  79. package/dist/{index-OG65ocOr.d.cts → index-tR9jTn26.d.ts} +4 -1
  80. package/dist/{index-D6b6na69.d.cts → index-x2vcsduE.d.cts} +1 -1
  81. package/dist/index.cjs +24 -14
  82. package/dist/index.cjs.map +1 -1
  83. package/dist/index.d.cts +11 -11
  84. package/dist/index.d.ts +11 -11
  85. package/dist/index.js +23 -13
  86. package/dist/model/index.cjs +7 -7
  87. package/dist/model/index.d.cts +48 -16
  88. package/dist/model/index.d.ts +48 -16
  89. package/dist/model/index.js +6 -6
  90. package/dist/{styled-DIRkk9Uz.d.ts → styled-Bl0ZK28h.d.ts} +40 -3
  91. package/dist/{styled-DJbkJQfj.d.cts → styled-Crv6SA4S.d.cts} +40 -3
  92. package/dist/theory/index.cjs +6 -6
  93. package/dist/theory/index.d.cts +9 -9
  94. package/dist/theory/index.d.ts +9 -9
  95. package/dist/theory/index.js +5 -5
  96. package/package.json +12 -9
  97. package/dist/chunk-36N73KZ3.cjs.map +0 -1
  98. package/dist/chunk-4HLLZPXT.cjs.map +0 -1
  99. package/dist/chunk-7MGZ7D62.cjs.map +0 -1
  100. package/dist/chunk-7NKDPF74.js.map +0 -1
  101. package/dist/chunk-CAN76NCU.cjs.map +0 -1
  102. package/dist/chunk-D2FAJBYL.js.map +0 -1
  103. package/dist/chunk-EHVEBWM5.js.map +0 -1
  104. package/dist/chunk-FCANXBAT.cjs.map +0 -1
  105. package/dist/chunk-FNDWXRRI.cjs.map +0 -1
  106. package/dist/chunk-GQSGS2N4.js.map +0 -1
  107. package/dist/chunk-KMHOICIY.js.map +0 -1
  108. package/dist/chunk-LW53AHQU.cjs.map +0 -1
  109. package/dist/chunk-NKIBQ6PT.js.map +0 -1
  110. package/dist/chunk-NP4NA2FB.cjs.map +0 -1
  111. package/dist/chunk-NVE34YGZ.cjs.map +0 -1
  112. package/dist/chunk-NYYWMV2C.js.map +0 -1
  113. package/dist/chunk-Q2XUP227.js.map +0 -1
  114. package/dist/chunk-Q6OP4U43.cjs.map +0 -1
  115. package/dist/chunk-RC6GBMCD.cjs.map +0 -1
  116. package/dist/chunk-RK35RIN7.js.map +0 -1
  117. package/dist/chunk-XEBG3G53.js.map +0 -1
  118. package/dist/chunk-YLEMGEFH.js.map +0 -1
  119. package/dist/chunk-ZEJR4TKL.cjs.map +0 -1
  120. /package/dist/{chunk-HKGYCQ34.js.map → chunk-ZGLJM4A4.js.map} +0 -0
@@ -7,8 +7,8 @@ import {
7
7
  tensionCurve,
8
8
  tensionCurveFrom,
9
9
  toVoiceNotes
10
- } from "../chunk-Q2XUP227.js";
11
- import "../chunk-YLEMGEFH.js";
10
+ } from "../chunk-YUE6TJ4M.js";
11
+ import "../chunk-2PTFGVD7.js";
12
12
  import {
13
13
  analyzeChord,
14
14
  borrowedSource,
@@ -26,12 +26,12 @@ import {
26
26
  parallelKey,
27
27
  romanToChord,
28
28
  secondaryDominant
29
- } from "../chunk-KMHOICIY.js";
29
+ } from "../chunk-NNB67XA2.js";
30
30
  import {
31
31
  NoteSafety
32
- } from "../chunk-NYYWMV2C.js";
33
- import "../chunk-D2FAJBYL.js";
34
- import "../chunk-XEBG3G53.js";
32
+ } from "../chunk-DZMHIGI4.js";
33
+ import "../chunk-KBTTEX7Q.js";
34
+ import "../chunk-R2VVFAIN.js";
35
35
  export {
36
36
  NoteSafety,
37
37
  analyzeArrangement,
@@ -11,7 +11,7 @@
11
11
 
12
12
 
13
13
 
14
- var _chunk7MGZ7D62cjs = require('./chunk-7MGZ7D62.cjs');
14
+ var _chunkPI3UXRWKcjs = require('./chunk-PI3UXRWK.cjs');
15
15
 
16
16
  // src/core/meter/index.ts
17
17
  var EPS = 1e-9;
@@ -23,35 +23,37 @@ function isMultiple(value, unit) {
23
23
  return Math.abs(ratio - Math.round(ratio)) < EPS;
24
24
  }
25
25
  function parseTimeSignature(text) {
26
- const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(text);
26
+ const match = /^\s*((?:\d+\s*\+\s*)*\d+)\s*\/\s*(\d+)\s*$/.exec(text);
27
27
  if (!match) {
28
- throw new (0, _chunk7MGZ7D62cjs.InvalidInputError)(`Invalid time signature: ${text}`);
28
+ throw new (0, _chunkPI3UXRWKcjs.InvalidInputError)(`Invalid time signature: ${text}`);
29
29
  }
30
- const numerator = Number.parseInt(_nullishCoalesce(match[1], () => ( "")), 10);
30
+ const groupTokens = (_nullishCoalesce(match[1], () => ( ""))).split("+").map((token) => Number.parseInt(token.trim(), 10));
31
+ const numerator = groupTokens.reduce((sum, value) => sum + value, 0);
31
32
  const denominator = Number.parseInt(_nullishCoalesce(match[2], () => ( "")), 10);
32
33
  if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator) || numerator <= 0 || denominator <= 0) {
33
- throw new (0, _chunk7MGZ7D62cjs.InvalidInputError)(`Invalid time signature: ${text}`);
34
+ throw new (0, _chunkPI3UXRWKcjs.InvalidInputError)(`Invalid time signature: ${text}`);
34
35
  }
35
- return _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, { numerator, denominator }, `time signature ${text}`);
36
+ const grouping = groupTokens.length > 1 ? groupTokens : void 0;
37
+ return _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, { numerator, denominator, grouping }, `time signature ${text}`);
36
38
  }
37
39
  function formatTimeSignature(ts, opts = {}) {
38
- _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, ts);
40
+ _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, ts);
39
41
  if (opts.grouping === true && ts.grouping !== void 0) {
40
42
  return `${ts.grouping.join("+")}/${ts.denominator}`;
41
43
  }
42
44
  return `${ts.numerator}/${ts.denominator}`;
43
45
  }
44
46
  function isCompound(ts) {
45
- _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, ts);
46
- return _chunk7MGZ7D62cjs.isCompoundNumerator.call(void 0, ts.numerator) && !_chunk7MGZ7D62cjs.isAdditiveReading.call(void 0, ts);
47
+ _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, ts);
48
+ return _chunkPI3UXRWKcjs.isCompoundNumerator.call(void 0, ts.numerator) && !_chunkPI3UXRWKcjs.isAdditiveReading.call(void 0, ts);
47
49
  }
48
50
  function beatsPerBar(ts) {
49
- _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, ts);
50
- return _chunk7MGZ7D62cjs.barBeatsOf.call(void 0, ts);
51
+ _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, ts);
52
+ return _chunkPI3UXRWKcjs.barBeatsOf.call(void 0, ts);
51
53
  }
52
54
  function pulsesPerBar(ts) {
53
- _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, ts);
54
- return _chunk7MGZ7D62cjs.pulseCountOf.call(void 0, ts);
55
+ _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, ts);
56
+ return _chunkPI3UXRWKcjs.pulseCountOf.call(void 0, ts);
55
57
  }
56
58
  function isGroupHead(grouping, pulseIndex) {
57
59
  let acc = 0;
@@ -64,43 +66,49 @@ function isGroupHead(grouping, pulseIndex) {
64
66
  return false;
65
67
  }
66
68
  function beatToBarPosition(beatInQuarters, ts) {
67
- _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
68
- _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, ts);
69
- const barLen = _chunk7MGZ7D62cjs.barBeatsOf.call(void 0, ts);
69
+ _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
70
+ _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, ts);
71
+ const barLen = _chunkPI3UXRWKcjs.barBeatsOf.call(void 0, ts);
70
72
  const bar = Math.floor(beatInQuarters / barLen);
71
73
  return { bar, beat: beatInQuarters - bar * barLen };
72
74
  }
73
75
  function pulseBeats(ts) {
74
- _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, ts);
75
- return _chunk7MGZ7D62cjs.pulseBeatsOf.call(void 0, ts);
76
+ _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, ts);
77
+ return _chunkPI3UXRWKcjs.pulseBeatsOf.call(void 0, ts);
76
78
  }
77
79
  function barPositionToPulse(pos, ts) {
78
- _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, pos.beat, "position.beat");
79
- _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, ts);
80
- return pos.beat / _chunk7MGZ7D62cjs.pulseBeatsOf.call(void 0, ts) + 1;
80
+ _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, pos.beat, "position.beat");
81
+ _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, ts);
82
+ return pos.beat / _chunkPI3UXRWKcjs.pulseBeatsOf.call(void 0, ts) + 1;
81
83
  }
82
84
  function formatBarPosition(beatInQuarters, ts, decimals = 2) {
85
+ _chunkPI3UXRWKcjs.assertInteger.call(void 0, decimals, "decimals", 0, 100);
83
86
  const position = beatToBarPosition(beatInQuarters, ts);
84
87
  const pulse = barPositionToPulse(position, ts);
85
88
  const rounded = Number(pulse.toFixed(decimals));
86
- return `${position.bar + 1}.${rounded}`;
89
+ if (rounded >= pulsesPerBar(ts) + 1) {
90
+ return `${position.bar + 2}.1`;
91
+ }
92
+ const wholePulse = Math.floor(rounded);
93
+ const fraction = Number((rounded - wholePulse).toFixed(decimals));
94
+ return fraction === 0 ? `${position.bar + 1}.${wholePulse}` : `${position.bar + 1}.${wholePulse}+${fraction}`;
87
95
  }
88
96
  function barPositionToBeat(pos, ts) {
89
- _chunk7MGZ7D62cjs.assertInteger.call(void 0, pos.bar, "bar position bar");
90
- _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, pos.beat, "bar position beat");
91
- _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, ts);
92
- return pos.bar * _chunk7MGZ7D62cjs.barBeatsOf.call(void 0, ts) + pos.beat;
97
+ _chunkPI3UXRWKcjs.assertInteger.call(void 0, pos.bar, "bar position bar");
98
+ _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, pos.beat, "bar position beat");
99
+ _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, ts);
100
+ return pos.bar * _chunkPI3UXRWKcjs.barBeatsOf.call(void 0, ts) + pos.beat;
93
101
  }
94
102
  function metricWeight(beatInQuarters, ts) {
95
- _chunk7MGZ7D62cjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
96
- _chunk7MGZ7D62cjs.assertTimeSignature.call(void 0, ts);
97
- const barLen = _chunk7MGZ7D62cjs.barBeatsOf.call(void 0, ts);
103
+ _chunkPI3UXRWKcjs.assertFiniteNumber.call(void 0, beatInQuarters, "beat");
104
+ _chunkPI3UXRWKcjs.assertTimeSignature.call(void 0, ts);
105
+ const barLen = _chunkPI3UXRWKcjs.barBeatsOf.call(void 0, ts);
98
106
  const beat = beatInQuarters - Math.floor(beatInQuarters / barLen) * barLen;
99
- const pulse = _chunk7MGZ7D62cjs.pulseBeatsOf.call(void 0, ts);
107
+ const pulse = _chunkPI3UXRWKcjs.pulseBeatsOf.call(void 0, ts);
100
108
  if (!isMultiple(beat, pulse)) {
101
109
  return 0;
102
110
  }
103
- const pulses = _chunk7MGZ7D62cjs.pulseCountOf.call(void 0, ts);
111
+ const pulses = _chunkPI3UXRWKcjs.pulseCountOf.call(void 0, ts);
104
112
  const pulseIndex = Math.round(beat / pulse) % pulses;
105
113
  if (pulseIndex === 0) {
106
114
  return 3;
@@ -118,8 +126,8 @@ function isStrongBeat(beatInQuarters, ts) {
118
126
  return metricWeight(beatInQuarters, ts) >= 2;
119
127
  }
120
128
  function tuplet(totalBeats, count) {
121
- _chunk7MGZ7D62cjs.assertRange.call(void 0, totalBeats, 0, Number.MAX_SAFE_INTEGER, "tuplet total beats");
122
- _chunk7MGZ7D62cjs.assertPositiveInt.call(void 0, count, "tuplet count");
129
+ _chunkPI3UXRWKcjs.assertRange.call(void 0, totalBeats, 0, Number.MAX_SAFE_INTEGER, "tuplet total beats");
130
+ _chunkPI3UXRWKcjs.assertPositiveInt.call(void 0, count, "tuplet count");
123
131
  return new Array(count).fill(totalBeats / count);
124
132
  }
125
133
 
@@ -139,50 +147,57 @@ function upperBound(values, beat) {
139
147
  return low;
140
148
  }
141
149
  function createNoteEventIndex(events, options = {}) {
142
- _chunk7MGZ7D62cjs.assertNoteEvents.call(void 0, events, _nullishCoalesce(options.name, () => ( "note events")), options);
150
+ _chunkPI3UXRWKcjs.assertNoteEvents.call(void 0, events, _nullishCoalesce(options.name, () => ( "note events")), options);
143
151
  const tieBreak = _nullishCoalesce(options.tieBreak, () => ( "highest"));
144
- const notes = events.map((note, originalIndex) => ({
145
- note,
146
- originalIndex,
147
- endBeat: note.startBeat + note.durationBeat
148
- })).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
149
- let prefixMaxEnd;
150
- const maxEndUpTo = (index) => {
151
- if (prefixMaxEnd === void 0) {
152
- const prefix = [];
153
- let maxEnd = Number.NEGATIVE_INFINITY;
154
- for (const indexed of notes) {
155
- maxEnd = Math.max(maxEnd, indexed.endBeat);
156
- prefix.push(maxEnd);
152
+ const notes = Object.freeze(
153
+ events.map((note, originalIndex) => ({
154
+ note: Object.freeze({ ...note }),
155
+ originalIndex,
156
+ endBeat: note.startBeat + note.durationBeat
157
+ })).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex)
158
+ );
159
+ let treeSize = 1;
160
+ while (treeSize < notes.length) treeSize *= 2;
161
+ const maxEndTree = new Array(treeSize * 2).fill(Number.NEGATIVE_INFINITY);
162
+ for (let index = 0; index < notes.length; index += 1) {
163
+ maxEndTree[treeSize + index] = _nullishCoalesce(_optionalChain([notes, 'access', _4 => _4[index], 'optionalAccess', _5 => _5.endBeat]), () => ( Number.NEGATIVE_INFINITY));
164
+ }
165
+ for (let index = treeSize - 1; index > 0; index -= 1) {
166
+ maxEndTree[index] = Math.max(
167
+ _nullishCoalesce(maxEndTree[index * 2], () => ( Number.NEGATIVE_INFINITY)),
168
+ _nullishCoalesce(maxEndTree[index * 2 + 1], () => ( Number.NEGATIVE_INFINITY))
169
+ );
170
+ }
171
+ const latestActiveIndex = (exclusive, beat) => {
172
+ const find = (node, start, end) => {
173
+ if (start >= exclusive || (_nullishCoalesce(maxEndTree[node], () => ( Number.NEGATIVE_INFINITY))) <= beat + EPS2) {
174
+ return -1;
157
175
  }
158
- prefixMaxEnd = prefix;
159
- }
160
- return _nullishCoalesce(prefixMaxEnd[index], () => ( Number.NEGATIVE_INFINITY));
176
+ if (end - start === 1) return start < notes.length ? start : -1;
177
+ const mid = Math.floor((start + end) / 2);
178
+ const right = find(node * 2 + 1, mid, end);
179
+ return right >= 0 ? right : find(node * 2, start, mid);
180
+ };
181
+ return find(1, 0, treeSize);
161
182
  };
162
183
  return {
163
184
  notes,
164
185
  at(beat) {
165
- let index = upperBound(notes, beat + EPS2) - 1;
186
+ const index = latestActiveIndex(upperBound(notes, beat + EPS2), beat);
187
+ if (index < 0) return void 0;
188
+ const onset = _optionalChain([notes, 'access', _6 => _6[index], 'optionalAccess', _7 => _7.note, 'access', _8 => _8.startBeat]);
166
189
  let best;
167
- while (index >= 0) {
168
- if (maxEndUpTo(index) <= beat + EPS2) {
169
- break;
170
- }
171
- const indexed = notes[index];
172
- index -= 1;
190
+ for (let candidateIndex = index; candidateIndex >= 0; candidateIndex -= 1) {
191
+ const indexed = notes[candidateIndex];
192
+ if (indexed !== void 0 && indexed.note.startBeat !== onset) break;
173
193
  if (indexed === void 0 || indexed.note.durationBeat <= 0 || indexed.note.startBeat - EPS2 > beat || beat >= indexed.endBeat - EPS2) {
174
194
  continue;
175
195
  }
176
196
  if (best === void 0) {
177
197
  best = indexed;
178
- if (tieBreak === "last") {
179
- break;
180
- }
181
198
  continue;
182
199
  }
183
- if (Math.abs(indexed.note.startBeat - best.note.startBeat) >= EPS2) {
184
- break;
185
- }
200
+ if (tieBreak === "last") continue;
186
201
  const higher = indexed.note.pitch > best.note.pitch;
187
202
  if (tieBreak === "highest" ? higher : !higher) {
188
203
  best = indexed;
@@ -209,7 +224,7 @@ function createNoteEventIndex(events, options = {}) {
209
224
  let index = upperBound(notes, startBeat + EPS2);
210
225
  while (index < notes.length) {
211
226
  const indexed = notes[index];
212
- const onset = _optionalChain([indexed, 'optionalAccess', _4 => _4.note, 'access', _5 => _5.startBeat]);
227
+ const onset = _optionalChain([indexed, 'optionalAccess', _9 => _9.note, 'access', _10 => _10.startBeat]);
213
228
  if (indexed === void 0 || onset === void 0 || onset >= endBeat - EPS2) {
214
229
  break;
215
230
  }
@@ -243,4 +258,4 @@ function createNoteEventIndex(events, options = {}) {
243
258
 
244
259
 
245
260
  exports.parseTimeSignature = parseTimeSignature; exports.formatTimeSignature = formatTimeSignature; exports.isCompound = isCompound; exports.beatsPerBar = beatsPerBar; exports.pulsesPerBar = pulsesPerBar; exports.beatToBarPosition = beatToBarPosition; exports.pulseBeats = pulseBeats; exports.barPositionToPulse = barPositionToPulse; exports.formatBarPosition = formatBarPosition; exports.barPositionToBeat = barPositionToBeat; exports.metricWeight = metricWeight; exports.isStrongBeat = isStrongBeat; exports.tuplet = tuplet; exports.createNoteEventIndex = createNoteEventIndex;
246
- //# sourceMappingURL=chunk-36N73KZ3.cjs.map
261
+ //# sourceMappingURL=chunk-2A2QMA74.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/libcantus/libcantus/dist/chunk-2A2QMA74.cjs","../src/core/meter/index.ts","../src/core/event-index/index.ts"],"names":["EPS"],"mappings":"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","file":"/home/runner/work/libcantus/libcantus/dist/chunk-2A2QMA74.cjs","sourcesContent":[null,"/**\n * Meter and time signatures: bar/beat arithmetic, simple vs. compound meters,\n * metric-accent hierarchy, and tuplet subdivision.\n *\n * Positions and durations are measured in quarter-note beats, matching the rest\n * of the library's beat convention (four quarter-note beats per bar in 4/4).\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n assertTimeSignature,\n} from '../validation/index.js';\nimport {\n barBeatsOf,\n isAdditiveReading,\n isCompoundNumerator,\n pulseBeatsOf,\n pulseCountOf,\n} from './internal.js';\n\n/**\n * A time signature as a numerator over a note-value denominator.\n *\n * @category Rhythm & Meter\n */\nexport type TimeSignature = {\n numerator: number;\n denominator: number;\n /**\n * Optional additive grouping of the bar's main pulses, as the felt-beat\n * lengths in pulses — e.g. `[2, 2, 3]` for a 2+2+3 reading of 7/8, or\n * `[3, 2]` for 5/8. The entries must be positive integers summing to the\n * numerator.\n *\n * A compound numerator accepts both readings. 9/8 groups its three dotted\n * pulses as `[1, 1, 1]`, or reads additively as `[2, 2, 2, 3]` quavers; a\n * grouping that sums to the numerator selects the additive reading, so\n * aksak and other additive metres are expressible. {@link pulsesPerBar}\n * and {@link isCompound} follow whichever reading the grouping selects.\n *\n * When present, the head pulse of each group (other than the downbeat) is\n * treated as a secondary strong pulse by\n * {@link metricWeight}/{@link isStrongBeat}; when absent, all main pulses\n * are weighted equally as flat, evenly divided pulses.\n */\n grouping?: number[];\n};\n\n/**\n * A position expressed as a bar index and a quarter-note offset within the bar.\n *\n * @category Rhythm & Meter\n */\nexport type BarPosition = {\n /** 0-based bar index. {@link formatBarPosition} renders it 1-based. */\n bar: number;\n /**\n * Quarter-note offset from the start of the bar — not the felt beat. In 6/8\n * the second felt beat is at `beat: 1.5`; {@link barPositionToPulse} converts\n * to felt-beat numbering.\n */\n beat: number;\n};\n\nconst EPS = 1e-9;\n\n/** Whether `value` is an integer multiple of `unit` (within a float tolerance). */\nfunction isMultiple(value: number, unit: number): boolean {\n if (unit === 0) {\n return false;\n }\n const ratio = value / unit;\n return Math.abs(ratio - Math.round(ratio)) < EPS;\n}\n\n/**\n * Parse a time signature such as `\"4/4\"` or `\"6/8\"`.\n *\n * @param text The signature text.\n * @returns The parsed time signature.\n * @throws If the text is not `n/d` with positive integers.\n * @example\n * ```ts\n * import { parseTimeSignature } from '@libraz/libcantus';\n * parseTimeSignature('6/8'); // { numerator: 6, denominator: 8 }\n * ```\n * @category Rhythm & Meter\n */\nexport function parseTimeSignature(text: string): TimeSignature {\n const match = /^\\s*((?:\\d+\\s*\\+\\s*)*\\d+)\\s*\\/\\s*(\\d+)\\s*$/.exec(text);\n if (!match) {\n throw new InvalidInputError(`Invalid time signature: ${text}`);\n }\n const groupTokens = (match[1] ?? '').split('+').map((token) => Number.parseInt(token.trim(), 10));\n const numerator = groupTokens.reduce((sum, value) => sum + value, 0);\n const denominator = Number.parseInt(match[2] ?? '', 10);\n if (\n !Number.isSafeInteger(numerator) ||\n !Number.isSafeInteger(denominator) ||\n numerator <= 0 ||\n denominator <= 0\n ) {\n throw new InvalidInputError(`Invalid time signature: ${text}`);\n }\n // The same signature every consumer validates, validated here too: parsing\n // a signature the rest of the library rejects only moves the failure.\n const grouping = groupTokens.length > 1 ? groupTokens : undefined;\n return assertTimeSignature({ numerator, denominator, grouping }, `time signature ${text}`);\n}\n\n/**\n * Render a time signature as `\"n/d\"`, or as `\"a+b+c/d\"` when it carries an\n * additive grouping and `grouping` is requested.\n *\n * The plain form drops the grouping, so a round trip through it reads 7/8 as\n * flat rather than as 2+2+3; ask for the additive form to keep it.\n *\n * @param ts The time signature.\n * @param opts Set `grouping: true` to render an additive grouping.\n * @returns The formatted signature.\n * @example\n * ```ts\n * import { formatTimeSignature } from '@libraz/libcantus';\n * const aksak = { numerator: 7, denominator: 8, grouping: [2, 2, 3] };\n * formatTimeSignature(aksak); // '7/8'\n * formatTimeSignature(aksak, { grouping: true }); // '2+2+3/8'\n * ```\n * @category Rhythm & Meter\n */\nexport function formatTimeSignature(ts: TimeSignature, opts: { grouping?: boolean } = {}): string {\n assertTimeSignature(ts);\n if (opts.grouping === true && ts.grouping !== undefined) {\n return `${ts.grouping.join('+')}/${ts.denominator}`;\n }\n return `${ts.numerator}/${ts.denominator}`;\n}\n\n/**\n * Whether a meter is compound: its main pulses each divide into three, as in\n * 6/8, 9/8, 12/8, or 6/4. Compound meters are those whose numerator is a\n * multiple of three greater than three, independent of the denominator. Meters\n * like 3/8 and 3/4 (simple triples, numerator 3) are not compound, and neither\n * is a signature whose {@link TimeSignature.grouping} selects the additive\n * reading (9/8 as 2+2+2+3).\n *\n * @param ts The time signature.\n * @returns True for compound meters.\n * @category Rhythm & Meter\n */\nexport function isCompound(ts: TimeSignature): boolean {\n assertTimeSignature(ts);\n return isCompoundNumerator(ts.numerator) && !isAdditiveReading(ts);\n}\n\n/**\n * Length of a bar in quarter-note beats.\n *\n * @param ts The time signature.\n * @returns The bar length in quarter notes.\n * @category Rhythm & Meter\n */\nexport function beatsPerBar(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return barBeatsOf(ts);\n}\n\n/**\n * Number of main pulses (felt beats) per bar: the numerator for simple meters,\n * a third of it for compound meters.\n *\n * For additive/irregular meters such as 7/8 or 5/8 this counts the raw pulses\n * (7 and 5 respectively), which are otherwise felt as flat, evenly divided and\n * equally accented pulses. To recover a 2+2+3 or 3+2 felt-beat grouping, set\n * {@link TimeSignature.grouping}; {@link metricWeight} then accents each\n * group's head pulse.\n *\n * @param ts The time signature.\n * @returns The pulse count per bar.\n * @category Rhythm & Meter\n */\nexport function pulsesPerBar(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return pulseCountOf(ts);\n}\n\n/** Whether `pulseIndex` is the head pulse of one of the additive groups. */\nfunction isGroupHead(grouping: number[], pulseIndex: number): boolean {\n let acc = 0;\n for (const g of grouping) {\n if (acc === pulseIndex) {\n return true;\n }\n acc += g;\n }\n return false;\n}\n\n/**\n * Convert an absolute quarter-note position to a bar index and in-bar offset.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param ts The time signature.\n * @returns The bar and in-bar quarter-note offset.\n * @example\n * ```ts\n * import { parseTimeSignature, beatToBarPosition } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * beatToBarPosition(5, ts); // { bar: 1, beat: 1 }\n * ```\n * @category Rhythm & Meter\n */\nexport function beatToBarPosition(beatInQuarters: number, ts: TimeSignature): BarPosition {\n assertFiniteNumber(beatInQuarters, 'beat');\n assertTimeSignature(ts);\n const barLen = barBeatsOf(ts);\n const bar = Math.floor(beatInQuarters / barLen);\n return { bar, beat: beatInQuarters - bar * barLen };\n}\n\n/**\n * Length of one felt beat (main pulse) in quarter-note beats.\n *\n * A quarter note in simple meters, a dotted quarter in compound ones, and one\n * denominator unit in an additive metre. Position displays, click tracks, and\n * score export are all written against this length.\n *\n * @param ts The time signature.\n * @returns The pulse length in quarter-note beats.\n * @example\n * ```ts\n * import { parseTimeSignature, pulseBeats } from '@libraz/libcantus';\n * pulseBeats(parseTimeSignature('6/8')); // 1.5\n * ```\n * @category Rhythm & Meter\n */\nexport function pulseBeats(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return pulseBeatsOf(ts);\n}\n\n/**\n * The felt-beat number of a bar position, 1-based.\n *\n * {@link BarPosition.beat} is a quarter-note offset, so in 6/8 the second felt\n * beat reads as 1.5. This is the number a musician says and a position display\n * shows.\n *\n * @param pos The bar position.\n * @param ts The time signature.\n * @returns The 1-based felt-beat number, fractional between pulses.\n * @example\n * ```ts\n * import { barPositionToPulse, parseTimeSignature } from '@libraz/libcantus';\n * barPositionToPulse({ bar: 0, beat: 1.5 }, parseTimeSignature('6/8')); // 2\n * ```\n * @category Rhythm & Meter\n */\nexport function barPositionToPulse(pos: BarPosition, ts: TimeSignature): number {\n assertFiniteNumber(pos.beat, 'position.beat');\n assertTimeSignature(ts);\n return pos.beat / pulseBeatsOf(ts) + 1;\n}\n\n/**\n * Render an absolute position as the `bar.beat` a DAW or a score shows:\n * 1-based bar, 1-based felt beat.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param ts The time signature.\n * @param decimals Digits of the fractional beat to keep.\n * @returns The formatted position, e.g. `'3.2'` for bar 3, felt beat 2.\n * @example\n * ```ts\n * import { formatBarPosition, parseTimeSignature } from '@libraz/libcantus';\n * formatBarPosition(7.5, parseTimeSignature('6/8')); // '3.2'\n * ```\n * @category Rhythm & Meter\n */\nexport function formatBarPosition(beatInQuarters: number, ts: TimeSignature, decimals = 2): string {\n assertInteger(decimals, 'decimals', 0, 100);\n const position = beatToBarPosition(beatInQuarters, ts);\n const pulse = barPositionToPulse(position, ts);\n const rounded = Number(pulse.toFixed(decimals));\n if (rounded >= pulsesPerBar(ts) + 1) {\n return `${position.bar + 2}.1`;\n }\n const wholePulse = Math.floor(rounded);\n const fraction = Number((rounded - wholePulse).toFixed(decimals));\n return fraction === 0\n ? `${position.bar + 1}.${wholePulse}`\n : `${position.bar + 1}.${wholePulse}+${fraction}`;\n}\n\n/**\n * Convert a bar index and in-bar offset back to an absolute quarter-note\n * position.\n *\n * @param pos The bar position.\n * @param ts The time signature.\n * @returns The absolute position in quarter-note beats.\n * @category Rhythm & Meter\n */\nexport function barPositionToBeat(pos: BarPosition, ts: TimeSignature): number {\n assertInteger(pos.bar, 'bar position bar');\n assertFiniteNumber(pos.beat, 'bar position beat');\n assertTimeSignature(ts);\n return pos.bar * barBeatsOf(ts) + pos.beat;\n}\n\n/**\n * Metric weight of a position within its bar, on a 0–3 scale:\n * 3 the downbeat, 2 a secondary strong pulse, 1 any other main pulse, and 0 an\n * off-pulse subdivision.\n *\n * The secondary strong pulse is the bar's midpoint in even simple/compound\n * meters. For an additive/irregular meter with {@link TimeSignature.grouping}\n * set (e.g. `[2, 2, 3]` for 7/8), each group's head pulse other than the\n * downbeat is the secondary strong pulse instead; without a grouping every\n * non-downbeat main pulse of such a meter weighs 1 (flat, evenly divided\n * pulses).\n *\n * @param beatInQuarters Absolute or in-bar quarter-note position.\n * @param ts The time signature.\n * @returns The metric weight (0–3).\n * @throws If the time signature is malformed, including a grouping that is not\n * a positive-integer list summing to the pulse count or the numerator.\n * @example\n * ```ts\n * import { parseTimeSignature, metricWeight } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * metricWeight(0, ts); // 3 (the downbeat)\n * ```\n * @category Rhythm & Meter\n */\nexport function metricWeight(beatInQuarters: number, ts: TimeSignature): number {\n assertFiniteNumber(beatInQuarters, 'beat');\n assertTimeSignature(ts);\n const barLen = barBeatsOf(ts);\n const beat = beatInQuarters - Math.floor(beatInQuarters / barLen) * barLen;\n const pulse = pulseBeatsOf(ts);\n if (!isMultiple(beat, pulse)) {\n return 0;\n }\n const pulses = pulseCountOf(ts);\n const pulseIndex = Math.round(beat / pulse) % pulses;\n if (pulseIndex === 0) {\n return 3;\n }\n const grouping = ts.grouping;\n if (grouping !== undefined) {\n return isGroupHead(grouping, pulseIndex) ? 2 : 1;\n }\n if (pulses % 2 === 0 && pulseIndex === pulses / 2) {\n return 2;\n }\n return 1;\n}\n\n/**\n * Whether a position is metrically accented (weight 2 or more — a downbeat or a\n * secondary strong pulse).\n *\n * @param beatInQuarters Absolute or in-bar quarter-note position.\n * @param ts The time signature.\n * @returns True on strong beats.\n * @category Rhythm & Meter\n */\nexport function isStrongBeat(beatInQuarters: number, ts: TimeSignature): boolean {\n return metricWeight(beatInQuarters, ts) >= 2;\n}\n\n/**\n * Subdivide a span into `count` equal tuplet durations (e.g. an eighth-note\n * triplet is `tuplet(1, 3)`).\n *\n * @param totalBeats Total span in quarter-note beats.\n * @param count Number of equal parts.\n * @returns `count` equal durations summing to `totalBeats`.\n * @throws If `count` is not a positive integer.\n * @category Rhythm & Meter\n */\nexport function tuplet(totalBeats: number, count: number): number[] {\n assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'tuplet total beats');\n assertPositiveInt(count, 'tuplet count');\n return new Array<number>(count).fill(totalBeats / count);\n}\n","import type { NoteEvent } from '../types.js';\nimport { assertNoteEvents } from '../validation/index.js';\n\n/**\n * A validated note retaining its position in the caller's original array.\n *\n * @category Core\n */\nexport type IndexedNoteEvent = {\n note: Readonly<NoteEvent>;\n originalIndex: number;\n endBeat: number;\n};\n\n/**\n * Which note wins when several sound from the same onset.\n *\n * `'highest'` and `'lowest'` name a voice, so the answer does not depend on the\n * order the caller happened to store the chord in; `'last'` takes the one\n * latest in the input array.\n *\n * @category Core\n */\nexport type OnsetTieBreak = 'highest' | 'lowest' | 'last';\n\n/**\n * Binary-searchable, stable-sorted index over note onsets and active spans.\n *\n * @category Core\n */\nexport type NoteEventIndex = {\n notes: readonly IndexedNoteEvent[];\n /**\n * Latest-onset note sounding at `beat`. Simultaneous onsets are resolved by\n * the index's {@link OnsetTieBreak}.\n */\n at: (beat: number) => IndexedNoteEvent | undefined;\n /** Whether one or more sounding notes attack at `beat`. */\n attacksAt: (beat: number) => boolean;\n /** Unique sounding-note attack beats strictly inside `(startBeat, endBeat)`. */\n onsetsBetween: (startBeat: number, endBeat: number) => number[];\n};\n\nconst EPS = 1e-9;\n\nfunction upperBound(values: readonly IndexedNoteEvent[], beat: number): number {\n let low = 0;\n let high = values.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\n/**\n * Validate and stable-sort note events once, then expose logarithmic onset and\n * active-note lookups. Non-positive-duration notes may be retained for callers\n * that intentionally filter them later, but never count as sounding.\n *\n * @category Core\n */\nexport function createNoteEventIndex(\n events: readonly NoteEvent[],\n options: {\n allowNonPositiveDuration?: boolean;\n tieBreak?: OnsetTieBreak;\n budget?: number;\n /** What the events are, for the validation error message. */\n name?: string;\n } = {},\n): NoteEventIndex {\n assertNoteEvents(events, options.name ?? 'note events', options);\n const tieBreak = options.tieBreak ?? 'highest';\n const notes = Object.freeze(\n events\n .map((note, originalIndex) => ({\n note: Object.freeze({ ...note }),\n originalIndex,\n endBeat: note.startBeat + note.durationBeat,\n }))\n .sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex),\n );\n // Segment-tree maxima find the latest active onset in logarithmic time even\n // when an early, very long note would defeat a prefix-max backwards scan.\n let treeSize = 1;\n while (treeSize < notes.length) treeSize *= 2;\n const maxEndTree = new Array<number>(treeSize * 2).fill(Number.NEGATIVE_INFINITY);\n for (let index = 0; index < notes.length; index += 1) {\n maxEndTree[treeSize + index] = notes[index]?.endBeat ?? Number.NEGATIVE_INFINITY;\n }\n for (let index = treeSize - 1; index > 0; index -= 1) {\n maxEndTree[index] = Math.max(\n maxEndTree[index * 2] ?? Number.NEGATIVE_INFINITY,\n maxEndTree[index * 2 + 1] ?? Number.NEGATIVE_INFINITY,\n );\n }\n const latestActiveIndex = (exclusive: number, beat: number): number => {\n const find = (node: number, start: number, end: number): number => {\n if (start >= exclusive || (maxEndTree[node] ?? Number.NEGATIVE_INFINITY) <= beat + EPS) {\n return -1;\n }\n if (end - start === 1) return start < notes.length ? start : -1;\n const mid = Math.floor((start + end) / 2);\n const right = find(node * 2 + 1, mid, end);\n return right >= 0 ? right : find(node * 2, start, mid);\n };\n return find(1, 0, treeSize);\n };\n\n return {\n notes,\n at(beat) {\n const index = latestActiveIndex(upperBound(notes, beat + EPS), beat);\n if (index < 0) return undefined;\n const onset = notes[index]?.note.startBeat;\n let best: IndexedNoteEvent | undefined;\n for (let candidateIndex = index; candidateIndex >= 0; candidateIndex -= 1) {\n const indexed = notes[candidateIndex];\n if (indexed !== undefined && indexed.note.startBeat !== onset) break;\n if (\n indexed === undefined ||\n indexed.note.durationBeat <= 0 ||\n indexed.note.startBeat - EPS > beat ||\n beat >= indexed.endBeat - EPS\n ) {\n continue;\n }\n if (best === undefined) {\n best = indexed;\n continue;\n }\n if (tieBreak === 'last') continue;\n const higher = indexed.note.pitch > best.note.pitch;\n if (tieBreak === 'highest' ? higher : !higher) {\n best = indexed;\n }\n }\n return best;\n },\n attacksAt(beat) {\n // Only a sounding note attacks: a zero-length artefact must not make\n // `attacksAt(b)` true on a beat where `at(b)` finds nothing.\n let index = upperBound(notes, beat + EPS) - 1;\n while (index >= 0) {\n const candidate = notes[index];\n if (candidate === undefined || Math.abs(candidate.note.startBeat - beat) >= EPS) {\n return false;\n }\n if (candidate.note.durationBeat > 0) {\n return true;\n }\n index -= 1;\n }\n return false;\n },\n onsetsBetween(startBeat, endBeat) {\n const result: number[] = [];\n let index = upperBound(notes, startBeat + EPS);\n while (index < notes.length) {\n const indexed = notes[index];\n const onset = indexed?.note.startBeat;\n if (indexed === undefined || onset === undefined || onset >= endBeat - EPS) {\n break;\n }\n index += 1;\n if (indexed.note.durationBeat <= 0) {\n continue;\n }\n const previous = result[result.length - 1];\n if (previous === undefined || Math.abs(previous - onset) >= EPS) {\n result.push(onset);\n }\n }\n return result;\n },\n };\n}\n"]}
@@ -11,7 +11,7 @@ import {
11
11
  isCompoundNumerator,
12
12
  pulseBeatsOf,
13
13
  pulseCountOf
14
- } from "./chunk-XEBG3G53.js";
14
+ } from "./chunk-R2VVFAIN.js";
15
15
 
16
16
  // src/core/meter/index.ts
17
17
  var EPS = 1e-9;
@@ -23,16 +23,18 @@ function isMultiple(value, unit) {
23
23
  return Math.abs(ratio - Math.round(ratio)) < EPS;
24
24
  }
25
25
  function parseTimeSignature(text) {
26
- const match = /^\s*(\d+)\s*\/\s*(\d+)\s*$/.exec(text);
26
+ const match = /^\s*((?:\d+\s*\+\s*)*\d+)\s*\/\s*(\d+)\s*$/.exec(text);
27
27
  if (!match) {
28
28
  throw new InvalidInputError(`Invalid time signature: ${text}`);
29
29
  }
30
- const numerator = Number.parseInt(match[1] ?? "", 10);
30
+ const groupTokens = (match[1] ?? "").split("+").map((token) => Number.parseInt(token.trim(), 10));
31
+ const numerator = groupTokens.reduce((sum, value) => sum + value, 0);
31
32
  const denominator = Number.parseInt(match[2] ?? "", 10);
32
33
  if (!Number.isSafeInteger(numerator) || !Number.isSafeInteger(denominator) || numerator <= 0 || denominator <= 0) {
33
34
  throw new InvalidInputError(`Invalid time signature: ${text}`);
34
35
  }
35
- return assertTimeSignature({ numerator, denominator }, `time signature ${text}`);
36
+ const grouping = groupTokens.length > 1 ? groupTokens : void 0;
37
+ return assertTimeSignature({ numerator, denominator, grouping }, `time signature ${text}`);
36
38
  }
37
39
  function formatTimeSignature(ts, opts = {}) {
38
40
  assertTimeSignature(ts);
@@ -80,10 +82,16 @@ function barPositionToPulse(pos, ts) {
80
82
  return pos.beat / pulseBeatsOf(ts) + 1;
81
83
  }
82
84
  function formatBarPosition(beatInQuarters, ts, decimals = 2) {
85
+ assertInteger(decimals, "decimals", 0, 100);
83
86
  const position = beatToBarPosition(beatInQuarters, ts);
84
87
  const pulse = barPositionToPulse(position, ts);
85
88
  const rounded = Number(pulse.toFixed(decimals));
86
- return `${position.bar + 1}.${rounded}`;
89
+ if (rounded >= pulsesPerBar(ts) + 1) {
90
+ return `${position.bar + 2}.1`;
91
+ }
92
+ const wholePulse = Math.floor(rounded);
93
+ const fraction = Number((rounded - wholePulse).toFixed(decimals));
94
+ return fraction === 0 ? `${position.bar + 1}.${wholePulse}` : `${position.bar + 1}.${wholePulse}+${fraction}`;
87
95
  }
88
96
  function barPositionToBeat(pos, ts) {
89
97
  assertInteger(pos.bar, "bar position bar");
@@ -141,48 +149,55 @@ function upperBound(values, beat) {
141
149
  function createNoteEventIndex(events, options = {}) {
142
150
  assertNoteEvents(events, options.name ?? "note events", options);
143
151
  const tieBreak = options.tieBreak ?? "highest";
144
- const notes = events.map((note, originalIndex) => ({
145
- note,
146
- originalIndex,
147
- endBeat: note.startBeat + note.durationBeat
148
- })).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex);
149
- let prefixMaxEnd;
150
- const maxEndUpTo = (index) => {
151
- if (prefixMaxEnd === void 0) {
152
- const prefix = [];
153
- let maxEnd = Number.NEGATIVE_INFINITY;
154
- for (const indexed of notes) {
155
- maxEnd = Math.max(maxEnd, indexed.endBeat);
156
- prefix.push(maxEnd);
152
+ const notes = Object.freeze(
153
+ events.map((note, originalIndex) => ({
154
+ note: Object.freeze({ ...note }),
155
+ originalIndex,
156
+ endBeat: note.startBeat + note.durationBeat
157
+ })).sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex)
158
+ );
159
+ let treeSize = 1;
160
+ while (treeSize < notes.length) treeSize *= 2;
161
+ const maxEndTree = new Array(treeSize * 2).fill(Number.NEGATIVE_INFINITY);
162
+ for (let index = 0; index < notes.length; index += 1) {
163
+ maxEndTree[treeSize + index] = notes[index]?.endBeat ?? Number.NEGATIVE_INFINITY;
164
+ }
165
+ for (let index = treeSize - 1; index > 0; index -= 1) {
166
+ maxEndTree[index] = Math.max(
167
+ maxEndTree[index * 2] ?? Number.NEGATIVE_INFINITY,
168
+ maxEndTree[index * 2 + 1] ?? Number.NEGATIVE_INFINITY
169
+ );
170
+ }
171
+ const latestActiveIndex = (exclusive, beat) => {
172
+ const find = (node, start, end) => {
173
+ if (start >= exclusive || (maxEndTree[node] ?? Number.NEGATIVE_INFINITY) <= beat + EPS2) {
174
+ return -1;
157
175
  }
158
- prefixMaxEnd = prefix;
159
- }
160
- return prefixMaxEnd[index] ?? Number.NEGATIVE_INFINITY;
176
+ if (end - start === 1) return start < notes.length ? start : -1;
177
+ const mid = Math.floor((start + end) / 2);
178
+ const right = find(node * 2 + 1, mid, end);
179
+ return right >= 0 ? right : find(node * 2, start, mid);
180
+ };
181
+ return find(1, 0, treeSize);
161
182
  };
162
183
  return {
163
184
  notes,
164
185
  at(beat) {
165
- let index = upperBound(notes, beat + EPS2) - 1;
186
+ const index = latestActiveIndex(upperBound(notes, beat + EPS2), beat);
187
+ if (index < 0) return void 0;
188
+ const onset = notes[index]?.note.startBeat;
166
189
  let best;
167
- while (index >= 0) {
168
- if (maxEndUpTo(index) <= beat + EPS2) {
169
- break;
170
- }
171
- const indexed = notes[index];
172
- index -= 1;
190
+ for (let candidateIndex = index; candidateIndex >= 0; candidateIndex -= 1) {
191
+ const indexed = notes[candidateIndex];
192
+ if (indexed !== void 0 && indexed.note.startBeat !== onset) break;
173
193
  if (indexed === void 0 || indexed.note.durationBeat <= 0 || indexed.note.startBeat - EPS2 > beat || beat >= indexed.endBeat - EPS2) {
174
194
  continue;
175
195
  }
176
196
  if (best === void 0) {
177
197
  best = indexed;
178
- if (tieBreak === "last") {
179
- break;
180
- }
181
198
  continue;
182
199
  }
183
- if (Math.abs(indexed.note.startBeat - best.note.startBeat) >= EPS2) {
184
- break;
185
- }
200
+ if (tieBreak === "last") continue;
186
201
  const higher = indexed.note.pitch > best.note.pitch;
187
202
  if (tieBreak === "highest" ? higher : !higher) {
188
203
  best = indexed;
@@ -243,4 +258,4 @@ export {
243
258
  tuplet,
244
259
  createNoteEventIndex
245
260
  };
246
- //# sourceMappingURL=chunk-YLEMGEFH.js.map
261
+ //# sourceMappingURL=chunk-2PTFGVD7.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/meter/index.ts","../src/core/event-index/index.ts"],"sourcesContent":["/**\n * Meter and time signatures: bar/beat arithmetic, simple vs. compound meters,\n * metric-accent hierarchy, and tuplet subdivision.\n *\n * Positions and durations are measured in quarter-note beats, matching the rest\n * of the library's beat convention (four quarter-note beats per bar in 4/4).\n */\n\nimport { InvalidInputError } from '../errors/index.js';\nimport {\n assertFiniteNumber,\n assertInteger,\n assertPositiveInt,\n assertRange,\n assertTimeSignature,\n} from '../validation/index.js';\nimport {\n barBeatsOf,\n isAdditiveReading,\n isCompoundNumerator,\n pulseBeatsOf,\n pulseCountOf,\n} from './internal.js';\n\n/**\n * A time signature as a numerator over a note-value denominator.\n *\n * @category Rhythm & Meter\n */\nexport type TimeSignature = {\n numerator: number;\n denominator: number;\n /**\n * Optional additive grouping of the bar's main pulses, as the felt-beat\n * lengths in pulses — e.g. `[2, 2, 3]` for a 2+2+3 reading of 7/8, or\n * `[3, 2]` for 5/8. The entries must be positive integers summing to the\n * numerator.\n *\n * A compound numerator accepts both readings. 9/8 groups its three dotted\n * pulses as `[1, 1, 1]`, or reads additively as `[2, 2, 2, 3]` quavers; a\n * grouping that sums to the numerator selects the additive reading, so\n * aksak and other additive metres are expressible. {@link pulsesPerBar}\n * and {@link isCompound} follow whichever reading the grouping selects.\n *\n * When present, the head pulse of each group (other than the downbeat) is\n * treated as a secondary strong pulse by\n * {@link metricWeight}/{@link isStrongBeat}; when absent, all main pulses\n * are weighted equally as flat, evenly divided pulses.\n */\n grouping?: number[];\n};\n\n/**\n * A position expressed as a bar index and a quarter-note offset within the bar.\n *\n * @category Rhythm & Meter\n */\nexport type BarPosition = {\n /** 0-based bar index. {@link formatBarPosition} renders it 1-based. */\n bar: number;\n /**\n * Quarter-note offset from the start of the bar — not the felt beat. In 6/8\n * the second felt beat is at `beat: 1.5`; {@link barPositionToPulse} converts\n * to felt-beat numbering.\n */\n beat: number;\n};\n\nconst EPS = 1e-9;\n\n/** Whether `value` is an integer multiple of `unit` (within a float tolerance). */\nfunction isMultiple(value: number, unit: number): boolean {\n if (unit === 0) {\n return false;\n }\n const ratio = value / unit;\n return Math.abs(ratio - Math.round(ratio)) < EPS;\n}\n\n/**\n * Parse a time signature such as `\"4/4\"` or `\"6/8\"`.\n *\n * @param text The signature text.\n * @returns The parsed time signature.\n * @throws If the text is not `n/d` with positive integers.\n * @example\n * ```ts\n * import { parseTimeSignature } from '@libraz/libcantus';\n * parseTimeSignature('6/8'); // { numerator: 6, denominator: 8 }\n * ```\n * @category Rhythm & Meter\n */\nexport function parseTimeSignature(text: string): TimeSignature {\n const match = /^\\s*((?:\\d+\\s*\\+\\s*)*\\d+)\\s*\\/\\s*(\\d+)\\s*$/.exec(text);\n if (!match) {\n throw new InvalidInputError(`Invalid time signature: ${text}`);\n }\n const groupTokens = (match[1] ?? '').split('+').map((token) => Number.parseInt(token.trim(), 10));\n const numerator = groupTokens.reduce((sum, value) => sum + value, 0);\n const denominator = Number.parseInt(match[2] ?? '', 10);\n if (\n !Number.isSafeInteger(numerator) ||\n !Number.isSafeInteger(denominator) ||\n numerator <= 0 ||\n denominator <= 0\n ) {\n throw new InvalidInputError(`Invalid time signature: ${text}`);\n }\n // The same signature every consumer validates, validated here too: parsing\n // a signature the rest of the library rejects only moves the failure.\n const grouping = groupTokens.length > 1 ? groupTokens : undefined;\n return assertTimeSignature({ numerator, denominator, grouping }, `time signature ${text}`);\n}\n\n/**\n * Render a time signature as `\"n/d\"`, or as `\"a+b+c/d\"` when it carries an\n * additive grouping and `grouping` is requested.\n *\n * The plain form drops the grouping, so a round trip through it reads 7/8 as\n * flat rather than as 2+2+3; ask for the additive form to keep it.\n *\n * @param ts The time signature.\n * @param opts Set `grouping: true` to render an additive grouping.\n * @returns The formatted signature.\n * @example\n * ```ts\n * import { formatTimeSignature } from '@libraz/libcantus';\n * const aksak = { numerator: 7, denominator: 8, grouping: [2, 2, 3] };\n * formatTimeSignature(aksak); // '7/8'\n * formatTimeSignature(aksak, { grouping: true }); // '2+2+3/8'\n * ```\n * @category Rhythm & Meter\n */\nexport function formatTimeSignature(ts: TimeSignature, opts: { grouping?: boolean } = {}): string {\n assertTimeSignature(ts);\n if (opts.grouping === true && ts.grouping !== undefined) {\n return `${ts.grouping.join('+')}/${ts.denominator}`;\n }\n return `${ts.numerator}/${ts.denominator}`;\n}\n\n/**\n * Whether a meter is compound: its main pulses each divide into three, as in\n * 6/8, 9/8, 12/8, or 6/4. Compound meters are those whose numerator is a\n * multiple of three greater than three, independent of the denominator. Meters\n * like 3/8 and 3/4 (simple triples, numerator 3) are not compound, and neither\n * is a signature whose {@link TimeSignature.grouping} selects the additive\n * reading (9/8 as 2+2+2+3).\n *\n * @param ts The time signature.\n * @returns True for compound meters.\n * @category Rhythm & Meter\n */\nexport function isCompound(ts: TimeSignature): boolean {\n assertTimeSignature(ts);\n return isCompoundNumerator(ts.numerator) && !isAdditiveReading(ts);\n}\n\n/**\n * Length of a bar in quarter-note beats.\n *\n * @param ts The time signature.\n * @returns The bar length in quarter notes.\n * @category Rhythm & Meter\n */\nexport function beatsPerBar(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return barBeatsOf(ts);\n}\n\n/**\n * Number of main pulses (felt beats) per bar: the numerator for simple meters,\n * a third of it for compound meters.\n *\n * For additive/irregular meters such as 7/8 or 5/8 this counts the raw pulses\n * (7 and 5 respectively), which are otherwise felt as flat, evenly divided and\n * equally accented pulses. To recover a 2+2+3 or 3+2 felt-beat grouping, set\n * {@link TimeSignature.grouping}; {@link metricWeight} then accents each\n * group's head pulse.\n *\n * @param ts The time signature.\n * @returns The pulse count per bar.\n * @category Rhythm & Meter\n */\nexport function pulsesPerBar(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return pulseCountOf(ts);\n}\n\n/** Whether `pulseIndex` is the head pulse of one of the additive groups. */\nfunction isGroupHead(grouping: number[], pulseIndex: number): boolean {\n let acc = 0;\n for (const g of grouping) {\n if (acc === pulseIndex) {\n return true;\n }\n acc += g;\n }\n return false;\n}\n\n/**\n * Convert an absolute quarter-note position to a bar index and in-bar offset.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param ts The time signature.\n * @returns The bar and in-bar quarter-note offset.\n * @example\n * ```ts\n * import { parseTimeSignature, beatToBarPosition } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * beatToBarPosition(5, ts); // { bar: 1, beat: 1 }\n * ```\n * @category Rhythm & Meter\n */\nexport function beatToBarPosition(beatInQuarters: number, ts: TimeSignature): BarPosition {\n assertFiniteNumber(beatInQuarters, 'beat');\n assertTimeSignature(ts);\n const barLen = barBeatsOf(ts);\n const bar = Math.floor(beatInQuarters / barLen);\n return { bar, beat: beatInQuarters - bar * barLen };\n}\n\n/**\n * Length of one felt beat (main pulse) in quarter-note beats.\n *\n * A quarter note in simple meters, a dotted quarter in compound ones, and one\n * denominator unit in an additive metre. Position displays, click tracks, and\n * score export are all written against this length.\n *\n * @param ts The time signature.\n * @returns The pulse length in quarter-note beats.\n * @example\n * ```ts\n * import { parseTimeSignature, pulseBeats } from '@libraz/libcantus';\n * pulseBeats(parseTimeSignature('6/8')); // 1.5\n * ```\n * @category Rhythm & Meter\n */\nexport function pulseBeats(ts: TimeSignature): number {\n assertTimeSignature(ts);\n return pulseBeatsOf(ts);\n}\n\n/**\n * The felt-beat number of a bar position, 1-based.\n *\n * {@link BarPosition.beat} is a quarter-note offset, so in 6/8 the second felt\n * beat reads as 1.5. This is the number a musician says and a position display\n * shows.\n *\n * @param pos The bar position.\n * @param ts The time signature.\n * @returns The 1-based felt-beat number, fractional between pulses.\n * @example\n * ```ts\n * import { barPositionToPulse, parseTimeSignature } from '@libraz/libcantus';\n * barPositionToPulse({ bar: 0, beat: 1.5 }, parseTimeSignature('6/8')); // 2\n * ```\n * @category Rhythm & Meter\n */\nexport function barPositionToPulse(pos: BarPosition, ts: TimeSignature): number {\n assertFiniteNumber(pos.beat, 'position.beat');\n assertTimeSignature(ts);\n return pos.beat / pulseBeatsOf(ts) + 1;\n}\n\n/**\n * Render an absolute position as the `bar.beat` a DAW or a score shows:\n * 1-based bar, 1-based felt beat.\n *\n * @param beatInQuarters Absolute position in quarter-note beats.\n * @param ts The time signature.\n * @param decimals Digits of the fractional beat to keep.\n * @returns The formatted position, e.g. `'3.2'` for bar 3, felt beat 2.\n * @example\n * ```ts\n * import { formatBarPosition, parseTimeSignature } from '@libraz/libcantus';\n * formatBarPosition(7.5, parseTimeSignature('6/8')); // '3.2'\n * ```\n * @category Rhythm & Meter\n */\nexport function formatBarPosition(beatInQuarters: number, ts: TimeSignature, decimals = 2): string {\n assertInteger(decimals, 'decimals', 0, 100);\n const position = beatToBarPosition(beatInQuarters, ts);\n const pulse = barPositionToPulse(position, ts);\n const rounded = Number(pulse.toFixed(decimals));\n if (rounded >= pulsesPerBar(ts) + 1) {\n return `${position.bar + 2}.1`;\n }\n const wholePulse = Math.floor(rounded);\n const fraction = Number((rounded - wholePulse).toFixed(decimals));\n return fraction === 0\n ? `${position.bar + 1}.${wholePulse}`\n : `${position.bar + 1}.${wholePulse}+${fraction}`;\n}\n\n/**\n * Convert a bar index and in-bar offset back to an absolute quarter-note\n * position.\n *\n * @param pos The bar position.\n * @param ts The time signature.\n * @returns The absolute position in quarter-note beats.\n * @category Rhythm & Meter\n */\nexport function barPositionToBeat(pos: BarPosition, ts: TimeSignature): number {\n assertInteger(pos.bar, 'bar position bar');\n assertFiniteNumber(pos.beat, 'bar position beat');\n assertTimeSignature(ts);\n return pos.bar * barBeatsOf(ts) + pos.beat;\n}\n\n/**\n * Metric weight of a position within its bar, on a 0–3 scale:\n * 3 the downbeat, 2 a secondary strong pulse, 1 any other main pulse, and 0 an\n * off-pulse subdivision.\n *\n * The secondary strong pulse is the bar's midpoint in even simple/compound\n * meters. For an additive/irregular meter with {@link TimeSignature.grouping}\n * set (e.g. `[2, 2, 3]` for 7/8), each group's head pulse other than the\n * downbeat is the secondary strong pulse instead; without a grouping every\n * non-downbeat main pulse of such a meter weighs 1 (flat, evenly divided\n * pulses).\n *\n * @param beatInQuarters Absolute or in-bar quarter-note position.\n * @param ts The time signature.\n * @returns The metric weight (0–3).\n * @throws If the time signature is malformed, including a grouping that is not\n * a positive-integer list summing to the pulse count or the numerator.\n * @example\n * ```ts\n * import { parseTimeSignature, metricWeight } from '@libraz/libcantus';\n * const ts = parseTimeSignature('4/4');\n * metricWeight(0, ts); // 3 (the downbeat)\n * ```\n * @category Rhythm & Meter\n */\nexport function metricWeight(beatInQuarters: number, ts: TimeSignature): number {\n assertFiniteNumber(beatInQuarters, 'beat');\n assertTimeSignature(ts);\n const barLen = barBeatsOf(ts);\n const beat = beatInQuarters - Math.floor(beatInQuarters / barLen) * barLen;\n const pulse = pulseBeatsOf(ts);\n if (!isMultiple(beat, pulse)) {\n return 0;\n }\n const pulses = pulseCountOf(ts);\n const pulseIndex = Math.round(beat / pulse) % pulses;\n if (pulseIndex === 0) {\n return 3;\n }\n const grouping = ts.grouping;\n if (grouping !== undefined) {\n return isGroupHead(grouping, pulseIndex) ? 2 : 1;\n }\n if (pulses % 2 === 0 && pulseIndex === pulses / 2) {\n return 2;\n }\n return 1;\n}\n\n/**\n * Whether a position is metrically accented (weight 2 or more — a downbeat or a\n * secondary strong pulse).\n *\n * @param beatInQuarters Absolute or in-bar quarter-note position.\n * @param ts The time signature.\n * @returns True on strong beats.\n * @category Rhythm & Meter\n */\nexport function isStrongBeat(beatInQuarters: number, ts: TimeSignature): boolean {\n return metricWeight(beatInQuarters, ts) >= 2;\n}\n\n/**\n * Subdivide a span into `count` equal tuplet durations (e.g. an eighth-note\n * triplet is `tuplet(1, 3)`).\n *\n * @param totalBeats Total span in quarter-note beats.\n * @param count Number of equal parts.\n * @returns `count` equal durations summing to `totalBeats`.\n * @throws If `count` is not a positive integer.\n * @category Rhythm & Meter\n */\nexport function tuplet(totalBeats: number, count: number): number[] {\n assertRange(totalBeats, 0, Number.MAX_SAFE_INTEGER, 'tuplet total beats');\n assertPositiveInt(count, 'tuplet count');\n return new Array<number>(count).fill(totalBeats / count);\n}\n","import type { NoteEvent } from '../types.js';\nimport { assertNoteEvents } from '../validation/index.js';\n\n/**\n * A validated note retaining its position in the caller's original array.\n *\n * @category Core\n */\nexport type IndexedNoteEvent = {\n note: Readonly<NoteEvent>;\n originalIndex: number;\n endBeat: number;\n};\n\n/**\n * Which note wins when several sound from the same onset.\n *\n * `'highest'` and `'lowest'` name a voice, so the answer does not depend on the\n * order the caller happened to store the chord in; `'last'` takes the one\n * latest in the input array.\n *\n * @category Core\n */\nexport type OnsetTieBreak = 'highest' | 'lowest' | 'last';\n\n/**\n * Binary-searchable, stable-sorted index over note onsets and active spans.\n *\n * @category Core\n */\nexport type NoteEventIndex = {\n notes: readonly IndexedNoteEvent[];\n /**\n * Latest-onset note sounding at `beat`. Simultaneous onsets are resolved by\n * the index's {@link OnsetTieBreak}.\n */\n at: (beat: number) => IndexedNoteEvent | undefined;\n /** Whether one or more sounding notes attack at `beat`. */\n attacksAt: (beat: number) => boolean;\n /** Unique sounding-note attack beats strictly inside `(startBeat, endBeat)`. */\n onsetsBetween: (startBeat: number, endBeat: number) => number[];\n};\n\nconst EPS = 1e-9;\n\nfunction upperBound(values: readonly IndexedNoteEvent[], beat: number): number {\n let low = 0;\n let high = values.length;\n while (low < high) {\n const middle = Math.floor((low + high) / 2);\n if ((values[middle]?.note.startBeat ?? Number.POSITIVE_INFINITY) <= beat) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return low;\n}\n\n/**\n * Validate and stable-sort note events once, then expose logarithmic onset and\n * active-note lookups. Non-positive-duration notes may be retained for callers\n * that intentionally filter them later, but never count as sounding.\n *\n * @category Core\n */\nexport function createNoteEventIndex(\n events: readonly NoteEvent[],\n options: {\n allowNonPositiveDuration?: boolean;\n tieBreak?: OnsetTieBreak;\n budget?: number;\n /** What the events are, for the validation error message. */\n name?: string;\n } = {},\n): NoteEventIndex {\n assertNoteEvents(events, options.name ?? 'note events', options);\n const tieBreak = options.tieBreak ?? 'highest';\n const notes = Object.freeze(\n events\n .map((note, originalIndex) => ({\n note: Object.freeze({ ...note }),\n originalIndex,\n endBeat: note.startBeat + note.durationBeat,\n }))\n .sort((a, b) => a.note.startBeat - b.note.startBeat || a.originalIndex - b.originalIndex),\n );\n // Segment-tree maxima find the latest active onset in logarithmic time even\n // when an early, very long note would defeat a prefix-max backwards scan.\n let treeSize = 1;\n while (treeSize < notes.length) treeSize *= 2;\n const maxEndTree = new Array<number>(treeSize * 2).fill(Number.NEGATIVE_INFINITY);\n for (let index = 0; index < notes.length; index += 1) {\n maxEndTree[treeSize + index] = notes[index]?.endBeat ?? Number.NEGATIVE_INFINITY;\n }\n for (let index = treeSize - 1; index > 0; index -= 1) {\n maxEndTree[index] = Math.max(\n maxEndTree[index * 2] ?? Number.NEGATIVE_INFINITY,\n maxEndTree[index * 2 + 1] ?? Number.NEGATIVE_INFINITY,\n );\n }\n const latestActiveIndex = (exclusive: number, beat: number): number => {\n const find = (node: number, start: number, end: number): number => {\n if (start >= exclusive || (maxEndTree[node] ?? Number.NEGATIVE_INFINITY) <= beat + EPS) {\n return -1;\n }\n if (end - start === 1) return start < notes.length ? start : -1;\n const mid = Math.floor((start + end) / 2);\n const right = find(node * 2 + 1, mid, end);\n return right >= 0 ? right : find(node * 2, start, mid);\n };\n return find(1, 0, treeSize);\n };\n\n return {\n notes,\n at(beat) {\n const index = latestActiveIndex(upperBound(notes, beat + EPS), beat);\n if (index < 0) return undefined;\n const onset = notes[index]?.note.startBeat;\n let best: IndexedNoteEvent | undefined;\n for (let candidateIndex = index; candidateIndex >= 0; candidateIndex -= 1) {\n const indexed = notes[candidateIndex];\n if (indexed !== undefined && indexed.note.startBeat !== onset) break;\n if (\n indexed === undefined ||\n indexed.note.durationBeat <= 0 ||\n indexed.note.startBeat - EPS > beat ||\n beat >= indexed.endBeat - EPS\n ) {\n continue;\n }\n if (best === undefined) {\n best = indexed;\n continue;\n }\n if (tieBreak === 'last') continue;\n const higher = indexed.note.pitch > best.note.pitch;\n if (tieBreak === 'highest' ? higher : !higher) {\n best = indexed;\n }\n }\n return best;\n },\n attacksAt(beat) {\n // Only a sounding note attacks: a zero-length artefact must not make\n // `attacksAt(b)` true on a beat where `at(b)` finds nothing.\n let index = upperBound(notes, beat + EPS) - 1;\n while (index >= 0) {\n const candidate = notes[index];\n if (candidate === undefined || Math.abs(candidate.note.startBeat - beat) >= EPS) {\n return false;\n }\n if (candidate.note.durationBeat > 0) {\n return true;\n }\n index -= 1;\n }\n return false;\n },\n onsetsBetween(startBeat, endBeat) {\n const result: number[] = [];\n let index = upperBound(notes, startBeat + EPS);\n while (index < notes.length) {\n const indexed = notes[index];\n const onset = indexed?.note.startBeat;\n if (indexed === undefined || onset === undefined || onset >= endBeat - EPS) {\n break;\n }\n index += 1;\n if (indexed.note.durationBeat <= 0) {\n continue;\n }\n const previous = result[result.length - 1];\n if (previous === undefined || Math.abs(previous - onset) >= EPS) {\n result.push(onset);\n }\n }\n return result;\n },\n };\n}\n"],"mappings":"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","names":["EPS"]}