linked-rolls 0.57.0 → 0.59.0

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 (130) hide show
  1. package/README.md +54 -367
  2. package/lib/{constraints.d.ts → analysis/constraints.d.ts} +1 -1
  3. package/lib/{constraints.js → analysis/constraints.js} +8 -7
  4. package/lib/analysis/ownPaper.d.ts +26 -0
  5. package/lib/analysis/ownPaper.js +41 -0
  6. package/lib/{reservations.d.ts → analysis/reservations.d.ts} +3 -3
  7. package/lib/{reservations.js → analysis/reservations.js} +3 -3
  8. package/lib/{sigla.d.ts → analysis/sigla.d.ts} +2 -2
  9. package/lib/{sigla.js → analysis/sigla.js} +4 -4
  10. package/lib/{witnesses.d.ts → analysis/witnesses.d.ts} +2 -2
  11. package/lib/{witnesses.js → analysis/witnesses.js} +2 -2
  12. package/lib/{Collation.d.ts → collation/Collation.d.ts} +4 -4
  13. package/lib/{Collation.js → collation/Collation.js} +4 -4
  14. package/lib/{alignment.d.ts → collation/alignment.d.ts} +4 -4
  15. package/lib/{alignment.js → collation/alignment.js} +10 -16
  16. package/lib/{scatter.d.ts → collation/scatter.d.ts} +4 -4
  17. package/lib/{scatter.js → collation/scatter.js} +3 -3
  18. package/lib/{statistics.d.ts → collation/statistics.d.ts} +1 -1
  19. package/lib/{statistics.js → collation/statistics.js} +2 -2
  20. package/lib/{substitution.d.ts → collation/substitution.d.ts} +9 -24
  21. package/lib/{substitution.js → collation/substitution.js} +13 -37
  22. package/lib/emulation/Emulation.d.ts +64 -0
  23. package/lib/emulation/Emulation.js +282 -0
  24. package/lib/emulation/negotiation.d.ts +14 -0
  25. package/lib/emulation/negotiation.js +20 -0
  26. package/lib/index.d.ts +43 -35
  27. package/lib/index.js +36 -35
  28. package/lib/{asJsonLd.d.ts → io/asJsonLd.d.ts} +1 -1
  29. package/lib/{asJsonLd.js → io/asJsonLd.js} +3 -3
  30. package/lib/{context.d.ts → io/context.d.ts} +4 -4
  31. package/lib/{context.js → io/context.js} +4 -4
  32. package/lib/{importJsonLd.d.ts → io/importJsonLd.d.ts} +1 -1
  33. package/lib/io/importJsonLd.js +128 -0
  34. package/lib/{migrate.js → io/migrate.js} +59 -55
  35. package/lib/{Agent.d.ts → model/Agent.d.ts} +1 -1
  36. package/lib/{Assumption.d.ts → model/Assumption.d.ts} +1 -1
  37. package/lib/{Edit.d.ts → model/Edit.d.ts} +1 -1
  38. package/lib/{Edition.d.ts → model/Edition.d.ts} +2 -2
  39. package/lib/{Feature.d.ts → model/Feature.d.ts} +1 -1
  40. package/lib/{FeatureSource.d.ts → model/FeatureSource.d.ts} +1 -1
  41. package/lib/{Perforator.d.ts → model/Perforator.d.ts} +1 -1
  42. package/lib/{RollCopy.d.ts → model/RollCopy.d.ts} +3 -10
  43. package/lib/{RollCopy.js → model/RollCopy.js} +3 -5
  44. package/lib/{Symbol.d.ts → model/Symbol.d.ts} +1 -1
  45. package/lib/{Version.d.ts → model/Version.d.ts} +10 -2
  46. package/lib/{Version.js → model/Version.js} +24 -1
  47. package/lib/{vocabulary.js → model/vocabulary.js} +2 -2
  48. package/lib/ops/beliefs.d.ts +9 -0
  49. package/lib/ops/beliefs.js +31 -0
  50. package/lib/ops/collation.d.ts +62 -0
  51. package/lib/ops/collation.js +228 -0
  52. package/lib/ops/commands.d.ts +9 -0
  53. package/lib/ops/commands.js +29 -0
  54. package/lib/ops/copies.d.ts +74 -0
  55. package/lib/ops/copies.js +135 -0
  56. package/lib/ops/draft.d.ts +59 -0
  57. package/lib/ops/draft.js +81 -0
  58. package/lib/ops/edits.d.ts +10 -0
  59. package/lib/ops/edits.js +82 -0
  60. package/lib/ops/features.d.ts +84 -0
  61. package/lib/ops/features.js +296 -0
  62. package/lib/ops/forget.d.ts +31 -0
  63. package/lib/ops/forget.js +110 -0
  64. package/lib/ops/immutable.d.ts +16 -0
  65. package/lib/ops/immutable.js +46 -0
  66. package/lib/ops/index.d.ts +13 -0
  67. package/lib/ops/index.js +7 -0
  68. package/lib/ops/versions.d.ts +33 -0
  69. package/lib/ops/versions.js +95 -0
  70. package/lib/readers/AtonParser.js +5 -7
  71. package/lib/readers/phillipsEroll.d.ts +3 -3
  72. package/lib/readers/phillipsEroll.js +4 -5
  73. package/lib/readers/spencerBar.d.ts +2 -2
  74. package/lib/readers/spencerBar.js +2 -3
  75. package/lib/readers/stanfordAton.d.ts +3 -3
  76. package/lib/readers/stanfordAton.js +2 -3
  77. package/lib/schema.json +0 -13
  78. package/lib/{ReproducingSystem.d.ts → systems/ReproducingSystem.d.ts} +3 -3
  79. package/lib/{TrackerBar.d.ts → systems/TrackerBar.d.ts} +48 -8
  80. package/lib/{TrackerBar.js → systems/TrackerBar.js} +11 -2
  81. package/lib/systems/index.d.ts +8 -2
  82. package/lib/systems/index.js +7 -1
  83. package/lib/systems/welte.d.ts +100 -0
  84. package/lib/systems/welte.js +119 -0
  85. package/lib/systems/welteLicensee/bar.d.ts +1 -1
  86. package/lib/systems/welteLicensee/bar.js +5 -2
  87. package/lib/systems/welteLicensee/system.d.ts +1 -1
  88. package/lib/systems/welteLicensee/system.js +1 -1
  89. package/lib/systems/welteT100/bar.d.ts +27 -1
  90. package/lib/systems/welteT100/bar.js +44 -2
  91. package/lib/systems/welteT100/system.d.ts +6 -39
  92. package/lib/systems/welteT100/system.js +19 -128
  93. package/lib/systems/welteT98/bar.d.ts +1 -1
  94. package/lib/systems/welteT98/bar.js +18 -2
  95. package/lib/systems/welteT98/system.d.ts +20 -38
  96. package/lib/systems/welteT98/system.js +20 -121
  97. package/lib/validate.d.ts +1 -1
  98. package/lib/{EditionView.d.ts → view/EditionView.d.ts} +17 -41
  99. package/lib/{EditionView.js → view/EditionView.js} +26 -75
  100. package/package.json +7 -2
  101. package/lib/Emulation.d.ts +0 -36
  102. package/lib/Emulation.js +0 -250
  103. package/lib/editionOps.d.ts +0 -260
  104. package/lib/editionOps.js +0 -1079
  105. package/lib/importJsonLd.js +0 -57
  106. /package/lib/{migrate.d.ts → io/migrate.d.ts} +0 -0
  107. /package/lib/{Agent.js → model/Agent.js} +0 -0
  108. /package/lib/{Assumption.js → model/Assumption.js} +0 -0
  109. /package/lib/{ConditionState.d.ts → model/ConditionState.d.ts} +0 -0
  110. /package/lib/{ConditionState.js → model/ConditionState.js} +0 -0
  111. /package/lib/{Edit.js → model/Edit.js} +0 -0
  112. /package/lib/{Edition.js → model/Edition.js} +0 -0
  113. /package/lib/{Feature.js → model/Feature.js} +0 -0
  114. /package/lib/{FeatureSource.js → model/FeatureSource.js} +0 -0
  115. /package/lib/{Perforator.js → model/Perforator.js} +0 -0
  116. /package/lib/{Quantity.d.ts → model/Quantity.d.ts} +0 -0
  117. /package/lib/{Quantity.js → model/Quantity.js} +0 -0
  118. /package/lib/{Symbol.js → model/Symbol.js} +0 -0
  119. /package/lib/{TrackCalibration.d.ts → model/TrackCalibration.d.ts} +0 -0
  120. /package/lib/{TrackCalibration.js → model/TrackCalibration.js} +0 -0
  121. /package/lib/{notes.d.ts → model/notes.d.ts} +0 -0
  122. /package/lib/{notes.js → model/notes.js} +0 -0
  123. /package/lib/{procedures.d.ts → model/procedures.d.ts} +0 -0
  124. /package/lib/{procedures.js → model/procedures.js} +0 -0
  125. /package/lib/{vocabulary.d.ts → model/vocabulary.d.ts} +0 -0
  126. /package/lib/{sorted.d.ts → shared/sorted.d.ts} +0 -0
  127. /package/lib/{sorted.js → shared/sorted.js} +0 -0
  128. /package/lib/{utils.d.ts → shared/utils.d.ts} +0 -0
  129. /package/lib/{utils.js → shared/utils.js} +0 -0
  130. /package/lib/{ReproducingSystem.js → systems/ReproducingSystem.js} +0 -0
