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,11 +1,9 @@
1
- import { Edition } from "./Edition.js";
2
- import { FeatureOrPatch, HorizontalSpan, NestedFeature } from "./Feature.js";
3
- import { AnySymbol, Expression, Note } from "./Symbol.js";
4
- import { Version } from "./Version.js";
5
- import { NegotiatedEvent } from "./ReproducingSystem.js";
6
- import { TrackerBar } from "./TrackerBar.js";
7
- import { Modification, ProductionEvent, RollCopy } from "./RollCopy.js";
8
- import { Millimeters } from "./Quantity.js";
1
+ import { Edition } from "../model/Edition.js";
2
+ import { FeatureOrPatch, HorizontalSpan, NestedFeature } from "../model/Feature.js";
3
+ import { AnySymbol } from "../model/Symbol.js";
4
+ import { Version } from "../model/Version.js";
5
+ import { Modification, ProductionEvent, RollCopy } from "../model/RollCopy.js";
6
+ import { Millimeters } from "../model/Quantity.js";
9
7
  export type Path = (string | number)[];
10
8
  export declare const getAt: <T>(path: Path, obj: unknown) => T | undefined;
11
9
  export declare class EditionView {
@@ -30,7 +28,13 @@ export declare class EditionView {
30
28
  private readonly symbolsById;
31
29
  private featuresById?;
32
30
  constructor(edition: Edition);
31
+ /** What stands at the path, or null where nothing does. A zero or an empty string stands there as well. */
33
32
  atPath<T>(path: Path): T | null;
33
+ /**
34
+ * Walks the edition and records where each id stands and where it is
35
+ * referenced. The constructor does this; a second call starts afresh
36
+ * rather than recording every reference twice.
37
+ */
34
38
  indexObjects(): void;
35
39
  version(id: string): Readonly<Version> | undefined;
36
40
  copy(id: string): Readonly<RollCopy> | undefined;
@@ -43,6 +47,11 @@ export declare class EditionView {
43
47
  features(ids: readonly string[]): Readonly<NestedFeature>[];
44
48
  getPath(anyId: string): Path | undefined;
45
49
  linksTo(anyId: string): Path[];
50
+ /**
51
+ * Calls back on the version and its ancestors along the principal
52
+ * line. A stemma that loops back on itself is walked once round, so
53
+ * that an edition in that state can still be read and repaired.
54
+ */
46
55
  travelUp(versionId: string, callback: (version: Readonly<Version>) => void): void;
47
56
  /** The version and its ancestors along the principal line, from the version up to the root. */
48
57
  lineageOf(versionId: string): Readonly<Version>[];
@@ -60,29 +69,6 @@ export declare class EditionView {
60
69
  * `mergeFeatures` asks before it reads them as one.
61
70
  */
62
71
  actOf(featureId: string): Readonly<ProductionEvent | Modification> | undefined;
63
- /**
64
- * How a place on the edition's shared axis relates to the paper of
65
- * this version: place × factor = millimetres of its own paper.
66
- *
67
- * Copies cut for different systems are scaled onto one axis so that
68
- * they can be collated at all, which leaves a version of another
69
- * system carrying places in the axis copy's millimetres. A
70
- * performance needs the paper the roll actually ran on, and the
71
- * factor is the inverse of the scale `alignCopy` recorded.
72
- *
73
- * It is read only from the copies of the version's own system, since
74
- * under the shared axis a green version's notes are carried by red
75
- * copies too and those say nothing about green paper. A copy whose
76
- * scale is put down to its own paper having stretched is left out as
77
- * well: that is a fact about the one exemplar, not about the speed
78
- * the system's rolls were cut at. Where what remains disagrees,
79
- * `constraintProblems` reports it rather than averaging it away.
80
- */
81
- toOwnPaperOf(version: Readonly<Version>): number | undefined;
82
- /** The scales of the version's own copies that are not their own paper stretch. */
83
- speedScalesIn(version: Readonly<Version>): number[];
84
- /** The copies of the version's own system that carry any of its symbols. */
85
- copiesOwning(version: Readonly<Version>): Readonly<RollCopy>[];
86
72
  /** The version the given one's text is read against, by its principal derivation. */
87
73
  predecessorOf(versionId: string): Readonly<Version> | undefined;
88
74
  /**
@@ -114,14 +100,4 @@ export declare class EditionView {
114
100
  withGenerations(): Array<Version & {
115
101
  generation: number;
116
102
  }>;
117
- /**
118
- * The symbol as a performance needs it: where it lies, and the
119
- * position the performing bar reads it on.
120
- *
121
- * Nothing where that bar reads nothing of it, which is the case a
122
- * transfer between systems leaves behind: a red `ForzandoOn` a
123
- * green version still inherits cannot be performed on a green
124
- * machine, and an edit has yet to say what took its place.
125
- */
126
- simplifySymbol(symbol: Note | Expression, bar: TrackerBar): NegotiatedEvent | null;
127
103
  }
@@ -1,9 +1,8 @@
1
- import { isPlaced, withBorneFeatures } from "./Feature.js";
2
- import { deletedBy, insertedBy, principalDerivationOf } from "./Version.js";
3
- import { systemIdOf } from "./TrackerBar.js";
4
- import { featuresMadeBy, featuresOf, isPaperStretch } from "./RollCopy.js";
5
- import { idOf, idsOf } from "./Assumption.js";
6
- import { mean } from "./Quantity.js";
1
+ import { isPlaced, withBorneFeatures } from "../model/Feature.js";
2
+ import { deletedBy, insertedBy, principalDerivationOf } from "../model/Version.js";
3
+ import { featuresMadeBy, featuresOf } from "../model/RollCopy.js";
4
+ import { idOf, idsOf } from "../model/Assumption.js";
5
+ import { mean } from "../model/Quantity.js";
7
6
  export const getAt = (path, obj) => {
8
7
  let node = obj;
9
8
  for (const key of path) {
@@ -95,11 +94,18 @@ export class EditionView {
95
94
  this.symbolsById = byIdIn(edition.versions.flatMap(insertedBy));
96
95
  this.indexObjects();
97
96
  }
97
+ /** What stands at the path, or null where nothing does. A zero or an empty string stands there as well. */
98
98
  atPath(path) {
99
- const node = getAt(path, this.edition);
100
- return node || null;
99
+ return getAt(path, this.edition) ?? null;
101
100
  }
101
+ /**
102
+ * Walks the edition and records where each id stands and where it is
103
+ * referenced. The constructor does this; a second call starts afresh
104
+ * rather than recording every reference twice.
105
+ */
102
106
  indexObjects() {
107
+ this.paths.clear();
108
+ this.links.clear();
103
109
  const visited = new WeakSet();
104
110
  const link = (id, trail) => {
105
111
  const trails = this.links.get(id);
@@ -172,14 +178,19 @@ export class EditionView {
172
178
  linksTo(anyId) {
173
179
  return (this.links.get(anyId) ?? []).map(pathOf);
174
180
  }
181
+ /**
182
+ * Calls back on the version and its ancestors along the principal
183
+ * line. A stemma that loops back on itself is walked once round, so
184
+ * that an edition in that state can still be read and repaired.
185
+ */
175
186
  travelUp(versionId, callback) {
176
- const v = this.version(versionId);
177
- if (!v)
178
- return;
179
- callback(v);
180
- const principal = principalDerivationOf(v);
181
- if (principal) {
182
- this.travelUp(idOf(principal), callback);
187
+ const visited = new Set();
188
+ let v = this.version(versionId);
189
+ while (v && !visited.has(v.id)) {
190
+ visited.add(v.id);
191
+ callback(v);
192
+ const principal = principalDerivationOf(v);
193
+ v = principal && this.version(idOf(principal));
183
194
  }
184
195
  }
185
196
  /** The version and its ancestors along the principal line, from the version up to the root. */
@@ -221,46 +232,6 @@ export class EditionView {
221
232
  return copy.production;
222
233
  return copy.modifications.find(act => states(featuresMadeBy(act)));
223
234
  }
224
- /**
225
- * How a place on the edition's shared axis relates to the paper of
226
- * this version: place × factor = millimetres of its own paper.
227
- *
228
- * Copies cut for different systems are scaled onto one axis so that
229
- * they can be collated at all, which leaves a version of another
230
- * system carrying places in the axis copy's millimetres. A
231
- * performance needs the paper the roll actually ran on, and the
232
- * factor is the inverse of the scale `alignCopy` recorded.
233
- *
234
- * It is read only from the copies of the version's own system, since
235
- * under the shared axis a green version's notes are carried by red
236
- * copies too and those say nothing about green paper. A copy whose
237
- * scale is put down to its own paper having stretched is left out as
238
- * well: that is a fact about the one exemplar, not about the speed
239
- * the system's rolls were cut at. Where what remains disagrees,
240
- * `constraintProblems` reports it rather than averaging it away.
241
- */
242
- toOwnPaperOf(version) {
243
- const scales = this.speedScalesIn(version);
244
- return scales.length === 1 ? 1 / scales[0] : undefined;
245
- }
246
- /** The scales of the version's own copies that are not their own paper stretch. */
247
- speedScalesIn(version) {
248
- return [...new Set(this.copiesOwning(version)
249
- .filter(copy => !copy.conditions.some(isPaperStretch))
250
- .map(copy => copy.measurements.scale)
251
- .filter((scale) => scale !== undefined && scale > 0))];
252
- }
253
- /** The copies of the version's own system that carry any of its symbols. */
254
- copiesOwning(version) {
255
- const system = systemIdOf(version.system);
256
- const carrying = new Set(this.snapshot(version.id)
257
- .flatMap(symbol => idsOf(symbol.carriers))
258
- .flatMap(id => {
259
- const copy = this.copyOf(id);
260
- return copy ? [copy.id] : [];
261
- }));
262
- return this.edition.copies.filter(copy => carrying.has(copy.id) && systemIdOf(copy.production?.system) === system);
263
- }
264
235
  /** The version the given one's text is read against, by its principal derivation. */
265
236
  predecessorOf(versionId) {
266
237
  const v = this.version(versionId);
@@ -355,24 +326,4 @@ export class EditionView {
355
326
  }));
356
327
  return withGen;
357
328
  }
358
- /**
359
- * The symbol as a performance needs it: where it lies, and the
360
- * position the performing bar reads it on.
361
- *
362
- * Nothing where that bar reads nothing of it, which is the case a
363
- * transfer between systems leaves behind: a red `ForzandoOn` a
364
- * green version still inherits cannot be performed on a green
365
- * machine, and an edit has yet to say what took its place.
366
- */
367
- simplifySymbol(symbol, bar) {
368
- const horizontal = this.placeOf(symbol);
369
- const position = bar.positionOf(symbol);
370
- if (!horizontal || position === undefined)
371
- return null;
372
- return {
373
- ...symbol,
374
- horizontal,
375
- vertical: { unit: 'track', from: position }
376
- };
377
- }
378
329
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.57.0",
3
+ "version": "0.59.0",
4
4
  "description": "Digital editions of piano rolls: import, collation, editorial assumptions, JSON-LD export, and emulation through a reproducing system",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -16,6 +16,10 @@
16
16
  "types": "./lib/index.d.ts",
17
17
  "default": "./lib/index.js"
18
18
  },
19
+ "./validate": {
20
+ "types": "./lib/validate.d.ts",
21
+ "default": "./lib/validate.js"
22
+ },
19
23
  "./welte-t100": {
20
24
  "types": "./lib/systems/welteT100/system.d.ts",
21
25
  "default": "./lib/systems/welteT100/system.js"
@@ -28,7 +32,6 @@
28
32
  "types": "./lib/systems/welteLicensee/system.d.ts",
29
33
  "default": "./lib/systems/welteLicensee/system.js"
30
34
  },
31
- "./lib/*": "./lib/*",
32
35
  "./package.json": "./package.json"
33
36
  },
34
37
  "files": [
@@ -40,6 +43,7 @@
40
43
  "build": "npm run build:schema && rm -rf lib && tsc",
41
44
  "prepublishOnly": "npm run build && vitest run && npm run smoke",
42
45
  "test": "vitest",
46
+ "lint": "oxlint -c .oxlintrc.json src test scripts schema",
43
47
  "build:schema": "npx ts-json-schema-generator --additional-properties --validation-keywords see --path 'src/**/*.ts' --type 'Edition' > schema_.json && node schema/postprocess.js && rm schema_.json && mv schema.json src/schema.json",
44
48
  "build:docs": "node schema/docs/generate.ts src/schema.json docs/index.html",
45
49
  "smoke": "node scripts/smoke.mjs"
@@ -50,6 +54,7 @@
50
54
  "@types/node": "^24.13.3",
51
55
  "jsonld": "^9.0.0",
52
56
  "n3": "^2.7.12",
57
+ "oxlint": "^1.85.0",
53
58
  "ts-json-schema-generator": "^2.9.0",
54
59
  "typescript": "^7.0.2",
55
60
  "vite": "^8.2.2",
@@ -1,36 +0,0 @@
1
- import { MidiFile } from "midifile-ts";
2
- import { EditionView } from "./EditionView.js";
3
- import { Version } from "./Version.js";
4
- import { AnyPerformedRollFeature, EmulatedCurve, NegotiatedEvent, ReproducingSystem } from "./ReproducingSystem.js";
5
- import { Millimeters } from "./Quantity.js";
6
- export type EmulationScope = {
7
- /** Only notes whose onset lies within this span of the roll are played. */
8
- range?: [Millimeters, Millimeters];
9
- /** Start the clock at the first note rather than at the beginning of the roll. */
10
- skipToFirstNote?: boolean;
11
- };
12
- /**
13
- * A version of the edition, performed: the symbols are negotiated into
14
- * placed events, the reproducing system plays them, and the result goes
15
- * out as MIDI in which every note and pedal step is labelled with the
16
- * symbol it performs.
17
- */
18
- export declare class Emulation<Options extends object> {
19
- readonly system: ReproducingSystem<Options>;
20
- options: Options;
21
- midiEvents: AnyPerformedRollFeature[];
22
- negotiatedEvents: NegotiatedEvent[];
23
- curves: readonly EmulatedCurve[];
24
- source?: string;
25
- constructor(system: ReproducingSystem<Options>, options?: Options);
26
- /**
27
- * Moves the negotiated events to where their statements put them.
28
- * The view supplies the copies, whose measurements decide how far
29
- * before or after its reference a command goes, and a punch
30
- * diameter, or a millimetre, where no copy agrees with a statement.
31
- */
32
- applyConstraints(view: EditionView): void;
33
- emulateVersion(version: Version, view: EditionView, { range, skipToFirstNote }?: EmulationScope): AnyPerformedRollFeature[];
34
- findEventsPerforming(id: string): AnyPerformedRollFeature[];
35
- asMIDI(): MidiFile;
36
- }
package/lib/Emulation.js DELETED
@@ -1,250 +0,0 @@
1
- import { MIDIControlEvents } from "midifile-ts";
2
- import { idOf } from "./Assumption.js";
3
- import { isCommand, pairsAmong, placementsOf } from "./Symbol.js";
4
- import { add, mean, mm, seconds, subtract } from "./Quantity.js";
5
- import { punchDiameterOf } from "./RollCopy.js";
6
- /** The punch diameter the edition's copies report, where any of them does. */
7
- const meanPunchDiameterOf = (view) => {
8
- const measured = view.edition.copies
9
- .map(punchDiameterOf)
10
- .filter(value => value !== undefined);
11
- return measured.length > 0 ? mean(measured) : undefined;
12
- };
13
- const propertiesOf = (view, version) => ({
14
- punchDiameter: meanPunchDiameterOf(view),
15
- tempo: view.edition.tempoAdjustment,
16
- toOwnPaper: view.toOwnPaperOf(version)
17
- });
18
- /** The onset a symbol has on each copy carrying it, by the copy's index, as the mean of its chains there. */
19
- const onsetsByCopy = (view, symbolId) => {
20
- const symbol = view.symbol(symbolId);
21
- if (!symbol)
22
- return new Map();
23
- const onsets = view.placedCarriersOf(symbol).flatMap((carrier) => {
24
- const copy = view.getPath(carrier.id)?.[1];
25
- return typeof copy === 'number' ? [[copy, carrier.horizontal.from]] : [];
26
- });
27
- const copies = new Set(onsets.map(([copy]) => copy));
28
- return new Map([...copies].map(copy => [copy, mean(onsets.filter(([at]) => at === copy).map(([, onset]) => onset))]));
29
- };
30
- /**
31
- * The measured distance from the onset of the reference to the onset of
32
- * the follower on each copy carrying both, negative where the follower
33
- * comes first there.
34
- */
35
- const offsetsBetween = (view, followerId, referenceId) => {
36
- const references = onsetsByCopy(view, referenceId);
37
- return [...onsetsByCopy(view, followerId)].flatMap(([copy, onset]) => {
38
- const reference = references.get(copy);
39
- return reference === undefined ? [] : [subtract(onset, reference)];
40
- });
41
- };
42
- /**
43
- * How far before or after its reference a follower is put when the
44
- * measurement does not already have it there: as far as the copies
45
- * that agree with the statement put it, and one gap where none does.
46
- */
47
- const distanceOnSide = (side, offsets, gap) => {
48
- const agreeing = offsets.filter(offset => side === 'before' ? offset < 0 : offset > 0);
49
- return agreeing.length > 0 ? mm(Math.abs(mean(agreeing))) : gap;
50
- };
51
- /**
52
- * How far each event has to move for the placements to hold:
53
- * an aligned event takes the onset of its reference, one placed before
54
- * or after its reference keeps its place where the measurement has it
55
- * on that side and is moved there where it does not, and a paired
56
- * event follows its partner so that the distance between the two is
57
- * kept. Events that stay where they are do not appear.
58
- */
59
- const displacementsOf = (events, offsetsBetween, gap) => {
60
- const byId = new Map(events.map(event => [event.id, event]));
61
- /** Where an event comes to lie once its statement and those of its references hold. */
62
- const placedOnsetOf = (event, visited = new Set()) => {
63
- const measured = event.horizontal.from;
64
- const placement = placementsOf(event)[0];
65
- const reference = placement && byId.get(idOf(placement.reference));
66
- if (!placement || !reference || reference.id === event.id || visited.has(event.id))
67
- return measured;
68
- const referenceOnset = placedOnsetOf(reference, new Set([...visited, event.id]));
69
- const distance = (side) => distanceOnSide(side, offsetsBetween(event.id, reference.id), gap);
70
- switch (placement.relation) {
71
- case 'alignedWith': return referenceOnset;
72
- case 'before': return measured < referenceOnset ? measured : subtract(referenceOnset, distance('before'));
73
- case 'after': return measured > referenceOnset ? measured : add(referenceOnset, distance('after'));
74
- }
75
- };
76
- const displacements = new Map(events
77
- .map((event) => [event, subtract(placedOnsetOf(event), event.horizontal.from)])
78
- .filter(([, distance]) => distance !== 0));
79
- pairsAmong(events).forEach(([one, other]) => {
80
- const shared = displacements.get(one) ?? displacements.get(other);
81
- if (shared === undefined)
82
- return;
83
- displacements.set(one, shared);
84
- displacements.set(other, shared);
85
- });
86
- return displacements;
87
- };
88
- /** The earliest of the times, or the beginning of the roll where there are none. */
89
- const earliestOf = (times) => times.length === 0 ? seconds(0) : times.reduce((soonest, at) => at < soonest ? at : soonest);
90
- /**
91
- * A version of the edition, performed: the symbols are negotiated into
92
- * placed events, the reproducing system plays them, and the result goes
93
- * out as MIDI in which every note and pedal step is labelled with the
94
- * symbol it performs.
95
- */
96
- export class Emulation {
97
- constructor(system, options = system.defaultOptions) {
98
- Object.defineProperty(this, "system", {
99
- enumerable: true,
100
- configurable: true,
101
- writable: true,
102
- value: void 0
103
- });
104
- Object.defineProperty(this, "options", {
105
- enumerable: true,
106
- configurable: true,
107
- writable: true,
108
- value: void 0
109
- });
110
- Object.defineProperty(this, "midiEvents", {
111
- enumerable: true,
112
- configurable: true,
113
- writable: true,
114
- value: []
115
- });
116
- // sorted list of events with the negotiated assumptions already applied
117
- Object.defineProperty(this, "negotiatedEvents", {
118
- enumerable: true,
119
- configurable: true,
120
- writable: true,
121
- value: []
122
- });
123
- Object.defineProperty(this, "curves", {
124
- enumerable: true,
125
- configurable: true,
126
- writable: true,
127
- value: []
128
- });
129
- Object.defineProperty(this, "source", {
130
- enumerable: true,
131
- configurable: true,
132
- writable: true,
133
- value: void 0
134
- });
135
- this.system = system;
136
- this.options = options;
137
- }
138
- /**
139
- * Moves the negotiated events to where their statements put them.
140
- * The view supplies the copies, whose measurements decide how far
141
- * before or after its reference a command goes, and a punch
142
- * diameter, or a millimetre, where no copy agrees with a statement.
143
- */
144
- applyConstraints(view) {
145
- const gap = meanPunchDiameterOf(view) ?? mm(1);
146
- const offsets = (follower, reference) => offsetsBetween(view, follower, reference);
147
- displacementsOf(this.negotiatedEvents, offsets, gap).forEach((distance, event) => {
148
- event.horizontal.from = add(event.horizontal.from, distance);
149
- event.horizontal.to = add(event.horizontal.to, distance);
150
- });
151
- }
152
- emulateVersion(version, view, { range, skipToFirstNote = false } = {}) {
153
- this.source = version.id;
154
- /** A note plays only where its onset falls in the range; expressions play throughout. */
155
- const inScope = (event) => !range || event.type !== 'note' || (event.horizontal.from > range[0] && event.horizontal.from < range[1]);
156
- this.negotiatedEvents =
157
- view.snapshot(version.id)
158
- .filter(isCommand)
159
- .map(symbol => view.simplifySymbol(symbol, this.system.trackerBar))
160
- .filter(event => event !== null)
161
- .filter(inScope);
162
- if (this.negotiatedEvents.length === 0) {
163
- this.midiEvents = [];
164
- this.curves = [];
165
- return this.midiEvents;
166
- }
167
- this.applyConstraints(view);
168
- const performance = this.system.perform(this.negotiatedEvents, this.options, propertiesOf(view, version));
169
- this.curves = performance.curves;
170
- const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
171
- const origin = skipToFirstNote ? earliestOf(onsets) : seconds(0);
172
- this.midiEvents = performance.events
173
- .map(event => ({ ...event, at: subtract(event.at, origin) }))
174
- .filter(event => event.at >= 0)
175
- .sort((a, b) => a.at - b.at);
176
- return this.midiEvents;
177
- }
178
- findEventsPerforming(id) {
179
- return this.midiEvents.filter(event => event.performs.id === id);
180
- }
181
- asMIDI() {
182
- const TICKS_PER_SECOND = 1000;
183
- const events = [];
184
- this.midiEvents.sort((a, b) => a.at - b.at);
185
- const text = (text, deltaTime = 0) => ({ type: 'meta', subtype: 'text', text, deltaTime });
186
- const controller = (controllerType, value, deltaTime = 0) => ({ type: 'channel', subtype: 'controller', controllerType, value, deltaTime, channel: 0 });
187
- const controllerOf = (event) => event.type === 'damper' ? MIDIControlEvents.SUSTAIN : MIDIControlEvents.SOFT_PEDAL;
188
- events.push(text(`linked-rolls (${this.system.name})`));
189
- if (this.source) {
190
- events.push(text(this.source));
191
- }
192
- for (const [key, value] of Object.entries(this.options)) {
193
- events.push(text(`${key}=${typeof value === 'object' ? JSON.stringify(value) : value}`));
194
- }
195
- events.push({
196
- type: 'meta',
197
- subtype: 'setTempo',
198
- microsecondsPerBeat: 1000000,
199
- deltaTime: 0
200
- });
201
- // both pedals start at rest
202
- events.push(controller(MIDIControlEvents.SUSTAIN, 0), controller(MIDIControlEvents.SOFT_PEDAL, 0));
203
- // a pedal step is labelled with its command only where that
204
- // command changes, so the file is not swamped with labels
205
- const lastCause = {};
206
- let currentTick = 0;
207
- for (const event of this.midiEvents) {
208
- const tick = Math.round(event.at * TICKS_PER_SECOND);
209
- const deltaTime = tick - currentTick;
210
- currentTick = tick;
211
- if (event.type === 'noteOn') {
212
- events.push(text(event.performs.id, deltaTime));
213
- events.push({
214
- type: 'channel',
215
- subtype: 'noteOn',
216
- noteNumber: event.pitch,
217
- velocity: +event.velocity.toFixed(0),
218
- deltaTime: 0,
219
- channel: 0
220
- });
221
- }
222
- else if (event.type === 'noteOff') {
223
- events.push({
224
- type: 'channel',
225
- subtype: 'noteOff',
226
- noteNumber: event.pitch,
227
- velocity: 127,
228
- deltaTime,
229
- channel: 0
230
- });
231
- }
232
- else {
233
- const labelled = lastCause[event.type] === event.performs.id;
234
- lastCause[event.type] = event.performs.id;
235
- if (!labelled) {
236
- events.push(text(event.performs.id, deltaTime));
237
- }
238
- events.push(controller(controllerOf(event), event.value, labelled ? deltaTime : 0));
239
- }
240
- }
241
- return {
242
- header: {
243
- ticksPerBeat: TICKS_PER_SECOND,
244
- formatType: 0,
245
- trackCount: 1
246
- },
247
- tracks: [events]
248
- };
249
- }
250
- }