linked-rolls 0.19.1 → 0.20.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.
@@ -1,6 +1,6 @@
1
1
  import { v4 } from "uuid";
2
2
  import { AtonParser } from "./AtonParser";
3
- import { systemOf, translationBetween } from "../TrackerBar";
3
+ import { systemOf } from "../TrackerBar";
4
4
  import { welteT100 } from "../systems/welteT100/bar";
5
5
  import { inMillimeters, mean, mm, pixelsPerInch, px, subtract, track } from "../Quantity";
6
6
  /** Values in these files carry their unit as a suffix, e.g. "37.7646px". */
@@ -110,7 +110,7 @@ const measuredByOf = (rollinfo) => {
110
110
  date
111
111
  };
112
112
  };
113
- export function readFromStanfordAton(atonString, { trackShift, bar = welteT100, system = bar, scan } = {}) {
113
+ export function readFromStanfordAton(atonString, { trackShift, system = welteT100, scan } = {}) {
114
114
  const parser = new AtonParser();
115
115
  const json = parser.parse(atonString);
116
116
  const holes = json.ROLLINFO.HOLES.HOLE;
@@ -128,11 +128,10 @@ export function readFromStanfordAton(atonString, { trackShift, bar = welteT100,
128
128
  };
129
129
  const punchDiameter = punchDiameterOf(holes, dpi);
130
130
  const chains = chainsAmong([...holes, ...chainedBadHoles(listOf(json.ROLLINFO.BADHOLES?.HOLE), calibration)]);
131
- const onBar = translationBetween(system, bar);
132
131
  const features = chains
133
132
  .flatMap(({ hole, attack, release }) => {
134
- const position = onBar(track(+hole.TRACKER_HOLE + shift));
135
- if (position === undefined)
133
+ const position = track(+hole.TRACKER_HOLE + shift);
134
+ if (!system.meaningOf(position))
136
135
  return [];
137
136
  const column = readPx(hole.ORIGIN_COL);
138
137
  const columnWidth = readPx(hole.WIDTH_COL);
@@ -1,5 +1,5 @@
1
1
  import { RollCopy } from "./RollCopy";
2
- export declare const reservationTypes: readonly ['source-not-stated', 'source-undocumented', 'features-interpreted', 'no-physical-evidence', 'measurement-undocumented', 'not-calibrated'];
2
+ export declare const reservationTypes: readonly ['source-not-stated', 'source-undocumented', 'features-interpreted', 'no-physical-evidence', 'measurement-undocumented', 'not-calibrated', 'system-unknown'];
3
3
  export type ReservationType = typeof reservationTypes[number];
4
4
  /**
5
5
  * Something an edition cannot vouch for in one of its copies.
@@ -1,12 +1,14 @@
1
1
  import { bearsPhysicalEvidence, isMeasured, sourceLabels } from "./FeatureSource";
2
2
  import { calibrationOf } from "./RollCopy";
3
+ import { trackerBarOf } from "./systems";
3
4
  export const reservationTypes = [
4
5
  'source-not-stated',
5
6
  'source-undocumented',
6
7
  'features-interpreted',
7
8
  'no-physical-evidence',
8
9
  'measurement-undocumented',
9
- 'not-calibrated'
10
+ 'not-calibrated',
11
+ 'system-unknown'
10
12
  ];
11
13
  const sourceStated = copy => copy.readFrom ? undefined : {
12
14
  type: 'source-not-stated',
@@ -37,13 +39,33 @@ const calibrated = copy => calibrationOf(copy) ? undefined : {
37
39
  type: 'not-calibrated',
38
40
  note: 'The copy is not calibrated against the tracker bar, so its track positions rest on the numbering its source used.'
39
41
  };
42
+ /**
43
+ * The bar decides what every track on the copy means, so a copy the
44
+ * edition cannot place in a system is read by the T-100 for want of
45
+ * anything better. Between two Welte scales that is a semitone rather
46
+ * than a visible error, which is why it is said out loud.
47
+ */
48
+ const systemKnown = copy => {
49
+ const system = copy.production?.system;
50
+ if (system === undefined) {
51
+ return {
52
+ type: 'system-unknown',
53
+ note: 'The copy names no reproducing system, so it is read by the T-100 tracker bar.'
54
+ };
55
+ }
56
+ return trackerBarOf(system) ? undefined : {
57
+ type: 'system-unknown',
58
+ note: `The copy names ${system.name || 'a reproducing system'}, which the edition has no tracker bar for, so it is read by the T-100's.`
59
+ };
60
+ };
40
61
  const checks = [
41
62
  sourceStated,
42
63
  sourceDocumented,
43
64
  featuresMeasured,
44
65
  physicalEvidence,
45
66
  measurementDocumented,
46
- calibrated
67
+ calibrated,
68
+ systemKnown
47
69
  ];
48
70
  /**
49
71
  * What the edition cannot vouch for in a copy, in the order the
package/lib/schema.json CHANGED
@@ -1802,15 +1802,10 @@
1802
1802
  "recordingEvent": {
1803
1803
  "$ref": "#/definitions/RecordingEvent",
1804
1804
  "description": "[ontology: lrmoo:R19i was realised through]"
1805
- },
1806
- "system": {
1807
- "$ref": "#/definitions/Concept",
1808
- "description": "The reproducing system the roll was cut for. A system the type vocabulary knows carries the IRI of its concept as `id`, from which the export takes the system's own context, so that the expression types are read as that system's. [ontology: crm:P2 has type]"
1809
1805
  }
1810
1806
  },
1811
1807
  "required": [
1812
1808
  "catalogueNumber",
1813
- "system",
1814
1809
  "recordingEvent"
1815
1810
  ],
1816
1811
  "type": "object"
@@ -2089,6 +2084,10 @@
2089
2084
  "$ref": "#/definitions/Derivation",
2090
2085
  "description": "If no derivation is defined, it is assumed that this version represents the mother roll. [ontology: lrmoo:R76 is derivative of]"
2091
2086
  },
2087
+ "creation": {
2088
+ "$ref": "#/definitions/VersionCreation",
2089
+ "description": "The act that made this version, where it is known: who carried it out, when, and by what rule. [ontology: lrmoo:R17i was created by]"
2090
+ },
2092
2091
  "edits": {
2093
2092
  "description": "The list of edits that, applied to the base version, produce this version. [ontology: reo:involvedEdit]",
2094
2093
  "items": {
@@ -2107,6 +2106,10 @@
2107
2106
  "description": "A short siglum to identify the version, e.g. \"A\", \"B1\", \"B2_rev\", etc. [ontology: reo:siglum]",
2108
2107
  "type": "string"
2109
2108
  },
2109
+ "system": {
2110
+ "$ref": "#/definitions/Concept",
2111
+ "description": "The reproducing system this version is coded for. One roll was often issued for several of them, and a version is a reading in one system's words: its expression types are that system's vocabulary and its notes sit on that bar's positions. A system the type vocabulary knows carries the IRI of its concept as `id`, from which the export takes the system's own context. [ontology: crm:P2 has type]"
2112
+ },
2110
2113
  "versionType": {
2111
2114
  "$ref": "#/definitions/VersionType",
2112
2115
  "description": "Whether the version served as a master for reproductions or exists on one copy only. [ontology: crm:P2 has type]"
@@ -2127,11 +2130,30 @@
2127
2130
  "@id",
2128
2131
  "motivations",
2129
2132
  "siglum",
2133
+ "system",
2130
2134
  "@type",
2131
2135
  "versionType"
2132
2136
  ],
2133
2137
  "type": "object"
2134
2138
  },
2139
+ "VersionCreation": {
2140
+ "description": "How a version was made, where that is known and worth stating.\n\nA roll issued for another system was re-punched by an editor of the publisher's, and that was an editorial act rather than a conversion: Lawson names Kähle as the man who corrected second masters for the green system. The rule he worked by is the procedure named here, and the version's edits carry it out, so the mechanical part of a transfer is stated once instead of being spelled out per note. [ontology: lrmoo:F28 Expression Creation]",
2141
+ "properties": {
2142
+ "actor": {
2143
+ "$ref": "#/definitions/ActorAssignment",
2144
+ "description": "Who carried the act out. An `ObjectAssumption`, so an attribution can carry the belief it rests on and the reasons for it. [ontology: crm:P14 carried out by]"
2145
+ },
2146
+ "date": {
2147
+ "$ref": "#/definitions/DateAssignment",
2148
+ "description": "When it took place. [ontology: dcterms:date]"
2149
+ },
2150
+ "procedure": {
2151
+ "$ref": "#/definitions/Concept",
2152
+ "description": "The rule followed, as a term of the vocabulary: the notes stand at their pitch, the expression is re-spelled in the other system's words. [ontology: crm:P33 used specific technique]"
2153
+ }
2154
+ },
2155
+ "type": "object"
2156
+ },
2135
2157
  "VersionType": {
2136
2158
  "description": "The type of a version. An 'edition' version may serve as the master for several roll copies; a 'unicum' version exists only on one specific copy.",
2137
2159
  "enum": [
@@ -76,6 +76,7 @@
76
76
  "@type": "@id"
77
77
  },
78
78
  "creation": "lrmoo:R17i_was_created_by",
79
+ "procedure": "crm:P33_used_specific_technique",
79
80
  "editors": "crm:P14_carried_out_by",
80
81
  "publisher": "crm:P14_carried_out_by",
81
82
  "publicationDate": {
@@ -0,0 +1,10 @@
1
+ {
2
+ "@context": {
3
+ "@version": 1.1,
4
+ "expressionType": {
5
+ "@id": "http://www.cidoc-crm.org/cidoc-crm/P2_has_type",
6
+ "@type": "@vocab",
7
+ "@context": { "@vocab": "https://w3id.org/reo/type/welte-t100/" }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,73 @@
1
+ import { CollationTolerance, Locate } from "./Collation";
2
+ import { AnySymbol, Expression } from "./Symbol";
3
+ import { Millimeters, Track } from "./Quantity";
4
+ import { TrackerBar } from "./TrackerBar";
5
+ /**
6
+ * How a scale spells a command: as a perforation that turns a function
7
+ * on, one that cancels it, or one that holds it for as long as it lasts.
8
+ */
9
+ export type Spelling = 'on' | 'off' | 'held';
10
+ /**
11
+ * A command as what it operates rather than as the word a scale uses
12
+ * for it, so that two scales can be compared at all.
13
+ */
14
+ export interface Command {
15
+ /** The function operated, named the same wherever a scale has it. */
16
+ operates: string;
17
+ spelling: Spelling;
18
+ /**
19
+ * Whether the half of the keyboard the valve serves is part of the
20
+ * command. The dynamics are per half; the pedals are not, and the
21
+ * two Welte scales put them on opposite edges of the paper, so a
22
+ * rule that compared sides would leave every pedal unpaired.
23
+ */
24
+ sided: boolean;
25
+ }
26
+ /** What a command operates, or nothing for a word no scale here knows. */
27
+ export declare const commandOf: (expressionType: string) => Command | undefined;
28
+ /**
29
+ * A latched function of the older version and the held perforation of
30
+ * the newer one that stands for it.
31
+ */
32
+ export interface Substitution {
33
+ /** The perforation that turned the function on, and the one that cancelled it. */
34
+ replaced: readonly [Expression, Expression];
35
+ /**
36
+ * The perforations that say the same thing in the other scale's
37
+ * words. Usually one, but a held command is punched as a chain of
38
+ * round holes with paper bridges rather than as a slot, and a scan
39
+ * whose analysis reports those singly gives one symbol per punch.
40
+ */
41
+ by: readonly Expression[];
42
+ }
43
+ /**
44
+ * How wide a paper bridge may be for two held perforations to be one
45
+ * command. The T-98 punches a hold as a chain of round holes on a
46
+ * 2.66 mm grid with bridges of about a millimetre, so a scan read hole
47
+ * by hole shows a run where the paper shows one command.
48
+ */
49
+ export declare const defaultChainGap: import("./Quantity").Quantity<"mm">;
50
+ /**
51
+ * The position a bar reads its own command for the same function on,
52
+ * where it has no word for this one.
53
+ *
54
+ * A version keeps the perforations it does away with in its deletions,
55
+ * and those may be a scale the version is not coded for: a green
56
+ * version deletes the red `SlowCrescendoOn` it inherits. That has no
57
+ * position on the green bar, so drawing it as a perforation is out of
58
+ * the question, but an edit still has to be shown somewhere, and the
59
+ * lane the green scale gives the same function is where it belongs.
60
+ */
61
+ export declare const positionOfSameFunction: (bar: TrackerBar, symbol: Expression) => Track | undefined;
62
+ /**
63
+ * Where a held perforation of the newer version stands for a latched
64
+ * function of the older one: the same function, on the same side where
65
+ * the side counts, spanning the same stretch of the roll.
66
+ *
67
+ * Only unambiguous correspondences are reported. Where two held
68
+ * perforations answer one latched interval, or one answers two, none of
69
+ * them is reported and the editor is left to say what happened. Nothing
70
+ * is invented either: both sides are perforations somebody punched, and
71
+ * all this asserts is which stands for which.
72
+ */
73
+ export declare const substitutionsBetween: (own: readonly AnySymbol[], inherited: readonly AnySymbol[], locate: Locate, tolerance?: CollationTolerance, chainGap?: Millimeters) => Substitution[];
@@ -0,0 +1,175 @@
1
+ import { defaultCollationTolerance } from "./Collation";
2
+ import { distance, mm, track } from "./Quantity";
3
+ /**
4
+ * What each Welte command operates.
5
+ *
6
+ * This cannot be read off the names. The T-100 latches a function on
7
+ * with one perforation and cancels it with a second, while the T-98
8
+ * holds it for as long as one perforation lasts (Hagmann pp. 89 f. and
9
+ * 100–103; Phillips p. 121), and Welte renamed two of the functions
10
+ * between the scales: the red `SlowCrescendo` is the green `Crescendo`,
11
+ * and the red's single `Forzando` valve answers to two green ones that
12
+ * name the end they pull towards. So the correspondence is stated here
13
+ * rather than derived, and only two commands that operate one function
14
+ * can stand for each other.
15
+ *
16
+ * The T-100's `MotorOn`/`MotorOff`, `Rewind` and `ElectricCutOff` are
17
+ * left out on purpose: the green scale has no word for any of them, its
18
+ * motor switch being an automatic mercury contact (Skala-Rolle 98 §12)
19
+ * and its rewind riding on the bass sforzando-piano line. A red command
20
+ * of those kinds has no counterpart and stays a plain deletion.
21
+ */
22
+ const commands = {
23
+ MezzoforteOn: { operates: 'mezzoforte', spelling: 'on', sided: true },
24
+ MezzoforteOff: { operates: 'mezzoforte', spelling: 'off', sided: true },
25
+ SlowCrescendoOn: { operates: 'crescendo', spelling: 'on', sided: true },
26
+ SlowCrescendoOff: { operates: 'crescendo', spelling: 'off', sided: true },
27
+ ForzandoOn: { operates: 'sforzando', spelling: 'on', sided: true },
28
+ ForzandoOff: { operates: 'sforzando', spelling: 'off', sided: true },
29
+ SustainPedalOn: { operates: 'sustainPedal', spelling: 'on', sided: false },
30
+ SustainPedalOff: { operates: 'sustainPedal', spelling: 'off', sided: false },
31
+ SoftPedalOn: { operates: 'softPedal', spelling: 'on', sided: false },
32
+ SoftPedalOff: { operates: 'softPedal', spelling: 'off', sided: false },
33
+ Mezzoforte: { operates: 'mezzoforte', spelling: 'held', sided: true },
34
+ Crescendo: { operates: 'crescendo', spelling: 'held', sided: true },
35
+ SforzandoForte: { operates: 'sforzando', spelling: 'held', sided: true },
36
+ SforzandoPiano: { operates: 'sforzando', spelling: 'held', sided: true },
37
+ SustainPedal: { operates: 'sustainPedal', spelling: 'held', sided: false },
38
+ SoftPedal: { operates: 'softPedal', spelling: 'held', sided: false }
39
+ };
40
+ /** What a command operates, or nothing for a word no scale here knows. */
41
+ export const commandOf = (expressionType) => commands[expressionType];
42
+ /**
43
+ * How wide a paper bridge may be for two held perforations to be one
44
+ * command. The T-98 punches a hold as a chain of round holes on a
45
+ * 2.66 mm grid with bridges of about a millimetre, so a scan read hole
46
+ * by hole shows a run where the paper shows one command.
47
+ */
48
+ export const defaultChainGap = mm(3);
49
+ const isExpression = (symbol) => symbol.type === 'expression';
50
+ /** The key a command is matched on: the function, and the side where the side counts. */
51
+ const functionKey = (symbol, command) => command.sided ? `${command.operates} ${symbol.scope}` : command.operates;
52
+ /**
53
+ * The position a bar reads its own command for the same function on,
54
+ * where it has no word for this one.
55
+ *
56
+ * A version keeps the perforations it does away with in its deletions,
57
+ * and those may be a scale the version is not coded for: a green
58
+ * version deletes the red `SlowCrescendoOn` it inherits. That has no
59
+ * position on the green bar, so drawing it as a perforation is out of
60
+ * the question, but an edit still has to be shown somewhere, and the
61
+ * lane the green scale gives the same function is where it belongs.
62
+ */
63
+ export const positionOfSameFunction = (bar, symbol) => {
64
+ const wanted = commandOf(symbol.expressionType);
65
+ if (!wanted)
66
+ return undefined;
67
+ const positions = Array.from({ length: bar.trackCount }, (_, index) => track(index + 1));
68
+ return positions.find(position => {
69
+ const meaning = bar.meaningOf(position);
70
+ if (meaning?.type !== 'expression')
71
+ return false;
72
+ const command = commandOf(meaning.expressionType);
73
+ return command !== undefined
74
+ && command.operates === wanted.operates
75
+ && functionKey(meaning, command) === functionKey(symbol, wanted);
76
+ });
77
+ };
78
+ /**
79
+ * The intervals the older version latches: each perforation that turns
80
+ * a function on, with the one that cancels it next on the same line.
81
+ */
82
+ const latchedIn = (symbols, locate) => {
83
+ const byFunction = new Map();
84
+ symbols.filter(isExpression).forEach(symbol => {
85
+ const command = commandOf(symbol.expressionType);
86
+ const at = locate(symbol)?.from;
87
+ if (!command || command.spelling === 'held' || at === undefined)
88
+ return;
89
+ const key = functionKey(symbol, command);
90
+ const group = byFunction.get(key) ?? [];
91
+ group.push({ symbol, at, spelling: command.spelling });
92
+ byFunction.set(key, group);
93
+ });
94
+ return [...byFunction.values()].flatMap(group => {
95
+ const inOrder = [...group].sort((a, b) => a.at - b.at);
96
+ return inOrder.reduce((state, entry) => {
97
+ // A second On while the function already stands is
98
+ // redundant, and the interval began at the first, which
99
+ // is how a red copy reads that reaches the end of the
100
+ // roll with two Ons and one Off.
101
+ if (entry.spelling === 'on') {
102
+ return state.open ? state : { ...state, open: { on: entry.symbol, from: entry.at } };
103
+ }
104
+ return state.open
105
+ ? {
106
+ open: undefined,
107
+ closed: [...state.closed, {
108
+ on: state.open.on,
109
+ off: entry.symbol,
110
+ from: state.open.from,
111
+ to: entry.at,
112
+ key: functionKey(state.open.on, commandOf(state.open.on.expressionType))
113
+ }]
114
+ }
115
+ : state;
116
+ }, { closed: [] }).closed;
117
+ });
118
+ };
119
+ /**
120
+ * The commands the newer version holds, a chain of punches counting as
121
+ * the one command the paper shows rather than as a run of them.
122
+ */
123
+ const heldRunsIn = (symbols, locate, chainGap) => {
124
+ const byFunction = new Map();
125
+ symbols.filter(isExpression).forEach(symbol => {
126
+ const command = commandOf(symbol.expressionType);
127
+ const place = locate(symbol);
128
+ if (!command || command.spelling !== 'held' || !place)
129
+ return;
130
+ const key = functionKey(symbol, command);
131
+ const group = byFunction.get(key) ?? [];
132
+ group.push({ symbol, from: place.from, to: place.to });
133
+ byFunction.set(key, group);
134
+ });
135
+ return [...byFunction.entries()].flatMap(([key, group]) => [...group]
136
+ .sort((a, b) => a.from - b.from)
137
+ .reduce((runs, punch) => {
138
+ const open = runs[runs.length - 1];
139
+ if (open && punch.from - open.to <= chainGap) {
140
+ open.of.push(punch.symbol);
141
+ open.to = punch.to > open.to ? punch.to : open.to;
142
+ return runs;
143
+ }
144
+ return [...runs, { of: [punch.symbol], from: punch.from, to: punch.to, key }];
145
+ }, []));
146
+ };
147
+ /** The only item of the list, or nothing where there is none or a rival. */
148
+ const theOnly = (items) => items.length === 1 ? items[0] : undefined;
149
+ /**
150
+ * Where a held perforation of the newer version stands for a latched
151
+ * function of the older one: the same function, on the same side where
152
+ * the side counts, spanning the same stretch of the roll.
153
+ *
154
+ * Only unambiguous correspondences are reported. Where two held
155
+ * perforations answer one latched interval, or one answers two, none of
156
+ * them is reported and the editor is left to say what happened. Nothing
157
+ * is invented either: both sides are perforations somebody punched, and
158
+ * all this asserts is which stands for which.
159
+ */
160
+ export const substitutionsBetween = (own, inherited, locate, tolerance = defaultCollationTolerance, chainGap = defaultChainGap) => {
161
+ const latched = latchedIn(inherited, locate);
162
+ const spans = (run, interval) => distance(run.from, interval.from) <= tolerance.toleranceStart
163
+ && distance(run.to, interval.to) <= tolerance.toleranceEnd;
164
+ const candidates = heldRunsIn(own, locate, chainGap).flatMap(run => {
165
+ const answered = latched.filter(interval => interval.key === run.key && spans(run, interval));
166
+ const only = theOnly(answered);
167
+ return only ? [{ run, interval: only }] : [];
168
+ });
169
+ // A latched interval two runs both answer is ambiguous from its side
170
+ // as well, so neither of them is reported.
171
+ const claims = candidates.reduce((counts, { interval }) => counts.set(interval.on.id, (counts.get(interval.on.id) ?? 0) + 1), new Map());
172
+ return candidates
173
+ .filter(({ interval }) => claims.get(interval.on.id) === 1)
174
+ .map(({ run, interval }) => ({ replaced: [interval.on, interval.off], by: run.of }));
175
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linked-rolls",
3
- "version": "0.19.1",
3
+ "version": "0.20.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": {