@@ -1,7 +1,7 @@
1
- import { keyOf } from "./TrackerBar.js";
2
- import { distance, mm } from "./Quantity.js";
3
- import { partitionPoint } from "./sorted.js";
4
- import { groupBy } from "./utils.js";
1
+ import { keyOf } from "../systems/TrackerBar.js";
2
+ import { distance, mm } from "../model/Quantity.js";
3
+ import { partitionPoint } from "../shared/sorted.js";
4
+ import { groupBy } from "../shared/utils.js";
5
5
  export const defaultCollationTolerance = { toleranceStart: mm(5), toleranceEnd: mm(5) };
6
6
  /** Where the window is centred at the start of a feature, on nothing where it names no offset. */
7
7
  export const offsetStartOf = (tolerance) => tolerance.offsetStart ?? mm(0);
@@ -1,7 +1,7 @@
1
- import { FeatureOrPatch } from "./Feature.js";
2
- import { RollCopy, Shift } from "./RollCopy.js";
3
- import { TrackerBar } from "./TrackerBar.js";
4
- import { Millimeters } from "./Quantity.js";
1
+ import { FeatureOrPatch } from "../model/Feature.js";
2
+ import { RollCopy, Shift } from "../model/RollCopy.js";
3
+ import { TrackerBar } from "../systems/TrackerBar.js";
4
+ import { Millimeters } from "../model/Quantity.js";
5
5
  export declare const applyShift: (shift: Shift, copy: RollCopy) => void;
