linked-rolls 0.56.0 → 0.58.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 (131) hide show
  1. package/README.md +54 -361
  2. package/lib/{constraints.d.ts → analysis/constraints.d.ts} +1 -1
  3. package/lib/{constraints.js → analysis/constraints.js} +9 -8
  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} +4 -4
  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} +6 -6
  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} +22 -51
  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.d.ts → io/migrate.d.ts} +9 -0
  35. package/lib/{migrate.js → io/migrate.js} +97 -54
  36. package/lib/{Agent.d.ts → model/Agent.d.ts} +1 -1
  37. package/lib/{Assumption.d.ts → model/Assumption.d.ts} +1 -1
  38. package/lib/{ConditionState.d.ts → model/ConditionState.d.ts} +1 -2
  39. package/lib/{Edit.d.ts → model/Edit.d.ts} +1 -1
  40. package/lib/{Edition.d.ts → model/Edition.d.ts} +2 -2
  41. package/lib/{Feature.d.ts → model/Feature.d.ts} +3 -1
  42. package/lib/{Feature.js → model/Feature.js} +2 -0
  43. package/lib/{FeatureSource.d.ts → model/FeatureSource.d.ts} +1 -1
  44. package/lib/{Perforator.d.ts → model/Perforator.d.ts} +1 -1
  45. package/lib/{RollCopy.d.ts → model/RollCopy.d.ts} +5 -12
  46. package/lib/{RollCopy.js → model/RollCopy.js} +3 -5
  47. package/lib/{Symbol.d.ts → model/Symbol.d.ts} +1 -1
  48. package/lib/{Version.d.ts → model/Version.d.ts} +11 -3
  49. package/lib/{Version.js → model/Version.js} +24 -1
  50. package/lib/{vocabulary.js → model/vocabulary.js} +2 -2
  51. package/lib/ops/beliefs.d.ts +9 -0
  52. package/lib/ops/beliefs.js +31 -0
  53. package/lib/ops/collation.d.ts +62 -0
  54. package/lib/ops/collation.js +228 -0
  55. package/lib/ops/commands.d.ts +9 -0
  56. package/lib/ops/commands.js +29 -0
  57. package/lib/ops/copies.d.ts +74 -0
  58. package/lib/ops/copies.js +135 -0
  59. package/lib/ops/draft.d.ts +59 -0
  60. package/lib/ops/draft.js +81 -0
  61. package/lib/ops/edits.d.ts +10 -0
  62. package/lib/ops/edits.js +82 -0
  63. package/lib/ops/features.d.ts +84 -0
  64. package/lib/ops/features.js +296 -0
  65. package/lib/ops/forget.d.ts +31 -0
  66. package/lib/ops/forget.js +110 -0
  67. package/lib/ops/immutable.d.ts +16 -0
  68. package/lib/ops/immutable.js +46 -0
  69. package/lib/ops/index.d.ts +13 -0
  70. package/lib/ops/index.js +7 -0
  71. package/lib/ops/versions.d.ts +33 -0
  72. package/lib/ops/versions.js +95 -0
  73. package/lib/readers/AtonParser.js +5 -7
  74. package/lib/readers/phillipsEroll.d.ts +3 -3
  75. package/lib/readers/phillipsEroll.js +4 -5
  76. package/lib/readers/spencerBar.d.ts +2 -2
  77. package/lib/readers/spencerBar.js +2 -3
  78. package/lib/readers/stanfordAton.d.ts +3 -3
  79. package/lib/readers/stanfordAton.js +2 -4
  80. package/lib/schema.json +11 -151
  81. package/lib/spec/context.json +2 -12
  82. package/lib/{ReproducingSystem.d.ts → systems/ReproducingSystem.d.ts} +3 -3
  83. package/lib/{TrackerBar.d.ts → systems/TrackerBar.d.ts} +48 -8
  84. package/lib/{TrackerBar.js → systems/TrackerBar.js} +11 -2
  85. package/lib/systems/index.d.ts +8 -2
  86. package/lib/systems/index.js +7 -1
  87. package/lib/systems/welte.d.ts +100 -0
  88. package/lib/systems/welte.js +119 -0
  89. package/lib/systems/welteLicensee/bar.d.ts +1 -1
  90. package/lib/systems/welteLicensee/bar.js +5 -2
  91. package/lib/systems/welteLicensee/system.d.ts +1 -1
  92. package/lib/systems/welteLicensee/system.js +1 -1
  93. package/lib/systems/welteT100/bar.d.ts +27 -1
  94. package/lib/systems/welteT100/bar.js +44 -2
  95. package/lib/systems/welteT100/system.d.ts +6 -39
  96. package/lib/systems/welteT100/system.js +19 -128
  97. package/lib/systems/welteT98/bar.d.ts +1 -1
  98. package/lib/systems/welteT98/bar.js +18 -2
  99. package/lib/systems/welteT98/system.d.ts +20 -38
  100. package/lib/systems/welteT98/system.js +20 -121
  101. package/lib/validate.d.ts +1 -1
  102. package/lib/{EditionView.d.ts → view/EditionView.d.ts} +38 -48
  103. package/lib/{EditionView.js → view/EditionView.js} +90 -96
  104. package/package.json +7 -2
  105. package/lib/Emulation.d.ts +0 -36
  106. package/lib/Emulation.js +0 -250
  107. package/lib/editionOps.d.ts +0 -260
  108. package/lib/editionOps.js +0 -1081
  109. package/lib/importJsonLd.js +0 -62
  110. /package/lib/{Agent.js → model/Agent.js} +0 -0
  111. /package/lib/{Assumption.js → model/Assumption.js} +0 -0
  112. /package/lib/{ConditionState.js → model/ConditionState.js} +0 -0
  113. /package/lib/{Edit.js → model/Edit.js} +0 -0
  114. /package/lib/{Edition.js → model/Edition.js} +0 -0
  115. /package/lib/{FeatureSource.js → model/FeatureSource.js} +0 -0
  116. /package/lib/{Perforator.js → model/Perforator.js} +0 -0
  117. /package/lib/{Quantity.d.ts → model/Quantity.d.ts} +0 -0
  118. /package/lib/{Quantity.js → model/Quantity.js} +0 -0
  119. /package/lib/{Symbol.js → model/Symbol.js} +0 -0
  120. /package/lib/{TrackCalibration.d.ts → model/TrackCalibration.d.ts} +0 -0
  121. /package/lib/{TrackCalibration.js → model/TrackCalibration.js} +0 -0
  122. /package/lib/{notes.d.ts → model/notes.d.ts} +0 -0
  123. /package/lib/{notes.js → model/notes.js} +0 -0
  124. /package/lib/{procedures.d.ts → model/procedures.d.ts} +0 -0
  125. /package/lib/{procedures.js → model/procedures.js} +0 -0
  126. /package/lib/{vocabulary.d.ts → model/vocabulary.d.ts} +0 -0
  127. /package/lib/{sorted.d.ts → shared/sorted.d.ts} +0 -0
  128. /package/lib/{sorted.js → shared/sorted.js} +0 -0
  129. /package/lib/{utils.d.ts → shared/utils.d.ts} +0 -0
  130. /package/lib/{utils.js → shared/utils.js} +0 -0
  131. /package/lib/{ReproducingSystem.js → systems/ReproducingSystem.js} +0 -0
