linked-rolls 0.19.1 → 0.21.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 (49) hide show
  1. package/README.md +31 -6
  2. package/lib/Collation.d.ts +1 -3
  3. package/lib/Collation.js +9 -11
  4. package/lib/Edition.d.ts +1 -9
  5. package/lib/EditionView.d.ts +54 -6
  6. package/lib/EditionView.js +93 -20
  7. package/lib/Emulation.js +5 -4
  8. package/lib/ReproducingSystem.d.ts +38 -2
  9. package/lib/RollCopy.d.ts +25 -2
  10. package/lib/RollCopy.js +30 -4
  11. package/lib/TrackerBar.d.ts +57 -2
  12. package/lib/TrackerBar.js +35 -17
  13. package/lib/Version.d.ts +49 -1
  14. package/lib/asJsonLd.js +23 -8
  15. package/lib/constraints.d.ts +8 -19
  16. package/lib/constraints.js +71 -15
  17. package/lib/context.d.ts +10 -1
  18. package/lib/context.js +10 -1
  19. package/lib/editionOps.d.ts +3 -1
  20. package/lib/editionOps.js +77 -15
  21. package/lib/importJsonLd.js +4 -2
  22. package/lib/index.d.ts +1 -0
  23. package/lib/index.js +1 -0
  24. package/lib/migrate.js +85 -11
  25. package/lib/readers/phillipsEroll.d.ts +4 -6
  26. package/lib/readers/phillipsEroll.js +6 -7
  27. package/lib/readers/spencerBar.d.ts +4 -5
  28. package/lib/readers/spencerBar.js +7 -8
  29. package/lib/readers/stanfordAton.d.ts +4 -7
  30. package/lib/readers/stanfordAton.js +4 -5
  31. package/lib/reservations.d.ts +1 -1
  32. package/lib/reservations.js +24 -2
  33. package/lib/schema.json +27 -5
  34. package/lib/spec/context.json +1 -0
  35. package/lib/spec/welte-licensee.context.json +10 -0
  36. package/lib/spec/welte-t98.context.json +10 -0
  37. package/lib/substitution.d.ts +73 -0
  38. package/lib/substitution.js +175 -0
  39. package/lib/systems/velocity.d.ts +34 -0
  40. package/lib/systems/velocity.js +36 -0
  41. package/lib/systems/welteLicensee/system.d.ts +46 -0
  42. package/lib/systems/welteLicensee/system.js +60 -0
  43. package/lib/systems/welteT100/system.d.ts +19 -16
  44. package/lib/systems/welteT100/system.js +42 -47
  45. package/lib/systems/welteT98/bar.d.ts +15 -2
  46. package/lib/systems/welteT98/bar.js +31 -3
  47. package/lib/systems/welteT98/system.d.ts +119 -0
  48. package/lib/systems/welteT98/system.js +245 -0
  49. package/package.json +12 -4
package/README.md CHANGED
@@ -79,7 +79,7 @@ tracker bar and a `perform` function; the core does not depend on any one
79
79
  instrument's model.
80
80
 
81
81
  The first system is the red Welte, `linked-rolls/welte-t100`, built on