6
6
  /** Scales the copy's features away from the beginning of the roll, and records the factor. */
7
7
  export declare const applyScale: (factor: number, copy: RollCopy) => void;
@@ -1,6 +1,6 @@
1
- import { featuresOf } from "./RollCopy.js";
2
- import { welteT100 } from "./systems/welteT100/bar.js";
3
- import { add, mm, scale, subtract } from "./Quantity.js";
1
+ import { featuresOf } from "../model/RollCopy.js";
2
+ import { defaultTrackerBar } from "../systems/index.js";
3
+ import { add, mm, scale, subtract } from "../model/Quantity.js";
4
4
  /** Moves both ends of a span, the far end only where the span has one. */
5
5
  const move = (span, by) => {
6
6
  span.from = add(span.from, by);
@@ -15,43 +15,39 @@ const stretch = (span, factor) => {
15
15
  };
16
16
  const back = (shift) => ({ horizontal: scale(shift.horizontal, -1), vertical: scale(shift.vertical, -1) });
17
17
  export const applyShift = (shift, copy) => {
18
- if (copy.ops.includes('shifted'))
18
+ if (copy.measurements.shift)
19
19
  return;
20
20
  featuresOf(copy).forEach(feature => {
21
21
  move(feature.horizontal, shift.horizontal);
22
22
  move(feature.vertical, shift.vertical);
23
23
  });
24
- copy.ops = [...copy.ops, 'shifted'];
25
24
  copy.measurements.shift = shift;
26
25
  };
27
26
  /** Scales the copy's features away from the beginning of the roll, and records the factor. */
28
27
  export const applyScale = (factor, copy) => {
29
- if (copy.ops.includes('stretched'))
28
+ if (copy.measurements.scale !== undefined)
30
29
  return;
31
30
  featuresOf(copy).forEach(feature => stretch(feature.horizontal, factor));
32
- copy.ops = [...copy.ops, 'stretched'];
33
31
  copy.measurements.scale = factor;
34
32
  };
35
33
  /** Takes the shift off the copy's features again, as far as one was applied. */
36
34
  export const revertShift = (copy) => {
37
35
  const shift = copy.measurements.shift;
38
- if (!copy.ops.includes('shifted') || !shift)
36
+ if (!shift)
39
37
  return;
40
38
  const reversed = back(shift);
41
39
  featuresOf(copy).forEach(feature => {
42
40
  move(feature.horizontal, reversed.horizontal);
43
41
  move(feature.vertical, reversed.vertical);
44
42
  });
45
- copy.ops = copy.ops.filter(op => op !== 'shifted');
46
43
  delete copy.measurements.shift;
47
44
  };
48
45
  /** Takes the scale off the copy's features again, as far as one was applied. */
49
46
  export const revertScale = (copy) => {
50
47
  const factor = copy.measurements.scale;
51
- if (!copy.ops.includes('stretched') || factor === undefined)
48
+ if (factor === undefined)
52
49
  return;
53
50
  featuresOf(copy).forEach(feature => stretch(feature.horizontal, 1 / factor));
54
- copy.ops = copy.ops.filter(op => op !== 'stretched');
55
51
  delete copy.measurements.scale;
56
52
  };
57
53
  const chainsOf = (copy) => featuresOf(copy).filter(feature => feature.type === 'HoleChain');
@@ -103,7 +99,7 @@ export const tooShortToShorten = (extension, copy, leaving = new Set()) => chain
103
99
  * extension, `tooShortToShorten` saying beforehand which those are.
104
100
  */
105
101
  export const shortenChains = (extension, copy, leaving = new Set()) => {
106
- if (copy.ops.includes('shortened'))
102
+ if (copy.measurements.readerExtension)
107
103
  return;
108
104
  const tooShort = tooShortToShorten(extension, copy, leaving);
109
105
  if (tooShort.length > 0) {
@@ -115,19 +111,17 @@ export const shortenChains = (extension, copy, leaving = new Set()) => {
115
111
  chainsOf(copy)
116
112
  .filter(chain => !leaving.has(chain.id))
117
113
  .forEach(chain => { chain.horizontal.to = subtract(chain.horizontal.to, extension); });
118
- copy.ops = [...copy.ops, 'shortened'];
119
114
  copy.measurements.readerExtension = { length: extension, ...(left.length > 0 && { leaving: left }) };
120
115
  };
121
116
  /** Puts the reader's extension back on the chains it was taken off, as far as one was taken off. */
122
117
  export const revertShortening = (copy) => {
123
118
  const taken = copy.measurements.readerExtension;
124
- if (!copy.ops.includes('shortened') || taken === undefined)
119
+ if (taken === undefined)
125
120
  return;
126
121
  const left = new Set(taken.leaving ?? []);
127
122
  chainsOf(copy)
128
123
  .filter(chain => !left.has(chain.id))
129
124
  .forEach(chain => { chain.horizontal.to = add(chain.horizontal.to, taken.length); });
130
- copy.ops = copy.ops.filter(op => op !== 'shortened');
131
125
  delete copy.measurements.readerExtension;
132
126
  };
133
127
  const byPlace = (x, y) => x.at - y.at || x.pitch - y.pitch;
@@ -251,7 +245,7 @@ const resultOf = ({ line, matches }) => {
251
245
  * sounds, and it is the pitches that are matched. Where a copy's holes
252
246
  * have already been put onto the edition's bar, that bar reads it.
253
247
  */
254
- export function alignFeatures(rollA, rollB, barA = welteT100, barB = barA) {
248
+ export function alignFeatures(rollA, rollB, barA = defaultTrackerBar, barB = barA) {
255
249
  const a = noteOnsets(rollA, barA);
256
250
  const b = noteOnsets(rollB, barB);
257
251
  const coarse = robustLine(anchors(a, b));
@@ -1,9 +1,9 @@
1
- import { Belief } from "./Assumption.js";
1
+ import { Belief } from "../model/Assumption.js";
2
2
  import { BothEnds, CollationTolerance, Displacement } from "./Collation.js";
3
- import { EditionView } from "./EditionView.js";
4
- import { Millimeters } from "./Quantity.js";
3
+ import { EditionView } from "../view/EditionView.js";
4
+ import { Millimeters } from "../model/Quantity.js";
5
5
  import { Histogram, Spread, Tail } from "./statistics.js";
6
- import { AnySymbol } from "./Symbol.js";
6
+ import { AnySymbol } from "../model/Symbol.js";
7
7
  /**
8
8
  * How far the copies of a roll disagree about where a symbol lies, and
9
9
  * the collation tolerance that follows from it.
@@ -1,9 +1,9 @@
1
1
  import { v4 } from "uuid";
2
2
  import { admits, offsetEndOf, offsetStartOf } from "./Collation.js";
3
- import { mean, mm } from "./Quantity.js";
3
+ import { mean, mm } from "../model/Quantity.js";
4
4
  import { excessKurtosisOf, histogramOf, normalQuantile, spreadOf, standardise, tailOf } from "./statistics.js";
5
- import { groupBy } from "./utils.js";
6
- import { deletedBy, insertedBy } from "./Version.js";
5
+ import { groupBy } from "../shared/utils.js";
6
+ import { deletedBy, insertedBy } from "../model/Version.js";
7
7
  const placeOf = (carriers) => ({
8
8
  from: mean(carriers.map(carrier => carrier.horizontal.from)),
9
9
  to: mean(carriers.map(carrier => carrier.horizontal.to))
@@ -1,4 +1,4 @@
1
- import { Quantity } from "./Quantity.js";
1
+ import { Quantity } from "../model/Quantity.js";
2
2
  /**
3
3
  * The statistics a sample of measurements is described with.
4
4
  *
@@ -1,4 +1,4 @@
1
- import { quantity } from "./Quantity.js";
1
+ import { quantity } from "../model/Quantity.js";
2
2
  /**
3
3
  * The statistics a sample of measurements is described with.
4
4
  *
@@ -116,6 +116,6 @@ export const histogramOf = (values, binWidth) => {
116
116
  counts: values.reduce((tally, value) => {
117
117
  tally[Math.min(bins - 1, Math.floor((value - first) / binWidth))] += 1;
118
118
  return tally;
119
- }, new Array(bins).fill(0))
119
+ }, Array.from({ length: bins }, () => 0))
120
120
  };
121
121
  };
@@ -1,29 +1,14 @@
1
1
  import { CollationTolerance, Locate } from "./Collation.js";
2
- import { AnySymbol, Expression } from "./Symbol.js";
3
- import { Millimeters, Track } from "./Quantity.js";
4
- import { TrackerBar } from "./TrackerBar.js";
2
+ import { AnySymbol, Expression } from "../model/Symbol.js";
3
+ import { Millimeters, Track } from "../model/Quantity.js";
4
+ import { Operation, TrackerBar } from "../systems/TrackerBar.js";
5
+ export type { Operation, Spelling } from "../systems/TrackerBar.js";
5
6
  /**
6
- * How a scale words a function: as a command that turns it on, one that
7
- * cancels it, or one that holds it for as long as it lasts.
7
+ * What an expression type operates, as the bar that reads it says, or
8
+ * nothing for a word no bar here knows or one without a counterpart in
9
+ * another scale. The bars share their words where they share a scale,
10
+ * as the Licensee does the T-100's, so the first that reads it answers.
8
11
  */
9
- export type Spelling = 'on' | 'off' | 'held';
10
- /**
11
- * What a word of a scale operates, rather than the word itself, so that
12
- * two scales can be compared at all.
13
- */
14
- export interface Operation {
15
- /** The function operated, named the same wherever a scale has it. */
16
- operates: string;
17
- spelling: Spelling;
18
- /**
19
- * Whether the half of the keyboard the valve serves is part of the
20
- * function operated. The dynamics are per half; the pedals are not,
21
- * and the two Welte scales put them on opposite edges of the paper,
22
- * so a rule that compared sides would leave every pedal unpaired.
23
- */
24
- sided: boolean;
25
- }
26
- /** What an expression type operates, or nothing for a word no scale here knows. */
27
12
  export declare const operationOf: (expressionType: string) => Operation | undefined;
28
13
  /**
29
14
  * A latched function of the older version and the held command of the
@@ -46,7 +31,7 @@ export interface Substitution {
46
31
  * 2.66 mm grid with bridges of about a millimetre, so a scan read hole
47
32
  * by hole shows a run where the paper shows one command.
48
33
  */
49
- export declare const defaultChainGap: import("./Quantity.js").Quantity<"mm">;
34
+ export declare const defaultChainGap: import("../index.js").Quantity<"mm">;
50
35
  /**
51
36
  * The position a bar reads its own command for the same function on,
52
37
  * where it has no word for this one.
@@ -1,44 +1,20 @@
1
1
  import { defaultCollationTolerance } from "./Collation.js";
2
- import { distance, mm, track } from "./Quantity.js";
2
+ import { distance, mm, track } from "../model/Quantity.js";
3
+ import { trackerBars } from "../systems/index.js";
3
4
  /**
4
- * What each Welte expression type operates.
5
- *
6
- * This cannot be read off the names. The T-100 latches a function on
7
- * with one perforation and cancels it with a second, while the T-98
8
- * holds it for as long as one perforation lasts (Hagmann pp. 89 f. and
9
- * 100–103; Phillips p. 121), and Welte renamed two of the functions
10
- * between the scales: the red `SlowCrescendo` is the green `Crescendo`,
11
- * and the red's single `Forzando` valve answers to two green ones that
12
- * name the end they pull towards. So the correspondence is stated here
13
- * rather than derived, and only two types that operate one function can
14
- * stand for each other.
15
- *
16
- * The T-100's `MotorOn`/`MotorOff`, `Rewind` and `ElectricCutOff` are
17
- * left out on purpose: the green scale has no word for any of them, its
18
- * motor switch being an automatic mercury contact (Skala-Rolle 98 §12)
19
- * and its rewind riding on the bass sforzando-piano line. A red command
20
- * of those kinds has no counterpart and stays a plain deletion.
5
+ * What an expression type operates, as the bar that reads it says, or
6
+ * nothing for a word no bar here knows or one without a counterpart in
7
+ * another scale. The bars share their words where they share a scale,
8
+ * as the Licensee does the T-100's, so the first that reads it answers.
21
9
  */
22
- const operations = {
23
- MezzoforteOn: { operates: 'mezzoforte', spelling: 'on', sided: true },
24
- MezzoforteOff: { operates: 'mezzoforte', spelling: 'off', sided: true },
25
- SlowCrescendoOn: { operates: 'crescendo', spelling: 'on', sided: true },
26
- SlowCrescendoOff: { operates: 'crescendo', spelling: 'off', sided: true },
27
- ForzandoOn: { operates: 'sforzando', spelling: 'on', sided: true },
28
- ForzandoOff: { operates: 'sforzando', spelling: 'off', sided: true },
29
- SustainPedalOn: { operates: 'sustainPedal', spelling: 'on', sided: false },
30
- SustainPedalOff: { operates: 'sustainPedal', spelling: 'off', sided: false },
31
- SoftPedalOn: { operates: 'softPedal', spelling: 'on', sided: false },
32
- SoftPedalOff: { operates: 'softPedal', spelling: 'off', sided: false },
33
- Mezzoforte: { operates: 'mezzoforte', spelling: 'held', sided: true },
34
- Crescendo: { operates: 'crescendo', spelling: 'held', sided: true },
35
- SforzandoForte: { operates: 'sforzando', spelling: 'held', sided: true },
36
- SforzandoPiano: { operates: 'sforzando', spelling: 'held', sided: true },
37
- SustainPedal: { operates: 'sustainPedal', spelling: 'held', sided: false },
38
- SoftPedal: { operates: 'softPedal', spelling: 'held', sided: false }
10
+ export const operationOf = (expressionType) => {
11
+ for (const bar of trackerBars) {
12
+ const operation = bar.operationOf(expressionType);
13
+ if (operation)
14
+ return operation;
15
+ }
16
+ return undefined;
39
17
  };
40
- /** What an expression type operates, or nothing for a word no scale here knows. */
41
- export const operationOf = (expressionType) => operations[expressionType];
42
18
  /**
43
19
  * How wide a paper bridge may be for two held perforations to be one
44
20
  * command. The T-98 punches a hold as a chain of round holes on a
@@ -0,0 +1,64 @@
1
+ import { MidiFile } from "midifile-ts";
2
+ import { EditionView } from "../view/EditionView.js";
3
+ import type { TrackerBar } from "../systems/TrackerBar.js";
4
+ import { Version } from "../model/Version.js";
5
+ import { AnyPerformedRollFeature, EmulatedCurve, NegotiatedEvent, ReproducingSystem } from "../systems/ReproducingSystem.js";
6
+ import { Millimeters } from "../model/Quantity.js";
7
+ export type EmulationScope = {
8
+ /** Only notes whose onset lies within this span of the roll are played. */
9
+ range?: [Millimeters, Millimeters];
10
+ /** Start the clock at the first note rather than at the beginning of the roll. */
11
+ skipToFirstNote?: boolean;
12
+ };
13
+ /**
14
+ * The events moved to where their statements put them, the events given
15
+ * left as they were. The view supplies the copies, whose measurements
16
+ * decide how far before or after its reference a command goes, and a
17
+ * punch diameter, or a millimetre, where no copy agrees with a statement.
18
+ */
19
+ export declare const withPlacementsApplied: (view: EditionView, events: readonly NegotiatedEvent[]) => NegotiatedEvent[];
20
+ /**
21
+ * The commands of a version as the bar performs them, in order of place,
22
+ * within the scope and with the placements applied. A note plays only
23
+ * where its onset falls in the range; expressions play throughout.
24
+ */
25
+ export declare const negotiatedEventsOf: (view: EditionView, version: Readonly<Version>, bar: TrackerBar, { range }?: Pick<EmulationScope, 'range'>) => NegotiatedEvent[];
26
+ /** A version performed: what was negotiated, and what the system made of it. */
27
+ export interface Emulated {
28
+ /** The id of the version performed. */
29
+ readonly source: string;
30
+ readonly negotiated: readonly NegotiatedEvent[];
31
+ /** The performed events in order of time, from the origin the scope sets. */
32
+ readonly events: readonly AnyPerformedRollFeature[];
33
+ readonly curves: readonly EmulatedCurve[];
34
+ }
35
+ /**
36
+ * Performs a version on a reproducing system: the symbols are negotiated
37
+ * into placed events and the system plays them. Nothing given is changed.
38
+ */
39
+ export declare const emulate: <Options extends object>(system: ReproducingSystem<Options>, version: Readonly<Version>, view: EditionView, options?: Options, { range, skipToFirstNote }?: EmulationScope) => Emulated;
40
+ /**
41
+ * The performed events as a MIDI file, in which every note and pedal step
42
+ * is labelled with the symbol it performs. The file names the system, the
43
+ * version and the options it was played with.
44
+ */
45
+ export declare const midiOf: (events: readonly AnyPerformedRollFeature[], systemName: string, options: object, source?: string) => MidiFile;
46
+ /**
47
+ * A version of the edition, performed and kept: `emulate` and `midiOf`
48
+ * with the result held on the object between the two calls. Prefer the
49
+ * functions where nothing needs holding.
50
+ */
51
+ export declare class Emulation<Options extends object> {
52
+ readonly system: ReproducingSystem<Options>;
53
+ options: Options;
54
+ midiEvents: AnyPerformedRollFeature[];
55
+ negotiatedEvents: NegotiatedEvent[];
56
+ curves: readonly EmulatedCurve[];
57
+ source?: string;
58
+ constructor(system: ReproducingSystem<Options>, options?: Options);
59
+ /** Moves the negotiated events to where their statements put them; see `withPlacementsApplied`. */
60
+ applyConstraints(view: EditionView): void;
61
+ emulateVersion(version: Version, view: EditionView, scope?: EmulationScope): AnyPerformedRollFeature[];
62
+ findEventsPerforming(id: string): AnyPerformedRollFeature[];
63
+ asMIDI(): MidiFile;
64
+ }
@@ -0,0 +1,282 @@
1
+ import { MIDIControlEvents } from "midifile-ts";
2
+ import { idOf } from "../model/Assumption.js";
3
+ import { isCommand, pairsAmong, placementsOf } from "../model/Symbol.js";
4
+ import { add, mean, mm, seconds, subtract } from "../model/Quantity.js";
5
+ import { punchDiameterOf } from "../model/RollCopy.js";
6
+ import { toOwnPaperOf } from "../analysis/ownPaper.js";
7
+ import { negotiatedEventOf } from "./negotiation.js";
8
+ /** The punch diameter the edition's copies report, where any of them does. */
9
+ const meanPunchDiameterOf = (view) => {
10
+ const measured = view.edition.copies
11
+ .map(punchDiameterOf)
12
+ .filter(value => value !== undefined);
13
+ return measured.length > 0 ? mean(measured) : undefined;
14
+ };
15
+ const propertiesOf = (view, version) => ({
16
+ punchDiameter: meanPunchDiameterOf(view),
17
+ tempo: view.edition.tempoAdjustment,
18
+ toOwnPaper: toOwnPaperOf(view, version)
19
+ });
20
+ /** The onset a symbol has on each copy carrying it, by the copy's id, as the mean of its chains there. */
21
+ const onsetsByCopy = (view, symbolId) => {
22
+ const symbol = view.symbol(symbolId);
23
+ if (!symbol)
24
+ return new Map();
25
+ const onsets = view.placedCarriersOf(symbol).flatMap((carrier) => {
26
+ const copy = view.copyOf(carrier.id);
27
+ return copy ? [[copy.id, carrier.horizontal.from]] : [];
28
+ });
29
+ const copies = new Set(onsets.map(([copy]) => copy));
30
+ return new Map([...copies].map(copy => [copy, mean(onsets.filter(([at]) => at === copy).map(([, onset]) => onset))]));
31
+ };
32
+ /**
33
+ * The measured distance from the onset of the reference to the onset of
34
+ * the follower on each copy carrying both, negative where the follower
35
+ * comes first there.
36
+ */
37
+ const offsetsBetween = (view, followerId, referenceId) => {
38
+ const references = onsetsByCopy(view, referenceId);
39
+ return [...onsetsByCopy(view, followerId)].flatMap(([copy, onset]) => {
40
+ const reference = references.get(copy);
41
+ return reference === undefined ? [] : [subtract(onset, reference)];
42
+ });
43
+ };
44
+ /**
45
+ * How far before or after its reference a follower is put when the
46
+ * measurement does not already have it there: as far as the copies
47
+ * that agree with the statement put it, and one gap where none does.
48
+ */
49
+ const distanceOnSide = (side, offsets, gap) => {
50
+ const agreeing = offsets.filter(offset => side === 'before' ? offset < 0 : offset > 0);
51
+ return agreeing.length > 0 ? mm(Math.abs(mean(agreeing))) : gap;
52
+ };
53
+ /**
54
+ * How far each event has to move for the placements to hold:
55
+ * an aligned event takes the onset of its reference, one placed before
56
+ * or after its reference keeps its place where the measurement has it
57
+ * on that side and is moved there where it does not, and a paired
58
+ * event follows its partner so that the distance between the two is
59
+ * kept. Events that stay where they are do not appear.
60
+ */
61
+ const displacementsOf = (events, offsetsBetween, gap) => {
62
+ const byId = new Map(events.map(event => [event.id, event]));
63
+ /** Where an event comes to lie once its statement and those of its references hold. */
64
+ const placedOnsetOf = (event, visited = new Set()) => {
65
+ const measured = event.horizontal.from;
66
+ const placement = placementsOf(event)[0];
67
+ const reference = placement && byId.get(idOf(placement.reference));
68
+ if (!placement || !reference || reference.id === event.id || visited.has(event.id))
69
+ return measured;
70
+ const referenceOnset = placedOnsetOf(reference, new Set([...visited, event.id]));
71
+ const distance = (side) => distanceOnSide(side, offsetsBetween(event.id, reference.id), gap);
72
+ switch (placement.relation) {
73
+ case 'alignedWith': return referenceOnset;
74
+ case 'before': return measured < referenceOnset ? measured : subtract(referenceOnset, distance('before'));
75
+ case 'after': return measured > referenceOnset ? measured : add(referenceOnset, distance('after'));
76
+ }
77
+ };
78
+ const displacements = new Map(events
79
+ .map((event) => [event, subtract(placedOnsetOf(event), event.horizontal.from)])
80
+ .filter(([, distance]) => distance !== 0));
81
+ pairsAmong(events).forEach(([one, other]) => {
82
+ const shared = displacements.get(one) ?? displacements.get(other);
83
+ if (shared === undefined)
84
+ return;
85
+ displacements.set(one, shared);
86
+ displacements.set(other, shared);
87
+ });
88
+ return displacements;
89
+ };
90
+ /** The earliest of the times, or the beginning of the roll where there are none. */
91
+ const earliestOf = (times) => times.length === 0 ? seconds(0) : times.reduce((soonest, at) => at < soonest ? at : soonest);
92
+ /**
93
+ * The events moved to where their statements put them, the events given
94
+ * left as they were. The view supplies the copies, whose measurements
95
+ * decide how far before or after its reference a command goes, and a
96
+ * punch diameter, or a millimetre, where no copy agrees with a statement.
97
+ */
98
+ export const withPlacementsApplied = (view, events) => {
99
+ const gap = meanPunchDiameterOf(view) ?? mm(1);
100
+ const offsets = (follower, reference) => offsetsBetween(view, follower, reference);
101
+ const displacements = displacementsOf(events, offsets, gap);
102
+ return events.map(event => {
103
+ const distance = displacements.get(event);
104
+ return distance === undefined ? event : {
105
+ ...event,
106
+ horizontal: { ...event.horizontal, from: add(event.horizontal.from, distance), to: add(event.horizontal.to, distance) }
107
+ };
108
+ });
109
+ };
110
+ /**
111
+ * The commands of a version as the bar performs them, in order of place,
112
+ * within the scope and with the placements applied. A note plays only
113
+ * where its onset falls in the range; expressions play throughout.
114
+ */
115
+ export const negotiatedEventsOf = (view, version, bar, { range } = {}) => {
116
+ const inScope = (event) => !range || event.type !== 'note' || (event.horizontal.from > range[0] && event.horizontal.from < range[1]);
117
+ return withPlacementsApplied(view, view.snapshot(version.id)
118
+ .filter(isCommand)
119
+ .map(symbol => negotiatedEventOf(view, symbol, bar))
120
+ .filter(event => event !== null)
121
+ .filter(inScope));
122
+ };
123
+ /**
124
+ * Performs a version on a reproducing system: the symbols are negotiated
125
+ * into placed events and the system plays them. Nothing given is changed.
126
+ */
127
+ export const emulate = (system, version, view, options = system.defaultOptions, { range, skipToFirstNote = false } = {}) => {
128
+ const negotiated = negotiatedEventsOf(view, version, system.trackerBar, { range });
129
+ if (negotiated.length === 0)
130
+ return { source: version.id, negotiated, events: [], curves: [] };
131
+ const performance = system.perform(negotiated, options, propertiesOf(view, version));
132
+ const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
133
+ const origin = skipToFirstNote ? earliestOf(onsets) : seconds(0);
134
+ return {
135
+ source: version.id,
136
+ negotiated,
137
+ curves: performance.curves,
138
+ events: performance.events
139
+ .map(event => ({ ...event, at: subtract(event.at, origin) }))
140
+ .filter(event => event.at >= 0)
141
+ .sort((a, b) => a.at - b.at)
142
+ };
143
+ };
144
+ /**
145
+ * The performed events as a MIDI file, in which every note and pedal step
146
+ * is labelled with the symbol it performs. The file names the system, the
147
+ * version and the options it was played with.
148
+ */
149
+ export const midiOf = (events, systemName, options, source) => {
150
+ const TICKS_PER_SECOND = 1000;
151
+ const midi = [];
152
+ const text = (text, deltaTime = 0) => ({ type: 'meta', subtype: 'text', text, deltaTime });
153
+ const controller = (controllerType, value, deltaTime = 0) => ({ type: 'channel', subtype: 'controller', controllerType, value, deltaTime, channel: 0 });
154
+ const controllerOf = (event) => event.type === 'damper' ? MIDIControlEvents.SUSTAIN : MIDIControlEvents.SOFT_PEDAL;
155
+ midi.push(text(`linked-rolls (${systemName})`));
156
+ if (source) {
157
+ midi.push(text(source));
158
+ }
159
+ for (const [key, value] of Object.entries(options)) {
160
+ midi.push(text(`${key}=${typeof value === 'object' ? JSON.stringify(value) : value}`));
161
+ }
162
+ midi.push({
163
+ type: 'meta',
164
+ subtype: 'setTempo',
165
+ microsecondsPerBeat: 1000000,
166
+ deltaTime: 0
167
+ });
168
+ // both pedals start at rest
169
+ midi.push(controller(MIDIControlEvents.SUSTAIN, 0), controller(MIDIControlEvents.SOFT_PEDAL, 0));
170
+ // a pedal step is labelled with its command only where that
171
+ // command changes, so the file is not swamped with labels
172
+ const lastCause = {};
173
+ let currentTick = 0;
174
+ for (const event of events.toSorted((a, b) => a.at - b.at)) {
175
+ const tick = Math.round(event.at * TICKS_PER_SECOND);
176
+ const deltaTime = tick - currentTick;
177
+ currentTick = tick;
178
+ if (event.type === 'noteOn') {
179
+ midi.push(text(event.performs.id, deltaTime));
180
+ midi.push({
181
+ type: 'channel',
182
+ subtype: 'noteOn',
183
+ noteNumber: event.pitch,
184
+ velocity: +event.velocity.toFixed(0),
185
+ deltaTime: 0,
186
+ channel: 0
187
+ });
188
+ }
189
+ else if (event.type === 'noteOff') {
190
+ midi.push({
191
+ type: 'channel',
192
+ subtype: 'noteOff',
193
+ noteNumber: event.pitch,
194
+ velocity: 127,
195
+ deltaTime,
196
+ channel: 0
197
+ });
198
+ }
199
+ else {
200
+ const labelled = lastCause[event.type] === event.performs.id;
201
+ lastCause[event.type] = event.performs.id;
202
+ if (!labelled) {
203
+ midi.push(text(event.performs.id, deltaTime));
204
+ }
205
+ midi.push(controller(controllerOf(event), event.value, labelled ? deltaTime : 0));
206
+ }
207
+ }
208
+ return {
209
+ header: {
210
+ ticksPerBeat: TICKS_PER_SECOND,
211
+ formatType: 0,
212
+ trackCount: 1
213
+ },
214
+ tracks: [midi]
215
+ };
216
+ };
217
+ /**
218
+ * A version of the edition, performed and kept: `emulate` and `midiOf`
219
+ * with the result held on the object between the two calls. Prefer the
220
+ * functions where nothing needs holding.
221
+ */
222
+ export class Emulation {
223
+ constructor(system, options = system.defaultOptions) {
224
+ Object.defineProperty(this, "system", {
225
+ enumerable: true,
226
+ configurable: true,
227
+ writable: true,
228
+ value: void 0
229
+ });
230
+ Object.defineProperty(this, "options", {
231
+ enumerable: true,
232
+ configurable: true,
233
+ writable: true,
234
+ value: void 0
235
+ });
236
+ Object.defineProperty(this, "midiEvents", {
237
+ enumerable: true,
238
+ configurable: true,
239
+ writable: true,
240
+ value: []
241
+ });
242
+ // sorted list of events with the negotiated assumptions already applied
243
+ Object.defineProperty(this, "negotiatedEvents", {
244
+ enumerable: true,
245
+ configurable: true,
246
+ writable: true,
247
+ value: []
248
+ });
249
+ Object.defineProperty(this, "curves", {
250
+ enumerable: true,
251
+ configurable: true,
252
+ writable: true,
253
+ value: []
254
+ });
255
+ Object.defineProperty(this, "source", {
256
+ enumerable: true,
257
+ configurable: true,
258
+ writable: true,
259
+ value: void 0
260
+ });
261
+ this.system = system;
262
+ this.options = options;
263
+ }
264
+ /** Moves the negotiated events to where their statements put them; see `withPlacementsApplied`. */
265
+ applyConstraints(view) {
266
+ this.negotiatedEvents = withPlacementsApplied(view, this.negotiatedEvents);
267
+ }
268
+ emulateVersion(version, view, scope = {}) {
269
+ const emulated = emulate(this.system, version, view, this.options, scope);
270
+ this.source = emulated.source;
271
+ this.negotiatedEvents = [...emulated.negotiated];
272
+ this.curves = emulated.curves;
273
+ this.midiEvents = [...emulated.events];
274
+ return this.midiEvents;
275
+ }
276
+ findEventsPerforming(id) {
277
+ return this.midiEvents.filter(event => event.performs.id === id);
278
+ }
279
+ asMIDI() {
280
+ return midiOf(this.midiEvents, this.system.name, this.options, this.source);
281
+ }
282
+ }
@@ -0,0 +1,14 @@
1
+ import type { EditionView } from "../view/EditionView.js";
2
+ import type { Expression, Note } from "../model/Symbol.js";
3
+ import type { TrackerBar } from "../systems/TrackerBar.js";
4
+ import type { NegotiatedEvent } from "../systems/ReproducingSystem.js";
5
+ /**
6
+ * The symbol as a performance needs it: where it lies, and the
7
+ * position the performing bar reads it on.
8
+ *
9
+ * Nothing where that bar reads nothing of it, which is the case a
10
+ * transfer between systems leaves behind: a red `ForzandoOn` a
11
+ * green version still inherits cannot be performed on a green
12
+ * machine, and an edit has yet to say what took its place.
13
+ */
14
+ export declare const negotiatedEventOf: (view: EditionView, symbol: Note | Expression, bar: TrackerBar) => NegotiatedEvent | null;