@@ -1,9 +1,10 @@
1
- import { geometryInMm, Grid, levelChanges, mezzoforteTravel, paperSeconds, pedalBrushing, pedalDefaults, ROWS_PER_MM, travelBetweenRails, WELTE_T98_SPOOL, } from "welte-mignon-emulator";
1
+ import { pedalBrushing, pedalDefaults, ROWS_PER_MM, WELTE_T98_SPOOL, } from "welte-mignon-emulator";
2
2
  import { aperturePorts, CHAIN_GAP_MM, DERIVED, GENUINE, instrumentT98Of, labelOf, nuanceOf, PUNCH_T98_MM, pneumaticT98Model, rewindAt, runPedals, sforzandoPianoLift, STARTING_VALUES, TRACKER_BORE_T98_MM, } from "welte-mignon-emulator/t98";
3
3
  import { welteT98 } from "./bar.js";
4
- import { inCentimeters, mm, seconds, track } from "../../Quantity.js";
5
- import { partitionPoint } from "../../sorted.js";
6
- import { defaultVelocityMap, velocityOf } from "../velocity.js";
4
+ import { mm, seconds, track } from "../../model/Quantity.js";
5
+ import { defaultVelocityMap } from "../velocity.js";
6
+ import { performWelte, sameParameters } from "../welte.js";
7
+ export { secondsAt } from "../welte.js";
7
8
  /**
8
9
  * The instruments the emulator offers for the green Welte, in three groups that
9
10
  * answer three different questions and must not be listed as one.
@@ -37,8 +38,6 @@ export const instrumentNames = [
37
38
  ...Object.keys(DERIVED).map(derived => ({ derived })),
38
39
  { unfitted: 'starting-values' }
39
40
  ];
40
- const sameParameters = (a, b) => Object.keys(a).length === Object.keys(b).length
41
- && Object.entries(a).every(([name, value]) => b[name] === value);
42
41
  /** The instrument a pair of nuancing constants belongs to, if it is one. */