82
- [welte-t100-emulator](https://github.com/pfefferniels/welte-t100): the
82
+ [welte-mignon-emulator](https://github.com/pfefferniels/welte-t100): the
83
83
  take-up spool sets the time axis, the Nuancierbälge fill through their
84
84
  conduits and are arrested by the Mezzoforte pin, and the two pedals travel
85
85
  rather than switch. The nuancing constants come as `instruments`: the
@@ -103,11 +103,36 @@ emulation.emulateVersion(version, view)
103
103
  const midi = emulation.asMIDI()
104
104
  ```
105
105
 
106
- The emulator is an optional peer dependency: an application that uses the
107
- T-100 system installs `welte-t100-emulator` itself, and one that only
108
- reads editions does not need it. For development on both at once, `npm
109
- link` a checkout of [welte-t100](https://github.com/pfefferniels/welte-t100)
110
- into this repository.
106
+ The second is the green Welte, `linked-rolls/welte-t98`. Everything
107
+ downstream of the relay is the same mechanism — Hagmann has the nuancing
108
+ unit built the same for both tracker scales (p. 96) and what differs is
109
+ in front of it: each function lasts exactly as long as its own perforation
110
+ runs over the glide block rather than latching until a cancel line is read,
111
+ four conduits stand on one bellows and their drives add as flows, the
112
+ crescendo's ceiling is the balance of its throttle against a permanently
113
+ open bleed rather than a cap, the two pedals sit on the opposite edges of
114
+ the paper, and a long perforation on the bass sforzando-piano line sends
115
+ the roll back. Its constants are **not fitted**: `instruments.genuine` and
116
+ `instruments.derived` are empty until their fits are run, and until then a
117
+ playback runs on unfitted starting values, which every curve says in its
118
+ own `instrument` field.
119
+
120
+ Both systems share one velocity map, and `DynamicsCurve.travel` means the
121
+ same thing on both: the position on the printed ordinate of Welte's own
122
+ ruled band, 0 at that half's P.P. gridline and 1 at the shared F.F. line.
123
+ That is what lets a red issue and a green issue of one recording be
124
+ compared at all, since the band is ruled the same way on both.
125
+
126
+ ```ts
127
+ import { welteT98System } from 'linked-rolls/welte-t98'
128
+ ```
129
+
130
+ The emulator is an optional peer dependency: an application that uses one
131
+ of the Welte systems installs `welte-mignon-emulator` itself, and one that
132
+ only reads editions does not need it. For development on both at once,
133
+ `npm link` a checkout of
134
+ [welte-t100](https://github.com/pfefferniels/welte-t100) into this
135
+ repository.
111
136
 
112
137
  ### Alignment, order and pairing
113
138
 
@@ -13,9 +13,7 @@ export interface CollationTolerance {
13
13
  }
14
14
  export declare const defaultCollationTolerance: CollationTolerance;
15
15
  /** Where a symbol lies along the roll, as its carriers put it, or nothing for a symbol without a place. */
16
- export type Locate = (symbol: AnySymbol) => Readonly<{
17
- horizontal: HorizontalSpan;
18
- }> | undefined;
16
+ export type Locate = (symbol: AnySymbol) => Readonly<HorizontalSpan> | undefined;
19
17
  /**
20
18
  * Two symbols collate when they are of one kind, say the same thing
21
19
  * (pitch, or expression type and scope), and lie at about the same
package/lib/Collation.js CHANGED
@@ -1,17 +1,15 @@
1
+ import { keyOf } from "./TrackerBar";
1
2
  import { distance, mm } from "./Quantity";
2
3
  import { partitionPoint } from "./sorted";
3
4
  export const defaultCollationTolerance = { toleranceStart: mm(5), toleranceEnd: mm(5) };
4
5
  /**
5
6
  * What a symbol says, as a key: the pitch of a note, the type and scope
6
- * of an expression. Symbols collate within one key only.
7
+ * of an expression. Symbols collate within one key only. This is the
8
+ * same key the tracker bars are indexed by, so two symbols collate
9
+ * exactly where two bars would read them as the same thing, which is
10
+ * what carries a note across a transfer between systems.
7
11
  */
8
- const kindOf = (symbol) => {
9
- switch (symbol.type) {
10
- case 'note': return `note ${symbol.pitch}`;
11
- case 'expression': return `expression ${symbol.scope} ${symbol.expressionType}`;
12
- case 'text': return 'text';
13
- }
14
- };
12
+ const kindOf = (symbol) => symbol.type === 'text' ? 'text' : keyOf(symbol);
15
13
  const nearby = (here, there, tolerance) => distance(here.from, there.from) <= tolerance.toleranceStart
16
14
  && distance(here.to, there.to) <= tolerance.toleranceEnd;
17
15
  /**
@@ -22,12 +20,12 @@ const nearby = (here, there, tolerance) => distance(here.from, there.from) <= to
22
20
  export const isCollatable = (a, b, locate, tolerance = defaultCollationTolerance) => {
23
21
  if (kindOf(a) !== kindOf(b))
24
22
  return false;
25
- const here = locate(a)?.horizontal;
26
- const there = locate(b)?.horizontal;
23
+ const here = locate(a);
24
+ const there = locate(b);
27
25
  return here !== undefined && there !== undefined && nearby(here, there, tolerance);
28
26
  };
29
27
  const placed = (symbols, locate) => symbols.flatMap((symbol, index) => {
30
- const horizontal = locate(symbol)?.horizontal;
28
+ const horizontal = locate(symbol);
31
29
  return horizontal ? [{ symbol, index, horizontal }] : [];
32
30
  });
33
31
  const groupBy = (items, keyOf) => items.reduce((groups, item) => {
package/lib/Edition.d.ts CHANGED
@@ -2,7 +2,7 @@ import { RollCopy } from "./RollCopy";
2
2
  import { Version } from "./Version";
3
3
  import { CollationTolerance } from "./Collation";
4
4
  import { DateAssignment, ObjectAssumption } from "./Assumption";
5
- import { Concept, Editor, Person, Place } from "./Agent";
5
+ import { Editor, Person, Place } from "./Agent";
6
6
  import { RollTempo } from "./ReproducingSystem";
7
7
  /**
8
8
  * This type describes the creation of an edition,
@@ -91,14 +91,6 @@ export interface Roll {
91
91
  * @see dcterms:identifier
92
92
  */
93
93
  catalogueNumber: string;
94
- /**
95
- * The reproducing system the roll was cut for. A system the
96
- * type vocabulary knows carries the IRI of its concept as `id`,
97
- * from which the export takes the system's own context, so that
98
- * the expression types are read as that system's.
99
- * @see crm:P2 has type
100
- */
101
- system: Concept;
102
94
  /**
103
95
  * @see lrmoo:R19i was realised through
104
96
  */
@@ -1,8 +1,10 @@
1
1
  import { Edition } from "./Edition";
2
- import { HorizontalSpan, VerticalSpan, AnyFeature } from "./Feature";
2
+ import { HorizontalSpan, AnyFeature } from "./Feature";
3
3
  import { AnySymbol, Expression, Note } from "./Symbol";
4
4
  import { Version } from "./Version";
5
5
  import { NegotiatedEvent } from "./ReproducingSystem";
6
+ import { TrackerBar } from "./TrackerBar";
7
+ import { RollCopy } from "./RollCopy";
6
8
  import { Millimeters } from "./Quantity";
7
9
  export type Path = (string | number)[];
8
10
  export declare const getAt: <T>(path: Path, obj: unknown) => T | undefined;
@@ -20,6 +22,8 @@ export declare class EditionView {
20
22
  * Map from id to paths where it is referenced
21
23
  */
22
24
  private readonly links;
25
+ /** Built when a copy is first asked for, the walk not recording it. */
26
+ private copiesByFeature?;
23
27
  constructor(edition: Edition);
24
28
  atPath<T>(path: Path): T | null;
25
29
  indexObjects(): void;
@@ -31,11 +35,46 @@ export declare class EditionView {
31
35
  /** The version and its ancestors, from the version up to the root. */
32
36
  private lineageOf;
33
37
  carriersOf(symbol: AnySymbol): Readonly<AnyFeature>[];
38
+ /** The copy a feature sits on, a patch and everything it bears included. */
39
+ copyOf(featureId: string): Readonly<RollCopy> | undefined;
40
+ /**
41
+ * How a place on the edition's shared axis relates to the paper of
42
+ * this version: place × factor = millimetres of its own paper.
43
+ *
44
+ * Copies cut for different systems are scaled onto one axis so that
45
+ * they can be collated at all, which leaves a version of another
46
+ * system carrying places in the axis copy's millimetres. A
47
+ * performance needs the paper the roll actually ran on, and the
48
+ * factor is the inverse of the scale `alignCopy` recorded.
49
+ *
50
+ * It is read only from the copies of the version's own system, since
51
+ * under the shared axis a green version's notes are carried by red
52
+ * copies too and those say nothing about green paper. A copy whose
53
+ * scale is put down to its own paper having stretched is left out as
54
+ * well: that is a fact about the one exemplar, not about the speed
55
+ * the system's rolls were cut at. Where what remains disagrees,
56
+ * `constraintProblems` reports it rather than averaging it away.
57
+ */
58
+ toOwnPaperOf(version: Readonly<Version>): number | undefined;
59
+ /** The scales of the version's own copies that are not their own paper stretch. */
60
+ speedScalesIn(version: Readonly<Version>): number[];
61
+ /** The copies of the version's own system that carry any of its symbols. */
62
+ copiesOwning(version: Readonly<Version>): Readonly<RollCopy>[];
34
63
  predecessorOf(versionId: string): Readonly<Version> | undefined;
35
- dimensionOf(symbol: AnySymbol): Readonly<{
36
- horizontal: HorizontalSpan;
37
- vertical: VerticalSpan;
38
- }> | undefined;
64
+ /**
65
+ * Where along the roll the symbol lies, as its carriers put it, or
66
+ * nothing for a symbol no copy carries.
67
+ *
68
+ * Only the place is measured. Which track the symbol sits on is not
69
+ * a measurement but a question for a tracker bar, since a note of
70
+ * one pitch sits on exactly one position of a given bar: ask
71
+ * `bar.positionOf`. The carriers cannot answer it, because a copy
72
+ * cut for another system numbers its tracks differently, and one
73
+ * symbol may be carried by copies of both — averaging a red carrier
74
+ * on track 47 with a green one on 45 would give 46, a legal
75
+ * position a semitone away on either bar.
76
+ */
77
+ placeOf(symbol: AnySymbol): Readonly<HorizontalSpan> | undefined;
39
78
  /** Where the symbol begins, as the mean onset of its carriers, or nothing for a symbol without a place. */
40
79
  onsetOf(symbol: AnySymbol): Millimeters | undefined;
41
80
  /** The symbols by onset, those without a place first; symbols at one place keep their order. */
@@ -51,5 +90,14 @@ export declare class EditionView {
51
90
  withGenerations(): Array<Version & {
52
91
  generation: number;
53
92
  }>;
54
- simplifySymbol(symbol: Note | Expression): NegotiatedEvent | null;
93
+ /**
94
+ * The symbol as a performance needs it: where it lies, and the
95
+ * position the performing bar reads it on.
96
+ *
97
+ * Nothing where that bar reads nothing of it, which is the case a
98
+ * transfer between systems leaves behind: a red `ForzandoOn` a
99
+ * green version still inherits cannot be performed on a green
100
+ * machine, and an edit has yet to say what took its place.
101
+ */
102
+ simplifySymbol(symbol: Note | Expression, bar: TrackerBar): NegotiatedEvent | null;
55
103
  }
@@ -1,4 +1,7 @@
1
+ import { withBorneFeatures } from "./Feature";
1
2
  import { deletedBy, insertedBy } from "./Version";
3
+ import { systemIdOf } from "./TrackerBar";
4
+ import { isPaperStretch } from "./RollCopy";
2
5
  import { idOf, idsOf } from "./Assumption";
3
6
  import { mean } from "./Quantity";
4
7
  export const getAt = (path, obj) => {
@@ -58,6 +61,13 @@ export class EditionView {
58
61
  writable: true,
59
62
  value: new Map()
60
63
  });
64
+ /** Built when a copy is first asked for, the walk not recording it. */
65
+ Object.defineProperty(this, "copiesByFeature", {
66
+ enumerable: true,
67
+ configurable: true,
68
+ writable: true,
69
+ value: void 0
70
+ });
61
71
  this.edition = edition;
62
72
  this.indexObjects();
63
73
  }
@@ -141,35 +151,87 @@ export class EditionView {
141
151
  carriersOf(symbol) {
142
152
  return this.getAll(idsOf(symbol.carriers));
143
153
  }
154
+ /** The copy a feature sits on, a patch and everything it bears included. */
155
+ copyOf(featureId) {
156
+ if (!this.copiesByFeature) {
157
+ this.copiesByFeature = new Map(this.edition.copies.flatMap(copy => copy.features
158
+ .flatMap(withBorneFeatures)
159
+ .map(feature => [feature.id, copy])));
160
+ }
161
+ return this.copiesByFeature.get(featureId);
162
+ }
163
+ /**
164
+ * How a place on the edition's shared axis relates to the paper of
165
+ * this version: place × factor = millimetres of its own paper.
166
+ *
167
+ * Copies cut for different systems are scaled onto one axis so that
168
+ * they can be collated at all, which leaves a version of another
169
+ * system carrying places in the axis copy's millimetres. A
170
+ * performance needs the paper the roll actually ran on, and the
171
+ * factor is the inverse of the scale `alignCopy` recorded.
172
+ *
173
+ * It is read only from the copies of the version's own system, since
174
+ * under the shared axis a green version's notes are carried by red
175
+ * copies too and those say nothing about green paper. A copy whose
176
+ * scale is put down to its own paper having stretched is left out as
177
+ * well: that is a fact about the one exemplar, not about the speed
178
+ * the system's rolls were cut at. Where what remains disagrees,
179
+ * `constraintProblems` reports it rather than averaging it away.
180
+ */
181
+ toOwnPaperOf(version) {
182
+ const scales = this.speedScalesIn(version);
183
+ return scales.length === 1 ? 1 / scales[0] : undefined;
184
+ }
185
+ /** The scales of the version's own copies that are not their own paper stretch. */
186
+ speedScalesIn(version) {
187
+ return [...new Set(this.copiesOwning(version)
188
+ .filter(copy => !copy.conditions.some(isPaperStretch))
189
+ .map(copy => copy.measurements.scale)
190
+ .filter((scale) => scale !== undefined && scale > 0))];
191
+ }
192
+ /** The copies of the version's own system that carry any of its symbols. */
193
+ copiesOwning(version) {
194
+ const system = systemIdOf(version.system);
195
+ const carrying = new Set(this.snapshot(version.id)
196
+ .flatMap(symbol => idsOf(symbol.carriers))
197
+ .flatMap(id => {
198
+ const copy = this.copyOf(id);
199
+ return copy ? [copy.id] : [];
200
+ }));
201
+ return this.edition.copies.filter(copy => carrying.has(copy.id) && systemIdOf(copy.production?.system) === system);
202
+ }
144
203
  predecessorOf(versionId) {
145
204
  const v = this.get(versionId);
146
205
  if (!v?.basedOn)
147
206
  return;
148
207
  return this.get(idOf(v.basedOn));
149
208
  }
150
- dimensionOf(symbol) {
209
+ /**
210
+ * Where along the roll the symbol lies, as its carriers put it, or
211
+ * nothing for a symbol no copy carries.
212
+ *
213
+ * Only the place is measured. Which track the symbol sits on is not
214
+ * a measurement but a question for a tracker bar, since a note of
215
+ * one pitch sits on exactly one position of a given bar: ask
216
+ * `bar.positionOf`. The carriers cannot answer it, because a copy
217
+ * cut for another system numbers its tracks differently, and one
218
+ * symbol may be carried by copies of both — averaging a red carrier
219
+ * on track 47 with a green one on 45 would give 46, a legal
220
+ * position a semitone away on either bar.
221
+ */
222
+ placeOf(symbol) {
151
223
  const carriers = this.getAll(idsOf(symbol.carriers));
152
- if (carriers.length === 0) {
224
+ if (carriers.length === 0)
153
225
  return;
154
- }
155
- const farEnds = carriers.flatMap(carrier => carrier.vertical.to === undefined ? [] : [carrier.vertical.to]);
156
226
  return {
157
- horizontal: {
158
- unit: 'mm',
159
- from: mean(carriers.map(carrier => carrier.horizontal.from)),
160
- to: mean(carriers.map(carrier => carrier.horizontal.to))
161
- },
162
- vertical: {
163
- unit: 'track',
164
- from: mean(carriers.map(carrier => carrier.vertical.from)),
165
- ...(farEnds.length > 0 && { to: mean(farEnds) })
166
- }
227
+ unit: 'mm',
228
+ from: mean(carriers.map(carrier => carrier.horizontal.from)),
229
+ to: mean(carriers.map(carrier => carrier.horizontal.to))
167
230
  };
168
231
  }
169
232
  /** Where the symbol begins, as the mean onset of its carriers, or nothing for a symbol without a place. */
170
233
  onsetOf(symbol) {
171
- const carriers = this.carriersOf(symbol);
172
- return carriers.length > 0 ? mean(carriers.map(carrier => carrier.horizontal.from)) : undefined;
234
+ return this.placeOf(symbol)?.from;
173
235
  }
174
236
  /** The symbols by onset, those without a place first; symbols at one place keep their order. */
175
237
  inOrderOfPlace(symbols) {
@@ -231,13 +293,24 @@ export class EditionView {
231
293
  }));
232
294
  return withGen;
233
295
  }
234
- simplifySymbol(symbol) {
235
- const dim = this.dimensionOf(symbol);
236
- if (!symbol.carriers.length || !dim)
296
+ /**
297
+ * The symbol as a performance needs it: where it lies, and the
298
+ * position the performing bar reads it on.
299
+ *
300
+ * Nothing where that bar reads nothing of it, which is the case a
301
+ * transfer between systems leaves behind: a red `ForzandoOn` a
302
+ * green version still inherits cannot be performed on a green
303
+ * machine, and an edit has yet to say what took its place.
304
+ */
305
+ simplifySymbol(symbol, bar) {
306
+ const horizontal = this.placeOf(symbol);
307
+ const position = bar.positionOf(symbol);
308
+ if (!horizontal || position === undefined)
237
309
  return null;
238
310
  return {
239
311
  ...symbol,
240
- ...dim
312
+ horizontal,
313
+ vertical: { unit: 'track', from: position }
241
314
  };
242
315
  }
243
316
  }
package/lib/Emulation.js CHANGED
@@ -9,9 +9,10 @@ const punchDiameterOf = (view) => {
9
9
  .filter((value) => value !== undefined && value > 0);
10
10
  return measured.length > 0 ? mean(measured) : undefined;
11
11
  };
12
- const propertiesOf = (view) => ({
12
+ const propertiesOf = (view, version) => ({
13
13
  punchDiameter: punchDiameterOf(view),
14
- tempo: view.edition.tempoAdjustment
14
+ tempo: view.edition.tempoAdjustment,
15
+ toOwnPaper: view.toOwnPaperOf(version)
15
16
  });
16
17
  /** The onset a symbol has on each copy carrying it, by the copy's index, as the mean of its holes there. */
17
18
  const onsetsByCopy = (view, symbolId) => {
@@ -154,7 +155,7 @@ export class Emulation {
154
155
  this.negotiatedEvents =
155
156
  view.snapshot(version.id)
156
157
  .filter(isPerforation)
157
- .map(symbol => view.simplifySymbol(symbol))
158
+ .map(symbol => view.simplifySymbol(symbol, this.system.trackerBar))
158
159
  .filter(event => event !== null)
159
160
  .filter(inScope);
160
161
  if (this.negotiatedEvents.length === 0) {
@@ -163,7 +164,7 @@ export class Emulation {
163
164
  return this.midiEvents;
164
165
  }
165
166
  this.applyConstraints(view);
166
- const performance = this.system.perform(this.negotiatedEvents, this.options, propertiesOf(view));
167
+ const performance = this.system.perform(this.negotiatedEvents, this.options, propertiesOf(view, version));
167
168
  this.curves = performance.curves;
168
169
  const onsets = performance.events.filter(event => event.type === 'noteOn').map(event => event.at);
169
170
  const origin = skipToFirstNote ? earliestOf(onsets) : seconds(0);
@@ -63,8 +63,18 @@ interface CurveSamples {
63
63
  readonly place: Float64Array;
64
64
  readonly seconds: Float64Array;
65
65
  /**
66
- * Position between the two ends of a travel: 0 with a bellows open and
67
- * 1 with it closed, 0 with a pedal up and 1 with it down.
66
+ * Position on the printed ordinate of the roll's own ruled band: 0 at that
67
+ * half's P.P. gridline, 1 at the shared F.F. line, with M.F. at 0.5. For a
68
+ * pedal, 0 with it up and 1 with it down.
69
+ *
70
+ * Both Welte scales rule that band the same way — five rails, "P.P. M.F.
71
+ * F.F. M.F. P.P.", three named levels a side with the centre F.F. shared,
72
+ * measuring 20.0 mm between rails on red 3309 and on green Welte 184 alike —
73
+ * so a curve from one system and a curve from the other are in one unit, on
74
+ * Welte's authority rather than on ours. That is what lets a red issue and a
75
+ * green issue of one recording be compared at all. It is a unit of *drawn
76
+ * deflection*: whether the deflection is linear in the bellows' own travel is
77
+ * open, and is what the emulator's `scaleWarp` exists to answer.
68
78
  */
69
79
  readonly travel: Float64Array;
70
80
  }
@@ -72,6 +82,14 @@ interface CurveSamples {
72
82
  export type DynamicsCurve = CurveSamples & {
73
83
  readonly kind: 'dynamics';
74
84
  readonly velocity: Float64Array;
85
+ /**
86
+ * The instrument whose constants produced it, by name. A comparison plot
87
+ * must not be able to put two curves side by side without saying which
88
+ * instrument each came from: a T-98 fitted to a green roll's drawn line and
89
+ * a T-98 fitted to a red copy's curve answer different questions, and their
90
+ * difference is the point of the comparison.
91
+ */
92
+ readonly instrument: string;
75
93
  };
76
94
  export type PedalCurve = CurveSamples & {
77
95
  readonly kind: 'pedal';
@@ -83,6 +101,24 @@ export type RollProperties = {
83
101
  punchDiameter?: Millimeters;
84
102
  /** The tempo the edition adjusts the roll to, where it states one. */
85
103
  tempo?: RollTempo;
104
+ /**
105
+ * Place on the edition's shared axis × this = millimetres of the version's
106
+ * own paper. About 0.775 for a green issue of a red recording, 1 for a
107
+ * version on the axis it was measured on, which is the default.
108
+ *
109
+ * Versions of several systems share one place axis, so a green version's
110
+ * places are in red millimetres and would play about 29 % long if the
111
+ * spool read them as green paper. The factor is the inverse of the scale
112
+ * `alignCopy` recorded when it put the copy on the shared axis, taken from
113
+ * the copies of the version's own system: red copies carry a green
114
+ * version's notes too and say nothing about green paper.
115
+ *
116
+ * It scales **places only**. Punch diameter, the chain gap and the tracker
117
+ * bore are measured on the version's own paper already, and scaling them
118
+ * as well would put the aperture model out by the factor with nothing
119
+ * failing loudly.
120
+ */
121
+ toOwnPaper?: number;
86
122
  };
87
123
  export type Performance = {
88
124
  readonly events: readonly AnyPerformedRollFeature[];
package/lib/RollCopy.d.ts CHANGED
@@ -301,17 +301,40 @@ export interface RollCopy extends WithType<'RollCopy'>, WithId {
301
301
  */
302
302
  readFrom?: FeatureSource;
303
303
  }
304
+ /**
305
+ * Whether the condition is the stretch or shrinkage of the paper. A
306
+ * scale put down to this belongs to the one exemplar and says nothing
307
+ * about the speed its system's rolls were cut at.
308
+ */
309
+ export declare const isPaperStretch: (condition: RollConditionAssignment) => boolean;
310
+ /**
311
+ * The bar a copy is read by, which is the one it was cut for. A copy
312
+ * that names no system falls back to the T-100, as every copy was read
313
+ * before the systems were told apart; `reservationsAbout` says so.
314
+ */
315
+ export declare const barOf: (copy: Pick<RollCopy, 'production'>) => TrackerBar;
304
316
  /**
305
317
  * Reads the features of a copy as the tracker bar would read them.
306
318
  * Holes on a position the bar does not read carry no symbol and are
307
319
  * dropped, which is what happens physically as well.
320
+ *
321
+ * The bar is named rather than defaulted: a copy is read by its own
322
+ * bar, and reading a green copy with the red one is a silent semitone.
323
+ *
324
+ * Reading stops where the bar says the roll's content ends. What a
325
+ * scanner punched past the rewind is not the roll speaking: Julian
326
+ * Dyer's scan of the green 225 carries a staircase of test punches
327
+ * across nearly every track after its rewind, which read on as symbols
328
+ * would be 318 notes nobody played. The features stay on the copy,
329
+ * since they are really on the paper and are his calibration of his own
330
+ * scan; it is the reading that stops.
308
331
  */
309
- export declare function asSymbols(features: AnyFeature[], bar?: TrackerBar): AnySymbol[];
332
+ export declare function asSymbols(features: AnyFeature[], bar: TrackerBar): AnySymbol[];
310
333
  /**
311
334
  * The tracks a copy carries holes on that the tracker bar does not read.
312
335
  * A non-empty result usually means the scan is calibrated wrongly.
313
336
  */
314
- export declare function unreadTracks(features: AnyFeature[], bar?: TrackerBar): Map<Track, number>;
337
+ export declare function unreadTracks(features: AnyFeature[], bar: TrackerBar): Map<Track, number>;
315
338
  /**
316
339
  * Falls back to the way the scan geometry was reconstructed before the
317
340
  * calibration was recorded: the bass hard margin stood in for the phase
package/lib/RollCopy.js CHANGED
@@ -1,19 +1,45 @@
1
1
  import { v4 } from "uuid";
2
2
  import { welteT100 } from "./systems/welteT100/bar";
3
+ import { trackerBarOf } from "./systems";
3
4
  import { assignReference } from "./Assumption";
4
5
  import { px, track } from "./Quantity";
5
6
  export const rollConditions = [
6
7
  'general',
7
8
  'paper-stretch'
8
9
  ];
10
+ /**
11
+ * Whether the condition is the stretch or shrinkage of the paper. A
12
+ * scale put down to this belongs to the one exemplar and says nothing
13
+ * about the speed its system's rolls were cut at.
14
+ */
15
+ export const isPaperStretch = (condition) => condition.conditionType === 'paper-stretch';
16
+ /**
17
+ * The bar a copy is read by, which is the one it was cut for. A copy
18
+ * that names no system falls back to the T-100, as every copy was read
19
+ * before the systems were told apart; `reservationsAbout` says so.
20
+ */
21
+ export const barOf = (copy) => trackerBarOf(copy.production?.system) ?? welteT100;
9
22
  /**
10
23
  * Reads the features of a copy as the tracker bar would read them.
11
24
  * Holes on a position the bar does not read carry no symbol and are
12
25
  * dropped, which is what happens physically as well.
26
+ *
27
+ * The bar is named rather than defaulted: a copy is read by its own
28
+ * bar, and reading a green copy with the red one is a silent semitone.
29
+ *
30
+ * Reading stops where the bar says the roll's content ends. What a
31
+ * scanner punched past the rewind is not the roll speaking: Julian
32
+ * Dyer's scan of the green 225 carries a staircase of test punches
33
+ * across nearly every track after its rewind, which read on as symbols
34
+ * would be 318 notes nobody played. The features stay on the copy,
35
+ * since they are really on the paper and are his calibration of his own
36
+ * scan; it is the reading that stops.
13
37
  */
14
- export function asSymbols(features, bar = welteT100) {
15
- return features
16
- .filter(feature => feature.type === 'Hole')
38
+ export function asSymbols(features, bar) {
39
+ const holes = features.filter(feature => feature.type === 'Hole');
40
+ const end = bar.endsAt(holes);
41
+ return holes
42
+ .filter(feature => end === undefined || feature.horizontal.from <= end.at)
17
43
  .flatMap((feature) => {
18
44
  const meaning = bar.meaningOf(feature.vertical.from);
19
45
  if (!meaning)
@@ -29,7 +55,7 @@ export function asSymbols(features, bar = welteT100) {
29
55
  * The tracks a copy carries holes on that the tracker bar does not read.
30
56
  * A non-empty result usually means the scan is calibrated wrongly.
31
57
  */
32
- export function unreadTracks(features, bar = welteT100) {
58
+ export function unreadTracks(features, bar) {
33
59
  const counts = new Map();
34
60
  features
35
61
  .filter(feature => feature.type === 'Hole')
@@ -56,13 +56,39 @@ export interface TrackerBar {
56
56
  * of their own, as the Licensee's do, states none.
57
57
  */
58
58
  readonly paperSpeed?: SpeedMeasure;
59
+ /**
60
+ * Where the roll's own content ends, from its perforations alone, or
61
+ * `undefined` where nothing on the paper says. A copy that reads
62
+ * `undefined` runs to the end of whatever was scanned, which is a fact
63
+ * about the scan rather than about the roll and should be reported as one.
64
+ *
65
+ * Takes anything with a place and a position, so a reader can ask it of a
66
+ * copy's features before there are symbols, and a collation can ask it of
67
+ * the symbols afterwards.
68
+ */
69
+ endsAt(features: readonly PlacedOnBar[]): RollEnd | undefined;
59
70
  /** `undefined` for a position the bar does not read. */
60
71
  meaningOf(position: Track): TrackMeaning | undefined;
72
+ /**
73
+ * `meaningOf` inverted: the position this bar reads the meaning on,
74
+ * or `undefined` where it does not read it at all. A symbol's track
75
+ * is this rather than anything measured, since a note of one pitch
76
+ * sits on exactly one position of a given bar.
77
+ */
78
+ positionOf(meaning: TrackMeaning): Track | undefined;
61
79
  /** `undefined` for a position the bar does not read. */
62
80
  roleOf(position: Track): TrackRole | undefined;
63
81
  }
82
+ /**
83
+ * What a position says, as a key. Two bars read the same thing exactly
84
+ * where their keys agree, which is what lets a symbol cross from one
85
+ * system to another and what decides whether two symbols collate.
86
+ */
87
+ export declare const keyOf: (meaning: TrackMeaning) => string;
64
88
  /** The roll system a tracker bar belongs to, as the roll metadata states it. */
65
89
  export declare const systemOf: (bar: TrackerBar) => Concept;
90
+ /** The identifier of a system the type vocabulary knows, from the IRI naming it. */
91
+ export declare const systemIdIn: (id: string | undefined) => string | undefined;
66
92
  /** The identifier of a system the type vocabulary knows, from its concept. */
67
93
  export declare const systemIdOf: (system: Concept | undefined) => string | undefined;
68
94
  /**
@@ -70,6 +96,28 @@ export declare const systemIdOf: (system: Concept | undefined) => string | undef
70
96
  * in the bar's own 1-based numbering; `describeTrackerBar` gives them
71
97
  * their type.
72
98
  */
99
+ /**
100
+ * Where a roll's own content ends, as far as its perforations say.
101
+ *
102
+ * This is a question about the paper, not about the mechanism: it asks where
103
+ * the rewind is *punched*, not when the rewind pneumatic takes hold. The second
104
+ * is a matter of valve lift and belongs to whatever performs the roll. Keeping
105
+ * them apart is what lets collation and counting ask this without an emulator.
106
+ */
107
+ export type RollEnd = {
108
+ readonly at: Millimeters;
109
+ readonly because: 'rewind';
110
+ };
111
+ /** Anything carrying a place on the roll and a position on the bar. */
112
+ export type PlacedOnBar = {
113
+ readonly horizontal: {
114
+ readonly from: Millimeters;
115
+ readonly to: Millimeters;
116
+ };
117
+ readonly vertical: {
118
+ readonly from: Track;
119
+ };
120
+ };
73
121
  export interface TrackerBarSpec {
74
122
  id: string;
75
123
  name: string;
@@ -92,12 +140,19 @@ export interface TrackerBarSpec {
92
140
  rewindTrack?: number;
93
141
  /** The speed the system runs its rolls at, where the literature states one. */
94
142
  paperSpeed?: SpeedMeasure;
143
+ /**
144
+ * How long a perforation on a *shared* rewind position has to be before it
145
+ * is the rewind rather than the command the position usually carries. Only
146
+ * meaningful with `rewindTrack`: a system that gives the rewind a line of
147
+ * its own needs no threshold, since anything there is the rewind.
148
+ */
149
+ rewindHold?: Millimeters;
95
150
  }
96
151
  export declare const describeTrackerBar: (spec: TrackerBarSpec) => TrackerBar;
97
152
  /**
98
153
  * Puts a position of one bar onto the position of another that reads
99
154
  * the same thing, or nowhere when the other bar does not read it. This
100
- * is how a copy cut for one system takes its place in an edition of
101
- * another, as a Licensee re-cut does in an edition of a T-100 roll.
155
+ * is how a copy read in one system's numbering is put into another's,
156
+ * as the migration does for a Licensee copy stored on T-100 tracks.
102
157
  */
103
158
  export declare const translationBetween: (from: TrackerBar, to: TrackerBar) => (position: Track) => Track | undefined;