43
42
  export const instrumentNameOf = (nuance) => instrumentNames.find(name => {
44
43
  const instrument = instrumentT98Of(name);
@@ -86,98 +85,10 @@ const CODES = {
86
85
  SoftPedal: 'hammerRail'
87
86
  };
88
87
  const isWelteT98ExpressionType = (type) => Object.hasOwn(CODES, type);
89
- const codeOf = (expressionType) => isWelteT98ExpressionType(expressionType) ? CODES[expressionType] : undefined;
90
- /** Paper the grid runs on past the last hole, so that a final pedal release completes. */
91
- const RUN_OUT = mm(100);
92
- const isNote = (event) => event.type === 'note';
93
- const isExpression = (event) => event.type === 'expression';
94
- const paperOf = (toOwnPaper) => ({
95
- rowOf: place => place * toOwnPaper * ROWS_PER_MM,
96
- placeOfRow: row => mm(row / (toOwnPaper * ROWS_PER_MM)),
97
- paperOfRow: row => mm(row / ROWS_PER_MM)
98
- });
99
- /** When the spool brings a place on the roll to the tracker bar. */
100
- export const secondsAt = (spool, place) => seconds(paperSeconds(spool, inCentimeters(place)));
101
- const halfOf = (note, division) => note.vertical.from >= division ? 'treble' : 'bass';
102
- const readingOf = (paper) => (event) => {
103
- const control = codeOf(event.expressionType);
104
- if (!control)
105
- return undefined;
106
- const half = event.scope;
107
- return {
108
- event,
109
- punch: {
110
- half,
111
- control,
112
- rowOn: paper.rowOf(event.horizontal.from),
113
- rowOff: paper.rowOf(event.horizontal.to)
114
- }
115
- };
116
- };
117
- const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
118
- /**
119
- * One sample per row of the scan the constants were fitted on, from the
120
- * beginning of the roll to a little past the last hole.
121
- */
122
- const gridOver = (events, spool, paper) => {
123
- const last = mm(events.reduce((furthest, event) => Math.max(furthest, event.horizontal.to), 0));
124
- const length = Math.ceil(paper.rowOf(last) + RUN_OUT * ROWS_PER_MM) + 1;
125
- const times = new Float64Array(length).map((_, row) => secondsAt(spool, paper.paperOfRow(row)));
126
- return new Grid(0, times);
127
- };
128
- const nuanceCurves = (grid, ports, samples, options) => {
129
- const instrument = `Welte-Mignon T-98, ${labelOf(options.instrument)}`;
130
- const curveOf = (half) => {
131
- const params = options.nuance[half];
132
- const output = pneumaticT98Model.run({ grid, half, ports }, params);
133
- const travel = travelBetweenRails(output, params);
134
- const hook = clamp(mezzoforteTravel(params), 0.01, 0.99);
135
- return {
136
- ...samples,
137
- name: half,
138
- kind: 'dynamics',
139
- instrument,
140
- travel,
141
- velocity: travel.map(value => velocityOf(value, hook, options.velocity))
142
- };
143
- };
144
- return { bass: curveOf('bass'), treble: curveOf('treble') };
145
- };
146
- const pedalCurves = (grid, ports, samples, options) => {
147
- const travel = runPedals({ grid, ports }, options.pedals);
148
- return {
149
- damper: { ...samples, name: 'damper', kind: 'pedal', travel: travel.damper },
150
- hammerRail: { ...samples, name: 'hammerRail', kind: 'pedal', travel: travel.hammerRail }
151
- };
152
- };
153
- const performNotes = (events, grid, nuance, options, paper) => events
154
- .filter(isNote)
155
- .flatMap((note) => {
156
- const curve = nuance[halfOf(note, options.division)];
157
- const velocity = curve.velocity[grid.indexOfRow(paper.rowOf(note.horizontal.from))];
158
- return [
159
- { type: 'noteOn', performs: note, pitch: note.pitch, velocity, at: secondsAt(options.spool, note.horizontal.from) },
160
- { type: 'noteOff', performs: note, pitch: note.pitch, velocity: 127, at: secondsAt(options.spool, note.horizontal.to) }
161
- ];
162
- });
163
- /**
164
- * The travel of one pedal as controller steps, each attributed to the last
165
- * perforation of that pedal the tracker bar has reached.
166
- */
167
- const performPedal = (type, curve, grid, readings, mode) => {
168
- if (readings.length === 0)
169
- return [];
170
- const ordered = readings.toSorted((a, b) => a.punch.rowOn - b.punch.rowOn);
171
- const causeOf = (row) => ordered[Math.max(partitionPoint(ordered, reading => reading.punch.rowOn <= row) - 1, 0)].event;
172
- return levelChanges(curve.travel, { mode })
173
- .filter(change => change.index > 0)
174
- .map(change => ({
175
- type,
176
- performs: causeOf(grid.rowAt(change.index)),
177
- value: change.value,
178
- at: seconds(curve.seconds[change.index])
179
- }));
180
- };
88
+ /** The punch a T-98 expression is read as: its control, held for as long as it runs. */
89
+ const punchOf = (event, rows) => isWelteT98ExpressionType(event.expressionType)
90
+ ? { half: event.scope, control: CODES[event.expressionType], ...rows }
91
+ : undefined;
181
92
  /**
182
93
  * The row at which the Abstellbalg trips and the roll goes back, or the end of
183
94
  * the grid where it never does. The shut-off hangs on the bass sforzando-piano
@@ -199,36 +110,24 @@ const cutCurve = (curve, rows) => ({
199
110
  ...(curve.kind === 'dynamics' ? { velocity: curve.velocity.slice(0, rows) } : {})
200
111
  });
201
112
  /**
202
- * The edition's tempo adjustment is left aside, as on the T-100: it is stated as
203
- * a paper speed, and what the spool holds constant is its rate of revolution.
113
+ * The shared Welte performance, cut off where the rewind takes hold unless
114
+ * the options ignore it.
204
115
  */
205
116
  const perform = (events, options, roll) => {
206
- const paper = paperOf(roll.toOwnPaper ?? 1);
207
- const readings = events
208
- .filter(isExpression)
209
- .map(readingOf(paper))
210
- .filter((reading) => reading !== undefined);
211
- const grid = gridOver(events, options.spool, paper);
212
- const geometry = geometryInMm(roll.punchDiameter ?? options.punchDiameter, options.trackerBore);
213
117
  const gap = options.chainGap * ROWS_PER_MM;
214
- const ports = aperturePorts(grid, readings.map(reading => reading.punch), geometry, gap);
215
- const samples = {
216
- place: grid.seconds.map((_, row) => paper.placeOfRow(row)),
217
- seconds: grid.seconds
218
- };
219
- const nuance = nuanceCurves(grid, ports, samples, options);
220
- const pedals = pedalCurves(grid, ports, samples, options);
221
- const readingsOf = (control) => readings.filter(reading => reading.punch.control === control);
118
+ const { grid, ports, events: performed, curves } = performWelte(events, options, roll, {
119
+ punchOf,
120
+ portsOf: (grid, punches, geometry) => aperturePorts(grid, punches, geometry, gap),
121
+ model: pneumaticT98Model,
122
+ runPedals,
123
+ instrument: `Welte-Mignon T-98, ${labelOf(options.instrument)}`
124
+ });
222
125
  const stops = rewindRow(grid, ports, options);
223
126
  const until = seconds(grid.seconds[Math.min(stops, grid.length - 1)]);
224
127
  const rows = Math.min(stops + 1, grid.length);
225
128
  return {
226
- events: truncated([
227
- ...performNotes(events, grid, nuance, options, paper),
228
- ...performPedal('damper', pedals.damper, grid, readingsOf('sustainPedal'), options.pedalMode),
229
- ...performPedal('hammerRail', pedals.hammerRail, grid, readingsOf('hammerRail'), options.pedalMode)
230
- ], until),
231
- curves: [nuance.bass, nuance.treble, pedals.damper, pedals.hammerRail].map(curve => cutCurve(curve, rows))
129
+ events: truncated(performed, until),
130
+ curves: curves.map(curve => cutCurve(curve, rows))
232
131
  };
233
132
  };
234
133
  /**
package/lib/validate.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type ErrorObject } from "ajv";
2
- import { Edition } from "./Edition.js";
2
+ import { Edition } from "./model/Edition.js";
3
3
  /**
4
4
  * Holds a document against the edition schema. `errors` says what the
5
5
  * last call found wrong, and is empty where it found nothing.
@@ -1,19 +1,13 @@
1
- import { Edition } from "./Edition.js";
2
- import { HorizontalSpan, FeatureOrPatch } 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 {
12
10
  readonly edition: Edition;
13
- /**
14
- * Map from id to object
15
- */
16
- private readonly byId;
17
11
  /**
18
12
  * Map from id to its path within the edition
19
13
  */
@@ -24,17 +18,46 @@ export declare class EditionView {
24
18
  private readonly links;
25
19
  /** Built when a copy is first asked for, the walk not recording it. */
26
20
  private copiesByFeature?;
21
+ /**
22
+ * One index for each kind of entity, built from where the edition
23
+ * holds it. The index an entity is found in says what it is, so a
24
+ * lookup needs neither a type discriminator nor a cast.
25
+ */
26
+ private readonly versionsById;
27
+ private readonly copiesById;
28
+ private readonly symbolsById;
29
+ private featuresById?;
27
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. */
28
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
+ */
29
38
  indexObjects(): void;
30
- get<T>(anyId: string): T | undefined;
31
- getAll<T>(anyIds: readonly string[]): T[];
39
+ version(id: string): Readonly<Version> | undefined;
40
+ copy(id: string): Readonly<RollCopy> | undefined;
41
+ symbol(id: string): Readonly<AnySymbol> | undefined;
42
+ /** The symbols under the ids, leaving out an id that names none. */
43
+ symbols(ids: readonly string[]): Readonly<AnySymbol>[];
44
+ /** A feature or a patch on any copy, one a patch bears included. */
45
+ feature(id: string): Readonly<NestedFeature> | undefined;
46
+ /** The features and patches under the ids, leaving out an id that names none. */
47
+ features(ids: readonly string[]): Readonly<NestedFeature>[];
32
48
  getPath(anyId: string): Path | undefined;
33
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
+ */
34
55
  travelUp(versionId: string, callback: (version: Readonly<Version>) => void): void;
35
56
  /** The version and its ancestors along the principal line, from the version up to the root. */
36
57
  lineageOf(versionId: string): Readonly<Version>[];
37
- carriersOf(symbol: AnySymbol): Readonly<FeatureOrPatch>[];
58
+ carriersOf(symbol: AnySymbol): Readonly<NestedFeature>[];
59
+ /** The carriers that state a place of their own, which a feature on a patch does not. */
60
+ placedCarriersOf(symbol: AnySymbol): Readonly<FeatureOrPatch>[];
38
61
  /** The copy a feature sits on, a patch and everything it bears included. */
39
62
  copyOf(featureId: string): Readonly<RollCopy> | undefined;
40
63
  /**
@@ -46,29 +69,6 @@ export declare class EditionView {
46
69
  * `mergeFeatures` asks before it reads them as one.
47
70
  */
48
71
  actOf(featureId: string): Readonly<ProductionEvent | Modification> | undefined;
49
- /**
50
- * How a place on the edition's shared axis relates to the paper of
51
- * this version: place × factor = millimetres of its own paper.
52
- *
53
- * Copies cut for different systems are scaled onto one axis so that
54
- * they can be collated at all, which leaves a version of another
55
- * system carrying places in the axis copy's millimetres. A
56
- * performance needs the paper the roll actually ran on, and the
57
- * factor is the inverse of the scale `alignCopy` recorded.
58
- *
59
- * It is read only from the copies of the version's own system, since
60
- * under the shared axis a green version's notes are carried by red
61
- * copies too and those say nothing about green paper. A copy whose
62
- * scale is put down to its own paper having stretched is left out as
63
- * well: that is a fact about the one exemplar, not about the speed
64
- * the system's rolls were cut at. Where what remains disagrees,
65
- * `constraintProblems` reports it rather than averaging it away.
66
- */
67
- toOwnPaperOf(version: Readonly<Version>): number | undefined;
68
- /** The scales of the version's own copies that are not their own paper stretch. */
69
- speedScalesIn(version: Readonly<Version>): number[];
70
- /** The copies of the version's own system that carry any of its symbols. */
71
- copiesOwning(version: Readonly<Version>): Readonly<RollCopy>[];
72
72
  /** The version the given one's text is read against, by its principal derivation. */
73
73
  predecessorOf(versionId: string): Readonly<Version> | undefined;
74
74
  /**
@@ -100,14 +100,4 @@ export declare class EditionView {
100
100
  withGenerations(): Array<Version & {
101
101
  generation: number;
102
102
  }>;
103
- /**
104
- * The symbol as a performance needs it: where it lies, and the
105
- * position the performing bar reads it on.
106
- *
107
- * Nothing where that bar reads nothing of it, which is the case a
108
- * transfer between systems leaves behind: a red `ForzandoOn` a
109
- * green version still inherits cannot be performed on a green
110
- * machine, and an edit has yet to say what took its place.
111
- */
112
- simplifySymbol(symbol: Note | Expression, bar: TrackerBar): NegotiatedEvent | null;
113
103
  }
@@ -1,9 +1,8 @@
1
- import { 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) {
@@ -15,6 +14,7 @@ export const getAt = (path, obj) => {
15
14
  };
16
15
  /** Keys under which an object names others by id, singly or in a list. */
17
16
  const referenceKeys = ['delete', 'comprehends', 'motivation', 'premises', 'used'];
17
+ const byIdIn = (entities) => new Map(entities.map(entity => [entity.id, entity]));
18
18
  const isObject = (v) => v !== null && typeof v === "object";
19
19
  /** What a reference may state besides the id: a belief about it, and the tolerance a derivation was collated at. */
20
20
  const referenceOwnKeys = new Set(['id', '@annotation', 'collationTolerance']);
@@ -34,15 +34,6 @@ export class EditionView {
34
34
  writable: true,
35
35
  value: void 0
36
36
  });
37
- /**
38
- * Map from id to object
39
- */
40
- Object.defineProperty(this, "byId", {
41
- enumerable: true,
42
- configurable: true,
43
- writable: true,
44
- value: new Map()
45
- });
46
37
  /**
47
38
  * Map from id to its path within the edition
48
39
  */
@@ -68,14 +59,53 @@ export class EditionView {
68
59
  writable: true,
69
60
  value: void 0
70
61
  });
62
+ /**
63
+ * One index for each kind of entity, built from where the edition
64
+ * holds it. The index an entity is found in says what it is, so a
65
+ * lookup needs neither a type discriminator nor a cast.
66
+ */
67
+ Object.defineProperty(this, "versionsById", {
68
+ enumerable: true,
69
+ configurable: true,
70
+ writable: true,
71
+ value: void 0
72
+ });
73
+ Object.defineProperty(this, "copiesById", {
74
+ enumerable: true,
75
+ configurable: true,
76
+ writable: true,
77
+ value: void 0
78
+ });
79
+ Object.defineProperty(this, "symbolsById", {
80
+ enumerable: true,
81
+ configurable: true,
82
+ writable: true,
83
+ value: void 0
84
+ });
85
+ Object.defineProperty(this, "featuresById", {
86
+ enumerable: true,
87
+ configurable: true,
88
+ writable: true,
89
+ value: void 0
90
+ });
71
91
  this.edition = edition;
92
+ this.versionsById = byIdIn(edition.versions);
93
+ this.copiesById = byIdIn(edition.copies);
94
+ this.symbolsById = byIdIn(edition.versions.flatMap(insertedBy));
72
95
  this.indexObjects();
73
96
  }
97
+ /** What stands at the path, or null where nothing does. A zero or an empty string stands there as well. */
74
98
  atPath(path) {
75
- const node = getAt(path, this.edition);
76
- return node || null;
99
+ return getAt(path, this.edition) ?? null;
77
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
+ */
78
106
  indexObjects() {
107
+ this.paths.clear();
108
+ this.links.clear();
79
109
  const visited = new WeakSet();
80
110
  const link = (id, trail) => {
81
111
  const trails = this.links.get(id);
@@ -89,8 +119,7 @@ export class EditionView {
89
119
  return;
90
120
  if (isReferenceOnly(keys))
91
121
  link(record.id, { key: 'id', up: trail });
92
- else if (!this.byId.has(record.id)) {
93
- this.byId.set(record.id, record);
122
+ else if (!this.paths.has(record.id)) {
94
123
  this.paths.set(record.id, trail);
95
124
  }
96
125
  };
@@ -120,11 +149,27 @@ export class EditionView {
120
149
  };
121
150
  traverse(this.edition, null);
122
151
  }
123
- get(anyId) {
124
- return this.byId.get(anyId);
152
+ version(id) {
153
+ return this.versionsById.get(id);
154
+ }
155
+ copy(id) {
156
+ return this.copiesById.get(id);
157
+ }
158
+ symbol(id) {
159
+ return this.symbolsById.get(id);
160
+ }
161
+ /** The symbols under the ids, leaving out an id that names none. */
162
+ symbols(ids) {
163
+ return ids.flatMap(id => this.symbol(id) ?? []);
125
164
  }
126
- getAll(anyIds) {
127
- return anyIds.map(id => this.get(id)).filter((v) => !!v);
165
+ /** A feature or a patch on any copy, one a patch bears included. */
166
+ feature(id) {
167
+ this.featuresById ?? (this.featuresById = byIdIn(this.edition.copies.flatMap(copy => featuresOf(copy).flatMap(withBorneFeatures))));
168
+ return this.featuresById.get(id);
169
+ }
170
+ /** The features and patches under the ids, leaving out an id that names none. */
171
+ features(ids) {
172
+ return ids.flatMap(id => this.feature(id) ?? []);
128
173
  }
129
174
  getPath(anyId) {
130
175
  const trail = this.paths.get(anyId);
@@ -133,14 +178,19 @@ export class EditionView {
133
178
  linksTo(anyId) {
134
179
  return (this.links.get(anyId) ?? []).map(pathOf);
135
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
+ */
136
186
  travelUp(versionId, callback) {
137
- const v = this.get(versionId);
138
- if (!v)
139
- return;
140
- callback(v);
141
- const principal = principalDerivationOf(v);
142
- if (principal) {
143
- 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));
144
194
  }
145
195
  }
146
196
  /** The version and its ancestors along the principal line, from the version up to the root. */
@@ -150,7 +200,11 @@ export class EditionView {
150
200
  return lineage;
151
201
  }
152
202
  carriersOf(symbol) {
153
- return this.getAll(idsOf(symbol.carriers));
203
+ return this.features(idsOf(symbol.carriers));
204
+ }
205
+ /** The carriers that state a place of their own, which a feature on a patch does not. */
206
+ placedCarriersOf(symbol) {
207
+ return this.carriersOf(symbol).filter(isPlaced);
154
208
  }
155
209
  /** The copy a feature sits on, a patch and everything it bears included. */
156
210
  copyOf(featureId) {
@@ -178,51 +232,11 @@ export class EditionView {
178
232
  return copy.production;
179
233
  return copy.modifications.find(act => states(featuresMadeBy(act)));
180
234
  }
181
- /**
182
- * How a place on the edition's shared axis relates to the paper of
183
- * this version: place × factor = millimetres of its own paper.
184
- *
185
- * Copies cut for different systems are scaled onto one axis so that
186
- * they can be collated at all, which leaves a version of another
187
- * system carrying places in the axis copy's millimetres. A
188
- * performance needs the paper the roll actually ran on, and the
189
- * factor is the inverse of the scale `alignCopy` recorded.
190
- *
191
- * It is read only from the copies of the version's own system, since
192
- * under the shared axis a green version's notes are carried by red
193
- * copies too and those say nothing about green paper. A copy whose
194
- * scale is put down to its own paper having stretched is left out as
195
- * well: that is a fact about the one exemplar, not about the speed
196
- * the system's rolls were cut at. Where what remains disagrees,
197
- * `constraintProblems` reports it rather than averaging it away.
198
- */
199
- toOwnPaperOf(version) {
200
- const scales = this.speedScalesIn(version);
201
- return scales.length === 1 ? 1 / scales[0] : undefined;
202
- }
203
- /** The scales of the version's own copies that are not their own paper stretch. */
204
- speedScalesIn(version) {
205
- return [...new Set(this.copiesOwning(version)
206
- .filter(copy => !copy.conditions.some(isPaperStretch))
207
- .map(copy => copy.measurements.scale)
208
- .filter((scale) => scale !== undefined && scale > 0))];
209
- }
210
- /** The copies of the version's own system that carry any of its symbols. */
211
- copiesOwning(version) {
212
- const system = systemIdOf(version.system);
213
- const carrying = new Set(this.snapshot(version.id)
214
- .flatMap(symbol => idsOf(symbol.carriers))
215
- .flatMap(id => {
216
- const copy = this.copyOf(id);
217
- return copy ? [copy.id] : [];
218
- }));
219
- return this.edition.copies.filter(copy => carrying.has(copy.id) && systemIdOf(copy.production?.system) === system);
220
- }
221
235
  /** The version the given one's text is read against, by its principal derivation. */
222
236
  predecessorOf(versionId) {
223
- const v = this.get(versionId);
237
+ const v = this.version(versionId);
224
238
  const principal = v && principalDerivationOf(v);
225
- return principal && this.get(idOf(principal));
239
+ return principal && this.version(idOf(principal));
226
240
  }
227
241
  /**
228
242
  * Where along the roll the symbol lies, as its carriers put it, or
@@ -238,7 +252,7 @@ export class EditionView {
238
252
  * position a semitone away on either bar.
239
253
  */
240
254
  placeOf(symbol) {
241
- const carriers = this.getAll(idsOf(symbol.carriers));
255
+ const carriers = this.placedCarriersOf(symbol);
242
256
  if (carriers.length === 0)
243
257
  return;
244
258
  return {
@@ -282,7 +296,7 @@ export class EditionView {
282
296
  if (inStack.has(id)) {
283
297
  throw new Error(`Cycle detected involving node '${id}'. Check parentId links.`);
284
298
  }
285
- const node = this.get(id);
299
+ const node = this.version(id);
286
300
  if (!node) {
287
301
  memo.set(id, 0);
288
302
  return 0;
@@ -294,7 +308,7 @@ export class EditionView {
294
308
  if (basedOn === undefined) {
295
309
  gen = 0; // root
296
310
  }
297
- else if (!this.get(basedOn)) {
311
+ else if (!this.version(basedOn)) {
298
312
  // Orphaned parent reference — treat boundary as root
299
313
  gen = 0;
300
314
  }
@@ -312,24 +326,4 @@ export class EditionView {
312
326
  }));
313
327
  return withGen;
314
328
  }
315
- /**
316
- * The symbol as a performance needs it: where it lies, and the
317
- * position the performing bar reads it on.
318
- *
319
- * Nothing where that bar reads nothing of it, which is the case a
320
- * transfer between systems leaves behind: a red `ForzandoOn` a
321
- * green version still inherits cannot be performed on a green
322
- * machine, and an edit has yet to say what took its place.
323
- */
324
- simplifySymbol(symbol, bar) {
325
- const horizontal = this.placeOf(symbol);
326
- const position = bar.positionOf(symbol);
327
- if (!horizontal || position === undefined)
328
- return null;
329
- return {
330
- ...symbol,
331
- horizontal,
332
- vertical: { unit: 'track', from: position }
333
- };
334
- }
335
329
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.56.0",
3
+ "version": "0.58.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
- }